]> git.decadent.org.uk Git - videolink.git/blob - webdvd.cpp
Added XML-escaping of VOB filenames.
[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 "link_iterator.hpp"
57 #include "pixbufs.hpp"
58 #include "style_sheets.hpp"
59 #include "temp_file.hpp"
60 #include "video.hpp"
61 #include "x_frame_buffer.hpp"
62 #include "xpcom_support.hpp"
63
64 using xpcom_support::check;
65
66 namespace
67 {
68     struct rectangle
69     {
70         int left, top;     // inclusive
71         int right, bottom; // exclusive
72
73         rectangle operator|=(const rectangle & other)
74             {
75                 if (other.empty())
76                 {
77                     // use current extents unchanged
78                 }
79                 else if (empty())
80                 {
81                     // use other extents
82                     *this = other;
83                 }
84                 else
85                 {
86                     // find rectangle enclosing both extents
87                     left = std::min(left, other.left);
88                     top = std::min(top, other.top);
89                     right = std::max(right, other.right);
90                     bottom = std::max(bottom, other.bottom);
91                 }
92
93                 return *this;
94             }
95
96         rectangle operator&=(const rectangle & other)
97             {
98                 // find rectangle enclosed in both extents
99                 left = std::max(left, other.left);
100                 top = std::max(top, other.top);
101                 right = std::max(left, std::min(right, other.right));
102                 bottom = std::max(top, std::min(bottom, other.bottom));
103                 return *this;
104             }
105
106         bool empty() const
107             {
108                 return left == right || bottom == top;
109             }
110     };
111
112     rectangle get_elem_rect(nsIDOMNSDocument * ns_doc,
113                             nsIDOMElement * elem)
114     {
115         rectangle result;
116
117         // Start with this element's bounding box
118         nsCOMPtr<nsIBoxObject> box;
119         check(ns_doc->GetBoxObjectFor(elem, getter_AddRefs(box)));
120         int width, height;
121         check(box->GetScreenX(&result.left));
122         check(box->GetScreenY(&result.top));
123         check(box->GetWidth(&width));
124         check(box->GetHeight(&height));
125         result.right = result.left + width;
126         result.bottom = result.top + height;
127
128         // Merge bounding boxes of all child elements
129         for (child_iterator it = child_iterator(elem), end; it != end; ++it)
130         {
131             nsCOMPtr<nsIDOMNode> child_node(*it);
132             PRUint16 child_type;
133             if (check(child_node->GetNodeType(&child_type)),
134                 child_type == nsIDOMNode::ELEMENT_NODE)
135             {
136                 nsCOMPtr<nsIDOMElement> child_elem(
137                     do_QueryInterface(child_node));
138                 result |= get_elem_rect(ns_doc, child_elem);
139             }
140         }
141
142         return result;
143     }
144
145
146     std::string xml_escape(const std::string & str)
147     {
148         std::string result;
149         std::size_t begin = 0;
150
151         for (;;)
152         {
153             std::size_t end = str.find_first_of("\"&'<>", begin);
154             result.append(str, begin, end - begin);
155             if (end == std::string::npos)
156                 return result;
157
158             const char * entity = NULL;
159             switch (str[end])
160             {
161             case '"':  entity = "&quot;"; break;
162             case '&':  entity = "&amp;";  break;
163             case '\'': entity = "&apos;"; break;
164             case '<':  entity = "&lt;";   break;
165             case '>':  entity = "&gt;";   break;
166             }
167             assert(entity);
168             result.append(entity);
169
170             begin = end + 1;
171         }
172     }
173
174     
175     struct dvd_contents
176     {
177         enum pgc_type { menu_pgc, title_pgc };
178         typedef std::pair<pgc_type, int> pgc_ref;
179
180         struct menu
181         {
182             menu()
183                     : vob_temp(new temp_file("webdvd-vob-"))
184                 {
185                     vob_temp->close();
186                 }
187
188             boost::shared_ptr<temp_file> vob_temp;
189             std::vector<pgc_ref> entries;
190         };
191
192         struct title
193         {
194             explicit title(const std::string & vob_list)
195                     : vob_list(vob_list)
196                 {}
197
198             std::string vob_list;
199         };
200
201         std::vector<menu> menus;
202         std::vector<title> titles;
203     };
204
205     class webdvd_window : public Gtk::Window
206     {
207     public:
208         webdvd_window(
209             const video::frame_params & frame_params,
210             const std::string & main_page_uri,
211             const std::string & output_dir);
212
213     private:
214         dvd_contents::pgc_ref add_menu(const std::string & uri);
215         dvd_contents::pgc_ref add_title(const std::string & uri);
216         void load_next_page();
217         void on_net_state_change(const char * uri, gint flags, guint status);
218         bool process_page();
219         void save_screenshot();
220         void process_links(nsIPresShell * pres_shell,
221                            nsIPresContext * pres_context,
222                            nsIDOMWindow * dom_window);
223         void generate_dvd();
224
225         video::frame_params frame_params_;
226         std::string output_dir_;
227         browser_widget browser_widget_;
228         nsCOMPtr<nsIStyleSheet> stylesheet_;
229
230         dvd_contents contents_;
231         typedef std::map<std::string, dvd_contents::pgc_ref> resource_map_type;
232         resource_map_type resource_map_;
233
234         std::queue<std::string> page_queue_;
235         bool pending_window_update_;
236         int pending_req_count_;
237         bool have_tweaked_page_;
238         std::auto_ptr<temp_file> background_temp_;
239         struct page_state;
240         std::auto_ptr<page_state> page_state_;
241     };
242
243     webdvd_window::webdvd_window(
244         const video::frame_params & frame_params,
245         const std::string & main_page_uri,
246         const std::string & output_dir)
247             : frame_params_(frame_params),
248               output_dir_(output_dir),
249               stylesheet_(load_css("file://" WEBDVD_LIB_DIR "/webdvd.css")),
250               pending_window_update_(false),
251               pending_req_count_(0),
252               have_tweaked_page_(false)
253     {
254         set_size_request(frame_params_.width, frame_params_.height);
255         set_resizable(false);
256
257         add(browser_widget_);
258         browser_widget_.show();
259         browser_widget_.signal_net_state().connect(
260             SigC::slot(*this, &webdvd_window::on_net_state_change));
261
262         add_menu(main_page_uri);
263         load_next_page();
264     }
265
266     dvd_contents::pgc_ref webdvd_window::add_menu(const std::string & uri)
267     {
268         dvd_contents::pgc_ref next_menu(dvd_contents::menu_pgc,
269                                         contents_.menus.size());
270         std::pair<resource_map_type::iterator, bool> insert_result(
271             resource_map_.insert(std::make_pair(uri, next_menu)));
272
273         if (!insert_result.second)
274         {
275             return insert_result.first->second;
276         }
277         else
278         {
279             page_queue_.push(uri);
280             contents_.menus.resize(contents_.menus.size() + 1);
281             return next_menu;
282         }
283     }
284
285     dvd_contents::pgc_ref webdvd_window::add_title(const std::string & uri)
286     {
287         dvd_contents::pgc_ref next_title(dvd_contents::title_pgc,
288                                          contents_.titles.size());
289         std::pair<resource_map_type::iterator, bool> insert_result(
290             resource_map_.insert(std::make_pair(uri, next_title)));
291
292         if (!insert_result.second)
293         {
294             return insert_result.first->second;
295         }
296         else
297         {
298             Glib::ustring hostname;
299             std::string filename(Glib::filename_from_uri(uri, hostname));
300             // FIXME: Should check the hostname
301
302             std::string vob_list;
303
304             // Store a reference to a linked VOB file, or the contents
305             // of a linked VOB list file.
306             if (filename.compare(filename.size() - 4, 4, ".vob") == 0)
307             {
308                 if (!Glib::file_test(filename, Glib::FILE_TEST_IS_REGULAR))
309                     throw std::runtime_error(
310                         filename + " is missing or not a regular file");
311                 vob_list
312                     .append("<vob file='")
313                     .append(xml_escape(filename))
314                     .append("'/>\n");
315             }
316             else
317             {
318                 assert(filename.compare(filename.size() - 8, 8, ".voblist")
319                        == 0);
320                 // TODO: Validate the file contents
321                 vob_list.assign(Glib::file_get_contents(filename));
322             }
323
324             contents_.titles.push_back(dvd_contents::title(vob_list));
325             return next_title;
326         }
327     }
328
329     void webdvd_window::load_next_page()
330     {
331         assert(!page_queue_.empty());
332         const std::string & uri = page_queue_.front();
333         std::cout << "loading " << uri << std::endl;
334
335         browser_widget_.load_uri(uri);
336     }
337
338     void webdvd_window::on_net_state_change(const char * uri,
339                                            gint flags, guint status)
340     {
341 #       ifdef DEBUG_ON_NET_STATE_CHANGE
342         std::cout << "webdvd_window::on_net_state_change(";
343         if (uri)
344             std::cout << '"' << uri << '"';
345         else
346             std::cout << "NULL";
347         std::cout << ", ";
348         {
349             gint flags_left = flags;
350             static const struct {
351                 gint value;
352                 const char * name;
353             } flag_names[] = {
354                 { GTK_MOZ_EMBED_FLAG_START, "STATE_START" },
355                 { GTK_MOZ_EMBED_FLAG_REDIRECTING, "STATE_REDIRECTING" },
356                 { GTK_MOZ_EMBED_FLAG_TRANSFERRING, "STATE_TRANSFERRING" },
357                 { GTK_MOZ_EMBED_FLAG_NEGOTIATING, "STATE_NEGOTIATING" },
358                 { GTK_MOZ_EMBED_FLAG_STOP, "STATE_STOP" },
359                 { GTK_MOZ_EMBED_FLAG_IS_REQUEST, "STATE_IS_REQUEST" },
360                 { GTK_MOZ_EMBED_FLAG_IS_DOCUMENT, "STATE_IS_DOCUMENT" },
361                 { GTK_MOZ_EMBED_FLAG_IS_NETWORK, "STATE_IS_NETWORK" },
362                 { GTK_MOZ_EMBED_FLAG_IS_WINDOW, "STATE_IS_WINDOW" }
363             };
364             for (int i = 0; i != sizeof(flag_names)/sizeof(flag_names[0]); ++i)
365             {
366                 if (flags & flag_names[i].value)
367                 {
368                     std::cout << flag_names[i].name;
369                     flags_left -= flag_names[i].value;
370                     if (flags_left)
371                         std::cout << " | ";
372                 }
373             }
374             if (flags_left)
375                 std::cout << "0x" << std::setbase(16) << flags_left;
376         }
377         std::cout << ", " << "0x" << std::setbase(16) << status << ")\n";
378 #       endif // DEBUG_ON_NET_STATE_CHANGE
379
380         if (flags & GTK_MOZ_EMBED_FLAG_IS_REQUEST)
381         {
382             if (flags & GTK_MOZ_EMBED_FLAG_START)
383                 ++pending_req_count_;
384
385             if (flags & GTK_MOZ_EMBED_FLAG_STOP)
386             {
387                 assert(pending_req_count_ != 0);
388                 --pending_req_count_;
389             }
390         }
391             
392         if (flags & GTK_MOZ_EMBED_FLAG_IS_DOCUMENT
393             && flags & GTK_MOZ_EMBED_FLAG_START)
394         {
395             pending_window_update_ = true;
396             have_tweaked_page_ = false;
397         }
398
399         if (flags & GTK_MOZ_EMBED_FLAG_IS_WINDOW
400             && flags & GTK_MOZ_EMBED_FLAG_STOP)
401         {
402             // Check whether the load was successful, ignoring this
403             // pseudo-error.
404             if (status != NS_IMAGELIB_ERROR_LOAD_ABORTED)
405                 check(status);
406
407             pending_window_update_ = false;
408         }
409
410         if (pending_req_count_ == 0 && !pending_window_update_)
411         {
412             try
413             {
414                 if (!process_page())
415                     Gtk::Main::quit();
416             }
417             catch (std::exception & e)
418             {
419                 std::cerr << "Fatal error";
420                 if (!page_queue_.empty())
421                     std::cerr << " while processing <" << page_queue_.front()
422                               << ">";
423                 std::cerr << ": " << e.what() << "\n";
424                 Gtk::Main::quit();
425             }
426         }
427     }
428
429     bool webdvd_window::process_page()
430     {
431         assert(!page_queue_.empty());
432
433         nsCOMPtr<nsIWebBrowser> browser(browser_widget_.get_browser());
434         nsCOMPtr<nsIDocShell> doc_shell(do_GetInterface(browser));
435         assert(doc_shell);
436         nsCOMPtr<nsIPresShell> pres_shell;
437         check(doc_shell->GetPresShell(getter_AddRefs(pres_shell)));
438         nsCOMPtr<nsIPresContext> pres_context;
439         check(doc_shell->GetPresContext(getter_AddRefs(pres_context)));
440         nsCOMPtr<nsIDOMWindow> dom_window;
441         check(browser->GetContentDOMWindow(getter_AddRefs(dom_window)));
442
443         // If we haven't done so already, apply the stylesheet and
444         // disable scrollbars.
445         if (!have_tweaked_page_)
446         {
447             apply_style_sheet(stylesheet_, pres_shell);
448
449             // This actually only needs to be done once.
450             nsCOMPtr<nsIDOMBarProp> dom_bar_prop;
451             check(dom_window->GetScrollbars(getter_AddRefs(dom_bar_prop)));
452             check(dom_bar_prop->SetVisible(false));
453
454             have_tweaked_page_ = true;
455
456             // Might need to wait a while for things to load or more
457             // likely for a re-layout.
458             if (pending_req_count_ > 0)
459                 return true;
460         }
461
462         // All further work should only be done if we're not in preview mode.
463         if (!output_dir_.empty())
464         {
465             // If we haven't already started work on this menu, save a
466             // screenshot of its normal appearance.
467             if (!page_state_.get())
468                 save_screenshot();
469
470             // Start or continue processing links.
471             process_links(pres_shell, pres_context, dom_window);
472
473             // If we've finished work on the links, move on to the
474             // next page, if any, or else generate the DVD filesystem.
475             if (!page_state_.get())
476             {
477                 page_queue_.pop();
478                 if (page_queue_.empty())
479                 {
480                     generate_dvd();
481                     return false;
482                 }
483                 else
484                 {
485                     load_next_page();
486                 }
487             }
488         }
489
490         return true;
491     }
492
493     void webdvd_window::save_screenshot()
494     {
495         Glib::RefPtr<Gdk::Window> window(get_window());
496         assert(window);
497         window->process_updates(true);
498
499         background_temp_.reset(new temp_file("webdvd-back-"));
500         background_temp_->close();
501         std::cout << "saving " << background_temp_->get_name() << std::endl;
502         Gdk::Pixbuf::create(Glib::RefPtr<Gdk::Drawable>(window),
503                             window->get_colormap(),
504                             0, 0, 0, 0,
505                             frame_params_.width, frame_params_.height)
506             ->save(background_temp_->get_name(), "png");
507     }
508
509     struct webdvd_window::page_state
510     {
511         page_state(nsIDOMDocument * doc, int width, int height)
512                 : diff_pixbuf(Gdk::Pixbuf::create(
513                                   Gdk::COLORSPACE_RGB,
514                                   true, 8, // has_alpha, bits_per_sample
515                                   width, height)),
516                   spumux_temp("webdvd-spumux-"),
517                   links_temp("webdvd-links-"),
518                   link_num(0),
519                   links_it(doc),
520                   link_changing(false)
521             {
522                 spumux_temp.close();
523                 links_temp.close();
524             }
525
526         Glib::RefPtr<Gdk::Pixbuf> diff_pixbuf;
527
528         temp_file spumux_temp;
529         std::ofstream spumux_file;
530
531         temp_file links_temp;
532
533         int link_num;
534         link_iterator links_it, links_end;
535
536         rectangle link_rect;
537         bool link_changing;
538         Glib::RefPtr<Gdk::Pixbuf> norm_pixbuf;
539     };
540
541     void webdvd_window::process_links(nsIPresShell * pres_shell,
542                                      nsIPresContext * pres_context,
543                                      nsIDOMWindow * dom_window)
544     {
545         Glib::RefPtr<Gdk::Window> window(get_window());
546         assert(window);
547
548         nsCOMPtr<nsIDOMDocument> basic_doc;
549         check(dom_window->GetDocument(getter_AddRefs(basic_doc)));
550         nsCOMPtr<nsIDOMNSDocument> ns_doc(do_QueryInterface(basic_doc));
551         assert(ns_doc);
552         nsCOMPtr<nsIEventStateManager> event_state_man(
553             pres_context->EventStateManager()); // does not AddRef
554         assert(event_state_man);
555         nsCOMPtr<nsIDOMDocumentEvent> event_factory(
556             do_QueryInterface(basic_doc));
557         assert(event_factory);
558         nsCOMPtr<nsIDOMDocumentView> doc_view(do_QueryInterface(basic_doc));
559         assert(doc_view);
560         nsCOMPtr<nsIDOMAbstractView> view;
561         check(doc_view->GetDefaultView(getter_AddRefs(view)));
562
563         // Set up or recover our iteration state.
564         std::auto_ptr<page_state> state(page_state_);
565         if (!state.get())
566         {
567             state.reset(
568                 new page_state(
569                     basic_doc, frame_params_.width, frame_params_.height));
570             
571             state->spumux_file.open(state->spumux_temp.get_name().c_str());
572             state->spumux_file <<
573                 "<subpictures>\n"
574                 "  <stream>\n"
575                 "    <spu force='yes' start='00:00:00.00'\n"
576                 "        highlight='" << state->links_temp.get_name() << "'\n"
577                 "        select='" << state->links_temp.get_name() << "'>\n";
578         }
579
580         rectangle window_rect = {
581             0, 0, frame_params_.width, frame_params_.height
582         };
583
584         int menu_num = resource_map_[page_queue_.front()].second;
585
586         for (/* no initialisation */;
587              state->links_it != state->links_end;
588              ++state->links_it)
589         {
590             nsCOMPtr<nsIDOMNode> node(*state->links_it);
591
592             // Find the link URI.
593             nsCOMPtr<nsILink> link(do_QueryInterface(node));
594             assert(link);
595             nsCOMPtr<nsIURI> uri;
596             check(link->GetHrefURI(getter_AddRefs(uri)));
597             std::string uri_string;
598             {
599                 nsCString uri_ns_string;
600                 check(uri->GetSpec(uri_ns_string));
601                 uri_string.assign(uri_ns_string.BeginReading(),
602                                   uri_ns_string.EndReading());
603             }
604             std::string uri_sans_fragment(uri_string, 0, uri_string.find('#'));
605
606             // Is this a new link?
607             if (!state->link_changing)
608             {
609                 // Find a rectangle enclosing the link and clip it to the
610                 // window.
611                 nsCOMPtr<nsIDOMElement> elem(do_QueryInterface(node));
612                 assert(elem);
613                 state->link_rect = get_elem_rect(ns_doc, elem);
614                 state->link_rect &= window_rect;
615
616                 if (state->link_rect.empty())
617                 {
618                     std::cerr << "Ignoring invisible link to "
619                               << uri_string << "\n";
620                     continue;
621                 }
622
623                 ++state->link_num;
624
625                 if (state->link_num >= dvd::menu_buttons_max)
626                 {
627                     if (state->link_num == dvd::menu_buttons_max)
628                         std::cerr << "No more than " << dvd::menu_buttons_max
629                                   << " buttons can be placed on a menu\n";
630                     std::cerr << "Ignoring link to " << uri_string << "\n";
631                     continue;
632                 }
633
634                 state->spumux_file <<
635                     "      <button x0='" << state->link_rect.left << "'"
636                     " y0='" << state->link_rect.top << "'"
637                     " x1='" << state->link_rect.right - 1 << "'"
638                     " y1='" << state->link_rect.bottom - 1 << "'/>\n";
639
640                 // Check whether this is a link to a video or a page then
641                 // add it to the known resources if not already seen; then
642                 // add it to the menu entries.
643                 nsCString path;
644                 check(uri->GetPath(path));
645                 dvd_contents::pgc_ref dest_pgc;
646                 // FIXME: This is a bit of a hack.  Perhaps we could decide
647                 // later based on the MIME type determined by Mozilla?
648                 if ((path.Length() > 4
649                      && std::strcmp(path.EndReading() - 4, ".vob") == 0)
650                     || (path.Length() > 8
651                         && std::strcmp(path.EndReading() - 8, ".voblist")
652                            == 0))
653                 {
654                     PRBool is_file;
655                     check(uri->SchemeIs("file", &is_file));
656                     if (!is_file)
657                     {
658                         std::cerr << "Links to video must use the file:"
659                                   << " scheme\n";
660                         continue;
661                     }
662                     dest_pgc = add_title(uri_sans_fragment);
663                 }
664                 else
665                 {
666                     dest_pgc = add_menu(uri_sans_fragment);
667                 }
668                 contents_.menus[menu_num].entries.push_back(dest_pgc);
669
670                 nsCOMPtr<nsIContent> content(do_QueryInterface(node));
671                 assert(content);
672                 nsCOMPtr<nsIDOMEventTarget> event_target(
673                     do_QueryInterface(node));
674                 assert(event_target);
675
676                 state->norm_pixbuf = Gdk::Pixbuf::create(
677                     Glib::RefPtr<Gdk::Drawable>(window),
678                     window->get_colormap(),
679                     state->link_rect.left,
680                     state->link_rect.top,
681                     0,
682                     0,
683                     state->link_rect.right - state->link_rect.left,
684                     state->link_rect.bottom - state->link_rect.top);
685
686                 nsCOMPtr<nsIDOMEvent> event;
687                 check(event_factory->CreateEvent(
688                           NS_ConvertASCIItoUTF16("MouseEvents"),
689                           getter_AddRefs(event)));
690                 nsCOMPtr<nsIDOMMouseEvent> mouse_event(
691                     do_QueryInterface(event));
692                 assert(mouse_event);
693                 check(mouse_event->InitMouseEvent(
694                           NS_ConvertASCIItoUTF16("mouseover"),
695                           true,  // can bubble
696                           true,  // cancelable
697                           view,
698                           0,     // detail: mouse click count
699                           state->link_rect.left, // screenX
700                           state->link_rect.top,  // screenY
701                           state->link_rect.left, // clientX
702                           state->link_rect.top,  // clientY
703                           false, false, false, false, // qualifiers
704                           0,     // button: left (or primary)
705                           0));   // related target
706                 PRBool dummy;
707                 check(event_target->DispatchEvent(mouse_event,
708                                                   &dummy));
709                 check(event_state_man->SetContentState(content,
710                                                        NS_EVENT_STATE_HOVER));
711
712                 pres_shell->FlushPendingNotifications(true);
713
714                 // We may have to exit and wait for image loading
715                 // to complete, at which point we will be called
716                 // again.
717                 if (pending_req_count_ > 0)
718                 {
719                     state->link_changing = true;
720                     page_state_ = state;
721                     return;
722                 }
723             }
724
725             window->process_updates(true);
726
727             Glib::RefPtr<Gdk::Pixbuf> changed_pixbuf(
728                 Gdk::Pixbuf::create(
729                     Glib::RefPtr<Gdk::Drawable>(window),
730                     window->get_colormap(),
731                     state->link_rect.left,
732                     state->link_rect.top,
733                     0,
734                     0,
735                     state->link_rect.right - state->link_rect.left,
736                     state->link_rect.bottom - state->link_rect.top));
737             diff_rgb_pixbufs(
738                 state->norm_pixbuf,
739                 changed_pixbuf,
740                 state->diff_pixbuf,
741                 state->link_rect.left,
742                 state->link_rect.top,
743                 state->link_rect.right - state->link_rect.left,
744                 state->link_rect.bottom - state->link_rect.top);
745         }
746
747         quantise_rgba_pixbuf(state->diff_pixbuf, dvd::button_n_colours);
748
749         std::cout << "saving " << state->links_temp.get_name()
750                   << std::endl;
751         state->diff_pixbuf->save(state->links_temp.get_name(), "png");
752
753         state->spumux_file <<
754             "    </spu>\n"
755             "  </stream>\n"
756             "</subpictures>\n";
757
758         state->spumux_file.close();
759
760         // TODO: if (!state->spumux_file) throw ...
761
762         {
763             std::ostringstream command_stream;
764             command_stream << "pngtopnm "
765                            << background_temp_->get_name()
766                            << " | ppmtoy4m -v0 -n1 -F"
767                            << frame_params_.rate_numer
768                            << ":" << frame_params_.rate_denom
769                            << " -A" << frame_params_.pixel_ratio_width
770                            << ":" << frame_params_.pixel_ratio_height
771                            << (" -Ip -S420_mpeg2"
772                                " | mpeg2enc -v0 -f8 -a2 -o/dev/stdout"
773                                " | mplex -v0 -f8 -o/dev/stdout /dev/stdin"
774                                " | spumux -v0 -mdvd ")
775                            << state->spumux_temp.get_name()
776                            << " > "
777                            << contents_.menus[menu_num].vob_temp->get_name();
778             std::string command(command_stream.str());
779             const char * argv[] = {
780                 "/bin/sh", "-c", command.c_str(), 0
781             };
782             std::cout << "running " << argv[2] << std::endl;
783             int command_result;
784             Glib::spawn_sync(".",
785                              Glib::ArrayHandle<std::string>(
786                                  argv, sizeof(argv)/sizeof(argv[0]),
787                                  Glib::OWNERSHIP_NONE),
788                              Glib::SPAWN_STDOUT_TO_DEV_NULL,
789                              SigC::Slot0<void>(),
790                              0, 0,
791                              &command_result);
792             if (command_result != 0)
793                 throw std::runtime_error("spumux pipeline failed");
794         }
795     }
796
797     void webdvd_window::generate_dvd()
798     {
799         temp_file temp("webdvd-dvdauthor-");
800         temp.close();
801         std::ofstream file(temp.get_name().c_str());
802
803         // We generate code that uses registers in the following way:
804         //
805         // g0:     scratch
806         // g1:     current location
807         // g12:    location that last jumped to a video
808         //
809         // All locations are divided into two bitfields: the least
810         // significant 10 bits are a page/menu number and the most
811         // significant 6 bits are a link/button number, and numbering
812         // starts at 1, not 0.  This is done for compatibility with
813         // the encoding of the s8 (button) register.
814         //
815         static const int button_mult = dvd::reg_s8_button_mult;
816         static const int menu_mask = button_mult - 1;
817         static const int button_mask = (1 << dvd::reg_bits) - button_mult;
818
819         file <<
820             "<dvdauthor>\n"
821             "  <vmgm>\n"
822             "    <menus>\n";
823             
824         for (std::size_t menu_num = 0;
825              menu_num != contents_.menus.size();
826              ++menu_num)
827         {
828             dvd_contents::menu & menu = contents_.menus[menu_num];
829
830             if (menu_num == 0)
831             {
832                 // This is the first (title) menu, displayed when the
833                 // disc is first played.
834                 file <<
835                     "      <pgc entry='title'>\n"
836                     "        <pre>\n"
837                     // Initialise the current location if it is not set
838                     // (all general registers are initially 0).
839                     "          if (g1 eq 0)\n"
840                     "            g1 = " << 1 + button_mult << ";\n";
841             }
842             else
843             {
844                 file <<
845                     "      <pgc>\n"
846                     "        <pre>\n";
847             }
848
849             // When a title finishes or the user presses the menu
850             // button, this always jumps to the titleset's root menu.
851             // We want to return the user to the last menu they used.
852             // So we arrange for each titleset's root menu to return
853             // to the vmgm title menu and then dispatch from there to
854             // whatever the correct menu is.  We determine the correct
855             // menu by looking at the menu part of g1.
856
857             file << "          g0 = g1 &amp; " << menu_mask << ";\n";
858
859             // There is a limit of 128 VM instructions in each PGC.
860             // Therefore in each menu's <pre> section we generate
861             // jumps to menus with numbers greater by 512, 256, 128,
862             // ..., 1 where (a) such a menu exists, (b) this menu
863             // number is divisible by twice that increment and (c) the
864             // correct menu is that or a later menu.  Thus each menu
865             // has at most 10 such conditional jumps and is reachable
866             // by at most 10 jumps from the title menu.  This chain of
867             // jumps might take too long on some players; this has yet
868             // to be investigated.
869             
870             for (std::size_t menu_incr = (menu_mask + 1) / 2;
871                  menu_incr != 0;
872                  menu_incr /= 2)
873             {
874                 if (menu_num + menu_incr < contents_.menus.size()
875                     && (menu_num & (menu_incr * 2 - 1)) == 0)
876                 {
877                     file <<
878                         "          if (g0 ge " << 1 + menu_num + menu_incr
879                                                << ")\n"
880                         "            jump menu " << 1 + menu_num + menu_incr
881                                                << ";\n";
882                 }
883             }
884
885             file <<
886                 // Highlight the appropriate button.
887                 "          s8 = g1 &amp; " << button_mask << ";\n"
888                 "        </pre>\n"
889                 "        <vob file='" << menu.vob_temp->get_name() << "'/>\n";
890
891             for (std::size_t button_num = 0;
892                  button_num != menu.entries.size();
893                  ++button_num)
894             {
895                 file << "        <button> ";
896
897                 if (menu.entries[button_num].first == dvd_contents::menu_pgc)
898                 {
899                     int dest_menu_num = menu.entries[button_num].second;
900
901                     // Look for a button on the new menu that links
902                     // back to this one.  If there is one, set that to
903                     // be the highlighted button; otherwise, use the
904                     // first button.
905                     const std::vector<dvd_contents::pgc_ref> &
906                         dest_menu_entries =
907                         contents_.menus[dest_menu_num].entries;
908                     dvd_contents::pgc_ref this_pgc(
909                         dvd_contents::menu_pgc, menu_num);
910                     std::size_t dest_button_num = dest_menu_entries.size();
911                     while (dest_button_num != 0
912                            && dest_menu_entries[--dest_button_num] != this_pgc)
913                         ;
914                          
915                     file << "g1 = "
916                          << (1 + dest_menu_num
917                              + (1 + dest_button_num) * button_mult)
918                          << "; jump menu " << 1 + dest_menu_num << ";";
919                 }
920                 else
921                 {
922                     assert(menu.entries[button_num].first
923                            == dvd_contents::title_pgc);
924
925                     file << "g1 = "
926                          << 1 + menu_num + (1 + button_num) * button_mult
927                          << "; jump title "
928                          << 1 + menu.entries[button_num].second << ";";
929                 }
930
931                 file <<  " </button>\n";
932             }
933
934             file << "      </pgc>\n";
935         }
936
937         file <<
938             "    </menus>\n"
939             "  </vmgm>\n";
940
941         // Generate a titleset for each title.  This appears to make
942         // jumping to titles a whole lot simpler (but limits us to 99
943         // titles).
944         for (std::size_t title_num = 0;
945              title_num != contents_.titles.size();
946              ++title_num)
947         {
948             file <<
949                 "  <titleset>\n"
950                 // Generate a dummy menu so that the menu button on the
951                 // remote control will work.
952                 "    <menus>\n"
953                 "      <pgc entry='root'>\n"
954                 "        <pre> jump vmgm menu; </pre>\n"
955                 "      </pgc>\n"
956                 "    </menus>\n"
957                 "    <titles>\n"
958                 "      <pgc>\n"
959                 // Record calling location.
960                 "        <pre> g12 = g1; </pre>\n"
961                  << contents_.titles[title_num].vob_list <<
962                 // If the menu location has not been changed during
963                 // the title, set the location to be the following
964                 // button in the menu.  In any case, return to some
965                 // menu.
966                 "        <post> if (g1 eq g12) g1 = g1 + " << button_mult
967                  << "; call menu; </post>\n"
968                 "      </pgc>\n"
969                 "    </titles>\n"
970                 "  </titleset>\n";
971         }
972
973         file <<
974             "</dvdauthor>\n";
975
976         file.close();
977
978         {
979             const char * argv[] = {
980                 "dvdauthor",
981                 "-o", output_dir_.c_str(),
982                 "-x", temp.get_name().c_str(),
983                 0
984             };
985             int command_result;
986             Glib::spawn_sync(".",
987                              Glib::ArrayHandle<std::string>(
988                                  argv, sizeof(argv)/sizeof(argv[0]),
989                                  Glib::OWNERSHIP_NONE),
990                              Glib::SPAWN_SEARCH_PATH
991                              | Glib::SPAWN_STDOUT_TO_DEV_NULL,
992                              SigC::Slot0<void>(),
993                              0, 0,
994                              &command_result);
995             if (command_result != 0)
996                 throw std::runtime_error("dvdauthor failed");
997         }
998     }
999
1000     const video::frame_params & lookup_frame_params(const char * str)
1001     {
1002         assert(str);
1003         static const struct { const char * str; bool is_ntsc; }
1004         known_strings[] = {
1005             { "NTSC",  true },
1006             { "ntsc",  true },
1007             { "PAL",   false },
1008             { "pal",   false },
1009             // For DVD purposes, SECAM can be treated identically to PAL.
1010             { "SECAM", false },
1011             { "secam", false }
1012         };
1013         for (std::size_t i = 0;
1014              i != sizeof(known_strings)/sizeof(known_strings[0]);
1015              ++i)
1016             if (std::strcmp(str, known_strings[i].str) == 0)
1017                 return known_strings[i].is_ntsc ?
1018                     video::ntsc_params : video::pal_params;
1019         throw std::runtime_error(
1020             std::string("Invalid video standard: ").append(str));
1021     }
1022
1023     void print_usage(std::ostream & stream, const char * command_name)
1024     {
1025         stream << "Usage: " << command_name
1026                << (" [gtk-options] [--video-std std-name]"
1027                    " [--preview] menu-url [output-dir]\n");
1028     }
1029     
1030     void set_browser_preferences()
1031     {
1032         // Disable IE-compatibility kluge that causes backgrounds to
1033         // sometimes/usually be missing from snapshots.  This is only
1034         // effective from Mozilla 1.8 onward.
1035 #       if MOZ_VERSION_MAJOR > 1                                 \
1036            || (MOZ_VERSION_MAJOR == 1 && MOZ_VERSION_MINOR >= 8)
1037         nsCOMPtr<nsIPrefService> pref_service;
1038         static const nsCID pref_service_cid = NS_PREFSERVICE_CID;
1039         check(CallGetService<nsIPrefService>(pref_service_cid,
1040                                              getter_AddRefs(pref_service)));
1041         nsCOMPtr<nsIPrefBranch> pref_branch;
1042         check(pref_service->GetDefaultBranch("layout",
1043                                              getter_AddRefs(pref_branch)));
1044         check(pref_branch->SetBoolPref(
1045                   "fire_onload_after_image_background_loads",
1046                   true));
1047 #       endif
1048
1049         // TODO: Set display resolution?  Unfortunately Mozilla doesn't
1050         // support non-square pixels (and neither do fontconfig or Xft
1051         // anyway).
1052     }
1053
1054 } // namespace
1055
1056 int main(int argc, char ** argv)
1057 {
1058     try
1059     {
1060         video::frame_params frame_params = video::pal_params;
1061         bool preview_mode = false;
1062         std::string menu_url;
1063         std::string output_dir;
1064
1065         // Do initial option parsing.  We have to do this before
1066         // letting Gtk parse the arguments since we may need to spawn
1067         // Xvfb first.
1068         int argi = 1;
1069         while (argi != argc)
1070         {
1071             if (std::strcmp(argv[argi], "--") == 0)
1072             {
1073                 break;
1074             }
1075             else if (std::strcmp(argv[argi], "--help") == 0)
1076             {
1077                 print_usage(std::cout, argv[0]);
1078                 return EXIT_SUCCESS;
1079             }
1080             else if (std::strcmp(argv[argi], "--preview") == 0)
1081             {
1082                 preview_mode = true;
1083                 argi += 1;
1084             }
1085             else if (std::strcmp(argv[argi], "--video-std") == 0)
1086             {
1087                 if (argi + 1 == argc)
1088                 {
1089                     std::cerr << "Missing argument to --video-std\n";
1090                     print_usage(std::cerr, argv[0]);
1091                     return EXIT_FAILURE;
1092                 }
1093                 frame_params = lookup_frame_params(argv[argi + 1]);
1094                 argi += 2;
1095             }
1096             else
1097             {
1098                 argi += 1;
1099             }
1100         }
1101
1102         std::auto_ptr<x_frame_buffer> fb;
1103         if (!preview_mode)
1104         {
1105             // Spawn Xvfb and set env variables so that Xlib will use it
1106             // Use 8 bits each for RGB components, which should translate into
1107             // "enough" bits for YUV components.
1108             fb.reset(new x_frame_buffer(frame_params.width,
1109                                         frame_params.height,
1110                                         3 * 8));
1111             setenv("XAUTHORITY", fb->get_authority().c_str(), true);
1112             setenv("DISPLAY", fb->get_display().c_str(), true);
1113         }
1114
1115         // Initialise Gtk
1116         Gtk::Main kit(argc, argv);
1117
1118         // Complete option parsing with Gtk's options out of the way.
1119         argi = 1;
1120         while (argi != argc)
1121         {
1122             if (std::strcmp(argv[argi], "--") == 0)
1123             {
1124                 argi += 1;
1125                 break;
1126             }
1127             else if (std::strcmp(argv[argi], "--preview") == 0)
1128             {
1129                 argi += 1;
1130             }
1131             else if (std::strcmp(argv[argi], "--video-std") == 0)
1132             {
1133                 argi += 2;
1134             }
1135             else if (argv[argi][0] == '-')
1136             {
1137                 std::cerr << "Invalid option: " << argv[argi] << "\n";
1138                 print_usage(std::cerr, argv[0]);
1139                 return EXIT_FAILURE;
1140             }
1141             else
1142             {
1143                 break;
1144             }
1145         }
1146
1147         // Look for a starting URL or filename and (except in preview
1148         // mode) an output directory after the options.
1149         if (argc - argi != (preview_mode ? 1 : 2))
1150         {
1151             print_usage(std::cerr, argv[0]);
1152             return EXIT_FAILURE;
1153         }
1154         if (std::strstr(argv[argi], "://"))
1155         {
1156             // It appears to be an absolute URL, so use it as-is.
1157             menu_url = argv[argi];
1158         }
1159         else
1160         {
1161             // Assume it's a filename.  Resolve it to an absolute URL.
1162             std::string path(argv[argi]);
1163             if (!Glib::path_is_absolute(path))
1164                 path = Glib::build_filename(Glib::get_current_dir(), path);
1165             menu_url = Glib::filename_to_uri(path);             
1166         }
1167         if (!preview_mode)
1168             output_dir = argv[argi + 1];
1169
1170         // Initialise Mozilla
1171         browser_widget::initialiser browser_init;
1172         set_browser_preferences();
1173
1174         // Run the browser/converter
1175         webdvd_window window(frame_params, menu_url, output_dir);
1176         Gtk::Main::run(window);
1177     }
1178     catch (std::exception & e)
1179     {
1180         std::cerr << "Fatal error: " << e.what() << "\n";
1181         return EXIT_FAILURE;
1182     }
1183
1184     return EXIT_SUCCESS;
1185 }