]> git.decadent.org.uk Git - videolink.git/commitdiff
Imported version 0.1 0.1
authorBen Hutchings <ben@decadent.org.uk>
Thu, 3 Nov 2005 02:22:37 +0000 (02:22 +0000)
committerBen Hutchings <ben@decadent.org.uk>
Sun, 2 Nov 2008 23:15:00 +0000 (23:15 +0000)
31 files changed:
COPYING [new file with mode: 0644]
INSTALL [new file with mode: 0644]
Makefile [new file with mode: 0644]
README [new file with mode: 0644]
TODO [new file with mode: 0644]
auto_array.hpp [new file with mode: 0644]
auto_fd.hpp [new file with mode: 0644]
auto_handle.hpp [new file with mode: 0644]
auto_proc.cpp [new file with mode: 0644]
auto_proc.hpp [new file with mode: 0644]
auto_temp_file.hpp [new file with mode: 0644]
browserwidget.cpp [new file with mode: 0644]
browserwidget.hpp [new file with mode: 0644]
childiterator.cpp [new file with mode: 0644]
childiterator.hpp [new file with mode: 0644]
dvd.hpp [new file with mode: 0644]
framebuffer.cpp [new file with mode: 0644]
framebuffer.hpp [new file with mode: 0644]
jquant2.c [new file with mode: 0644]
jquant2.h [new file with mode: 0644]
linkiterator.cpp [new file with mode: 0644]
linkiterator.hpp [new file with mode: 0644]
pixbufs.cpp [new file with mode: 0644]
pixbufs.hpp [new file with mode: 0644]
stylesheets.cpp [new file with mode: 0644]
stylesheets.hpp [new file with mode: 0644]
video.hpp [new file with mode: 0644]
webdvd.cpp [new file with mode: 0644]
webdvd.css [new file with mode: 0644]
xpcom_support.cpp [new file with mode: 0644]
xpcom_support.hpp [new file with mode: 0644]

diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..a115095
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,355 @@
+WebDVD is licenced under the GNU GPL, version 2, with the following
+additions:
+
+A. You may combine and distribute WebDVD and derivative works with
+   libraries licenced under the GNU LGPL without exercising the option to
+   treat them as licenced under the GPL.
+
+B. When distributing WebDVD or derivative works without source code
+   included you must ensure that the documentation retains the notice that
+   "this software is based in part on the work of the Independent JPEG
+   Group", or else remove the code contained in jquant2.c to which that
+   notice applies.
+
+--
+
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+     59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+\f
+                   GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+\f
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+\f
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+\f
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                           NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                    END OF TERMS AND CONDITIONS
+\f
+           How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year  name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/INSTALL b/INSTALL
new file mode 100644 (file)
index 0000000..e5ae87b
--- /dev/null
+++ b/INSTALL
@@ -0,0 +1,17 @@
+Building WebDVD
+===============
+
+WebDVD is written in C++ and requires a recent C++ compiler e.g. g++
+3.3.
+
+It requires headers and libraries for gtkmm and Mozilla.  I have
+developed and tested it with gtkmm 2.2.12 and Mozilla 1.7.8 but it may
+well work with earlier or later versions of these.
+
+I use Debian Linux and have not yet attempted to build it on other
+systems, but it should work on most modern Unix-like systems.
+
+If the above requirements are satisfied, building it should be as
+simple as running "make" and then "sudo make install".
+
+- Ben Hutchings <ben@decadentplace.org.uk>
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..6a5ddc8
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,74 @@
+prefix := /usr/local
+
+moz_include_dir := \
+    $(shell pkg-config --variable=prefix mozilla-gtkmozembed)/include/mozilla
+moz_lib_dir := \
+    $(shell pkg-config --variable=prefix mozilla-gtkmozembed)/lib/mozilla
+
+CFLAGS := -Wall
+CPPFLAGS := -D_REENTRANT
+CXXFLAGS := -Wall
+LDFLAGS := -lpthread $(shell pkg-config --libs gtkmm-2.0 mozilla-gtkmozembed) \
+           -Wl,-rpath -Wl,$(moz_lib_dir)
+
+ifdef NDEBUG
+    CPPFLAGS += -DNDEBUG
+else
+    CFLAGS += -g
+    CXXFLAGS += -g
+    LDFLAGS += -g
+endif
+
+cxxsources := \
+    auto_proc.cpp browserwidget.cpp childiterator.cpp framebuffer.cpp \
+    linkiterator.cpp pixbufs.cpp stylesheets.cpp webdvd.cpp xpcom_support.cpp
+csources := jquant2.c
+
+webdvd : $(cxxsources:.cpp=.o) $(csources:.c=.o)
+       $(CXX) $(LDFLAGS) -o $@ $^
+
+clean :
+       rm -f webdvd *.d *.o *~ \#* *.orig *.rej
+
+install :
+       mkdir -p -m 755 $(prefix)/bin $(prefix)/lib/webdvd
+       install -m 755 -s webdvd $(prefix)/bin
+       install -m 644 webdvd.css $(prefix)/lib/webdvd
+
+.PHONY : clean install
+
+browserwidget.% : CPPFLAGS += -DMOZ_LIB_DIR='"$(moz_lib_dir)"'
+
+webdvd.% \
+    : CPPFLAGS += -DWEBDVD_LIB_DIR='"$(prefix)/lib/webdvd"'
+
+browserwidget.% pixbufs.% webdvd.% \
+    : CPPFLAGS += $(shell pkg-config --cflags gtkmm-2.0)
+
+browserwidget.% childiterator.o linkiterator.% stylesheets.% webdvd.% \
+xpcom_support.%                                                       \
+    : CPPFLAGS += $(shell pkg-config --cflags mozilla-gtkmozembed)
+
+# These dig a bit deeper into Mozilla
+linkiterator.% stylesheets.% webdvd.%                                        \
+    : CPPFLAGS += $(addprefix -I$(moz_include_dir)/,                         \
+                    content docshell dom gfx layout necko webshell widget)
+
+%.d : %.cpp
+       $(CXX) $(CXXFLAGS) $(CPPFLAGS) -MM -MF $@ $<
+
+%.d : %.c
+       $(CC) $(CFLAGS) $(CPPFLAGS) -MM -MF $@ $<
+
+ifneq ($(MAKECMDGOALS),clean)
+    include $(cxxsources:.cpp=.d) $(csources:.c=.d)
+endif
+
+# Temporary rule for testing output files.
+
+%.vob : %_back.png %.spumux %_links.png
+       pngtopnm $*_back.png                                   \
+       | ppmtoy4m -v0 -n 1 -F 25:1 -A 59:54 -I p -S 420_mpeg2 \
+       | mpeg2enc -v0 -f 8 -a 2 -o /dev/stdout                \
+       | mplex -v0 -f 8 -o /dev/stdout /dev/stdin             \
+       | spumux -v0 -m dvd $*.spumux > $@
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..ec6534a
--- /dev/null
+++ b/README
@@ -0,0 +1,113 @@
+WebDVD
+======
+
+WebDVD is intended to provide a simple way of producing DVDs with
+attractive and usable menus.  It converts HTML pages into DVD menus by
+rendering them in Mozilla and reproducing their link structure.  This
+allows you to design DVDs using familiar HTML editing tools or your
+favourite text editor.  This very early version produces a set of
+files suitable for passing to the "dvdauthor" and "spumux" programs,
+but in future it is intended to run the necessary external programs
+automatically.
+
+Requirements
+------------
+
+WebDVD depends on the following software:
+
+- Gtkmm 2.0
+- Mozilla 1.7.x (later versions may work but are untested)
+- Xvfb (from XFree86 or X.org)
+
+To build a complete DVD image you will also need:
+
+- dvdauthor
+- mjpegtools
+- mkisofs
+- netpbm
+
+You will also need a program such as ffmpeg or mencoder for producing
+DVD-suitable MPEG-1 or MPEG-2 video files.
+
+Usage
+-----
+
+Run "webdvd URL" where URL is the URL for the page that is to be the
+top menu of the DVD.  It will automatically follow links to other
+pages and to video files, rendering each page.  You must be careful
+not to link to pages that you do not want to appear on the disc, such
+as normal web sites.
+
+By default, webdvd uses a frame size of 720x576, which is suitable for
+PAL DVDs.  If you wish to produce NTSC DVDs you must override this
+by adding the option "-geometry 720x480".
+
+This will create the following files (with NNNNNN replaced with each
+successive page number):
+
+- webdvd.dvdauthor: This is an XML file to be passed to dvdauthor.
+- page_NNNNNN.spumux: These are XML files to be passed to spumux.
+- page_NNNNNN_back.png: This is a static image of the page, which
+  becomes the menu background.
+- page_NNNNNN_links.png: This is an image of the change in appearance
+  of each link when the pointer is over it.
+
+Currently, you must run commands along the following lines to produce
+a complete DVD image:
+
+for spumux in page_??????.spumux; do
+    page=$(basename $spumux .spumux)
+    pngtopnm ${page}_back.png                              \
+    | ppmtoy4m -v0 -n 1 -F 25:1 -A 59:54 -I p -S 420_mpeg2 \
+    | mpeg2enc -v0 -f 8 -a 2 -o /dev/stdout                \
+    | mplex -v0 -f 8 -o /dev/stdout /dev/stdin             \
+    | spumux -v0 -m dvd $spumux > ${page}.vob
+done
+rm -rf dvd-temp
+dvdauthor -o dvd-temp -x webdvd.dvdauthor
+mkisofs -dvd-video dvd-temp >dvd.iso
+rm -rf dvd-temp
+
+Adjust the name of the temporary directory (here "dvd-temp") and the
+output file ("dvd.iso") as you please.
+
+If you are using NTSC video, you will need to change the ppmtoy4m
+parameters.  Use "-F 30000:1001 -A 10:11" instead of "-F 25:1 -A 59:54".
+
+Limitations
+-----------
+
+Each page must fit within the frame - DVD players do not support
+scrolling menus and WebDVD currently is not able to split them into
+multiple menus.  Note also that the video frame is somewhat larger
+than the visible area of a normal TV.  For this reason WebDVD applies
+a stylesheet to all pages that adds 50-60 pixels of padding on all
+sides of the body.
+
+WebDVD sends a "mouseover" event for each link and sets it into its
+"hover" state, then records how this changes its appearance.  This
+change is then shown when the corresponding button on the DVD menu is
+highlighted.  WebDVD applies a stylesheet which changes the colour of
+text links in the "hover" state, but this has no effect on image
+links.  You must ensure that image links are highlighted in an obvious
+way when the mouse pointer is over them.
+
+The DVD specifications limit each menu to having no more than 36
+buttons.  In any case, it is poor design to have very large numbers of
+buttons on a single menu.  WebDVD will warn you if you use more than
+this number of a links on a page, and will ignore any additional ones.
+
+The DVD specification also limits the overlays that are used for
+highlighting of buttons to using no more than 4 colours.  WebDVD will
+reduce link highlighting to 1 transparent and 3 opaque colours using
+Floyd-Steinberg dithering, which is certainly good enough for
+anti-alised text but may not be so good for complex highlighting.
+
+Author and copyright
+--------------------
+
+WebDVD was written by Ben Hutchings <ben@decadentplace.org.uk>.
+Copyright 2005 Ben Hutchings.
+
+This software is based in part on the work of the Independent JPEG Group.
+Copyright 1991-1998 Thomas G. Lane.  (This applies to the file jquant2.c.)
diff --git a/TODO b/TODO
new file mode 100644 (file)
index 0000000..f57b53d
--- /dev/null
+++ b/TODO
@@ -0,0 +1,6 @@
+Call spumux.
+Disable scrollbars (not sure how how to do this).
+Call dvdauthor.
+May need to override default menu navigation actions because dvdauthor
+seems to cause rather undesirable behaviour when buttons are not
+arranged in a grid.
diff --git a/auto_array.hpp b/auto_array.hpp
new file mode 100644 (file)
index 0000000..6567be2
--- /dev/null
@@ -0,0 +1,79 @@
+// Copyright 2005 Ben Hutchings <ben@decadentplace.org.uk>.
+// See the file "COPYING" for licence details.
+
+#ifndef INC_AUTO_ARRAY_HPP
+#define INC_AUTO_ARRAY_HPP
+
+#include <cstddef>
+
+// Like auto_ptr, but for arrays
+
+template<typename element_type>
+class auto_array_ref;
+
+template<typename element_type>
+class auto_array
+{
+    typedef auto_array_ref<element_type> ref_type;
+public:
+    auto_array()
+           : ptr_(0)
+       {}
+    explicit auto_array(element_type * ptr)
+           : ptr_(ptr)
+       {}
+    auto_array(ref_type other)
+           : ptr_(other.release())
+       {}
+    auto_array & operator=(auto_array & other)
+       {
+           reset(other.release());
+       }
+    ~auto_array()
+       {
+           reset();
+       }
+    element_type * get() const
+       {
+           return ptr_;
+       }
+    element_type * release()
+       {
+           element_type * ptr(ptr_);
+           ptr_ = 0;
+           return ptr;
+       }
+    void reset(element_type * ptr = 0)
+       {
+           delete[] ptr_;
+           ptr_ = ptr;
+       }
+    element_type & operator[](std::ptrdiff_t index)
+       {
+           return ptr_[index];
+       }
+    operator ref_type()
+       {
+           return ref_type(*this);
+       }
+private:
+    element_type * ptr_;
+};
+
+template<typename element_type>
+class auto_array_ref
+{
+    typedef auto_array<element_type> target_type;
+public:
+    explicit auto_array_ref(target_type & target)
+           : target_(target)
+       {}
+    element_type * release()
+       {
+           return target_.release();
+       }
+private:
+    target_type & target_;
+};
+
+#endif // !INC_AUTO_ARRAY_HPP
diff --git a/auto_fd.hpp b/auto_fd.hpp
new file mode 100644 (file)
index 0000000..d89cd8c
--- /dev/null
@@ -0,0 +1,30 @@
+// Copyright 2005 Ben Hutchings <ben@decadentplace.org.uk>.
+// See the file "COPYING" for licence details.
+
+#ifndef INC_AUTO_FD_HPP
+#define INC_AUTO_FD_HPP
+
+#include "auto_handle.hpp"
+
+#include <cassert>
+
+#include <unistd.h>
+
+struct auto_fd_closer
+{
+    void operator()(int fd) const
+       {
+           if (fd >= 0)
+           {
+               int result = close(fd);
+               assert(result == 0);
+           }
+       }
+};
+struct auto_fd_factory
+{
+    int operator()() const { return -1; }
+};
+typedef auto_handle<int, auto_fd_closer, auto_fd_factory> auto_fd;
+
+#endif // !INC_AUTO_FD_HPP
diff --git a/auto_handle.hpp b/auto_handle.hpp
new file mode 100644 (file)
index 0000000..b9d0b6b
--- /dev/null
@@ -0,0 +1,83 @@
+#ifndef INC_AUTO_HANDLE_H
+#define INC_AUTO_HANDLE_H
+
+// Like auto_ptr, but for arbitrary "handle" types.
+// The parameters are:
+// - handle_type: the type of the raw handle to be wrapped
+// - closer_type: a function object type whose operator() takes a raw handle
+//                and closes it (or does nothing if it is a null handle)
+// - factory_type: a function object type whose operator() returns a null
+//                 handle
+
+template<typename handle_type, typename closer_type, typename factory_type>
+class auto_handle_ref;
+
+template<typename handle_type, typename closer_type, typename factory_type>
+class auto_handle
+    // Use inheritance so we can benefit from the empty base optimisation
+    : private closer_type, private factory_type
+{
+    typedef auto_handle_ref<handle_type, closer_type, factory_type> ref_type;
+public:
+    auto_handle()
+           : handle_(factory_type::operator()())
+       {}
+    explicit auto_handle(handle_type handle)
+           : handle_(handle)
+       {}
+    auto_handle(ref_type other)
+           : handle_(other.release())
+       {}
+    auto_handle & operator=(auto_handle & other)
+       {
+           reset(other.release());
+       }
+    ~auto_handle()
+       {
+           reset();
+       }
+    handle_type get() const
+       {
+           return handle_;
+       }
+    handle_type release()
+       {
+           handle_type handle(handle_);
+           handle_ = factory_type::operator()();
+           return handle;
+       }
+    void reset()
+       {
+           closer_type::operator()(handle_);
+           handle_ = factory_type::operator()();
+       }
+    void reset(handle_type handle)
+       {
+           closer_type::operator()(handle_);
+           handle_ = handle;
+       }
+    operator ref_type()
+       {
+           return ref_type(*this);
+       }
+private:
+    handle_type handle_;
+};
+
+template<typename handle_type, typename closer_type, typename factory_type>
+class auto_handle_ref
+{
+    typedef auto_handle<handle_type, closer_type, factory_type> target_type;
+public:
+    explicit auto_handle_ref(target_type & target)
+           : target_(target)
+       {}
+    handle_type release()
+       {
+           return target_.release();
+       }
+private:
+    target_type & target_;
+};
+
+#endif // !INC_AUTO_HANDLE_H
diff --git a/auto_proc.cpp b/auto_proc.cpp
new file mode 100644 (file)
index 0000000..1e7d70c
--- /dev/null
@@ -0,0 +1,32 @@
+// Copyright 2005 Ben Hutchings <ben@decadentplace.org.uk>.
+// See the file "COPYING" for licence details.
+
+#include <cassert>
+
+#include <errno.h>
+#include <signal.h>
+#include <unistd.h>
+#include <wait.h>
+
+#include "auto_proc.hpp"
+
+void auto_kill_proc_closer::operator()(pid_t pid) const
+{
+    assert(pid >= -1);
+
+    if (pid > 0 && waitpid(pid, NULL, WNOHANG) == 0)
+       kill(pid, SIGTERM);
+}
+
+void auto_wait_proc_closer::operator()(pid_t pid) const
+{
+    assert(pid >= -1);
+
+    if (pid > 0)
+       while (waitpid(pid, NULL, 0) == -1)
+           if (errno != EINTR)
+           {
+               assert(!"invalid pid in auto_wait_proc_closer");
+               break;
+           }
+}
diff --git a/auto_proc.hpp b/auto_proc.hpp
new file mode 100644 (file)
index 0000000..3dd2b66
--- /dev/null
@@ -0,0 +1,30 @@
+// Copyright 2005 Ben Hutchings <ben@decadentplace.org.uk>.
+// See the file "COPYING" for licence details.
+
+#ifndef INC_AUTO_PROC_HPP
+#define INC_AUTO_PROC_HPP
+
+#include <sys/types.h>
+
+#include "auto_handle.hpp"
+
+struct auto_proc_factory
+{
+    pid_t operator()() const { return -1; }
+};
+
+struct auto_kill_proc_closer
+{
+    void operator()(pid_t pid) const;
+};
+typedef auto_handle<pid_t, auto_kill_proc_closer, auto_proc_factory>
+    auto_kill_proc;
+
+struct auto_wait_proc_closer
+{
+    void operator()(pid_t pid) const;
+};
+typedef auto_handle<pid_t, auto_wait_proc_closer, auto_proc_factory>
+    auto_wait_proc;
+
+#endif // !INC_AUTO_PROC_HPP
diff --git a/auto_temp_file.hpp b/auto_temp_file.hpp
new file mode 100644 (file)
index 0000000..5aa3961
--- /dev/null
@@ -0,0 +1,28 @@
+// Copyright 2005 Ben Hutchings <ben@decadentplace.org.uk>.
+// See the file "COPYING" for licence details.
+
+#ifndef INC_AUTO_TEMP_FILE_HPP
+#define INC_AUTO_TEMP_FILE_HPP
+
+#include "auto_handle.hpp"
+
+#include <string>
+
+#include <unistd.h>
+
+struct auto_temp_file_closer
+{
+    void operator()(const std::string & name) const
+       {
+           if (!name.empty())
+               unlink(name.c_str());
+       }
+};
+struct auto_temp_file_factory
+{
+    std::string operator()() const { return std::string(); }
+};
+typedef auto_handle<std::string, auto_temp_file_closer, auto_temp_file_factory>
+    auto_temp_file;
+
+#endif // !INC_AUTO_TEMP_FILE_HPP
diff --git a/browserwidget.cpp b/browserwidget.cpp
new file mode 100644 (file)
index 0000000..77c1846
--- /dev/null
@@ -0,0 +1,532 @@
+// Copyright 2005 Ben Hutchings <ben@decadentplace.org.uk>.
+// See the file "COPYING" for licence details.
+
+#include "browserwidget.hpp"
+
+#include <cassert>
+
+#include <gtkmozembed_internal.h>
+
+BrowserWidget::BrowserWidget()
+       : Gtk::Bin(GTK_BIN(gtk_moz_embed_new()))
+{
+}
+BrowserWidget::~BrowserWidget()
+{
+}
+
+GtkMozEmbed * BrowserWidget::gobj()
+{
+    return GTK_MOZ_EMBED(gobject_);
+}
+const GtkMozEmbed * BrowserWidget::gobj() const
+{
+    return GTK_MOZ_EMBED(gobject_);
+}
+
+void BrowserWidget::load_uri(const char * uri)
+{
+    gtk_moz_embed_load_url(gobj(), uri);
+}
+void BrowserWidget::load_uri(const std::string & uri)
+{
+    return load_uri(uri.c_str());
+}
+void BrowserWidget::stop_load()
+{
+    gtk_moz_embed_stop_load(gobj());
+}
+void BrowserWidget::go_back()
+{
+    gtk_moz_embed_go_back(gobj());
+}
+void BrowserWidget::go_forward()
+{
+    gtk_moz_embed_go_forward(gobj());
+}
+void BrowserWidget::reload(gint32 flags)
+{
+    gtk_moz_embed_reload(gobj(), flags);
+}
+
+bool BrowserWidget::can_go_back() const
+{
+    return gtk_moz_embed_can_go_back(const_cast<GtkMozEmbed *>(gobj()));
+}
+bool BrowserWidget::can_go_forward() const
+{
+    return gtk_moz_embed_can_go_forward(const_cast<GtkMozEmbed *>(gobj()));
+}
+
+namespace
+{
+    template<typename T>
+    class c_scoped_ptr
+    {
+    public:
+       explicit c_scoped_ptr(T * p = 0) : p_(p) {}
+       ~c_scoped_ptr() { free(p_); }
+       T * get() const { return p_; }
+       T * release()
+       {
+           T * p = p_;
+           p_ = NULL;
+           return p;
+       }
+       void reset(T * p = 0)
+       {
+           free(p_);
+           p_ = p;
+       }
+    private:
+       T * p_;
+    };
+}
+
+std::string BrowserWidget::get_link_message() const
+{
+    c_scoped_ptr<char> str(
+       gtk_moz_embed_get_link_message(const_cast<GtkMozEmbed *>(gobj())));
+    return std::string(str.get());
+}
+std::string BrowserWidget::get_js_status() const
+{
+    c_scoped_ptr<char> str(
+       gtk_moz_embed_get_js_status(const_cast<GtkMozEmbed *>(gobj())));
+    return std::string(str.get());
+}
+std::string BrowserWidget::get_title() const
+{
+    c_scoped_ptr<char> str(
+       gtk_moz_embed_get_title(const_cast<GtkMozEmbed *>(gobj())));
+    return std::string(str.get());
+}
+std::string BrowserWidget::get_location() const
+{
+    c_scoped_ptr<char> str(
+       gtk_moz_embed_get_location(const_cast<GtkMozEmbed *>(gobj())));
+    return std::string(str.get());
+}
+already_AddRefed<nsIWebBrowser> BrowserWidget::get_browser()
+{
+    nsIWebBrowser * result = 0;
+    gtk_moz_embed_get_nsIWebBrowser(gobj(), &result);
+    assert(result);
+    return dont_AddRef(result);
+}
+
+namespace
+{
+    void BrowserWidget_signal_link_message_callback(GtkMozEmbed * self, void * data)
+    {
+       typedef SigC::Slot0<void> SlotType;
+
+       if (Glib::ObjectBase::_get_current_wrapper((GObject *)self))
+       {
+           try
+           {
+               if (SigC::SlotNode * const slot =
+                   Glib::SignalProxyNormal::data_to_slot(data))
+                   (*(SlotType::Proxy)(slot->proxy_))(slot);
+           }
+           catch(...)
+           {
+               Glib::exception_handlers_invoke();
+           }
+       }
+    }
+
+    const Glib::SignalProxyInfo BrowserWidget_signal_link_message_info =
+    {
+       "link_message",
+       (GCallback) &BrowserWidget_signal_link_message_callback,
+       (GCallback) &BrowserWidget_signal_link_message_callback
+    };
+
+    void BrowserWidget_signal_js_status_callback(GtkMozEmbed * self, void * data)
+    {
+       typedef SigC::Slot0<void> SlotType;
+
+       if (Glib::ObjectBase::_get_current_wrapper((GObject *)self))
+       {
+           try
+           {
+               if (SigC::SlotNode * const slot =
+                   Glib::SignalProxyNormal::data_to_slot(data))
+                   (*(SlotType::Proxy)(slot->proxy_))(slot);
+           }
+           catch(...)
+           {
+               Glib::exception_handlers_invoke();
+           }
+       }
+    }
+
+    const Glib::SignalProxyInfo BrowserWidget_signal_js_status_info =
+    {
+       "js_status",
+       (GCallback) &BrowserWidget_signal_js_status_callback,
+       (GCallback) &BrowserWidget_signal_js_status_callback
+    };
+
+    void BrowserWidget_signal_location_callback(GtkMozEmbed * self, void * data)
+    {
+       typedef SigC::Slot0<void> SlotType;
+
+       if (Glib::ObjectBase::_get_current_wrapper((GObject *)self))
+       {
+           try
+           {
+               if (SigC::SlotNode * const slot =
+                   Glib::SignalProxyNormal::data_to_slot(data))
+                   (*(SlotType::Proxy)(slot->proxy_))(slot);
+           }
+           catch(...)
+           {
+               Glib::exception_handlers_invoke();
+           }
+       }
+    }
+
+    const Glib::SignalProxyInfo BrowserWidget_signal_location_info =
+    {
+       "location",
+       (GCallback) &BrowserWidget_signal_location_callback,
+       (GCallback) &BrowserWidget_signal_location_callback
+    };
+
+    void BrowserWidget_signal_title_callback(GtkMozEmbed * self, void * data)
+    {
+       typedef SigC::Slot0<void> SlotType;
+
+       if (Glib::ObjectBase::_get_current_wrapper((GObject *)self))
+       {
+           try
+           {
+               if (SigC::SlotNode * const slot =
+                   Glib::SignalProxyNormal::data_to_slot(data))
+                   (*(SlotType::Proxy)(slot->proxy_))(slot);
+           }
+           catch(...)
+           {
+               Glib::exception_handlers_invoke();
+           }
+       }
+    }
+
+    const Glib::SignalProxyInfo BrowserWidget_signal_title_info =
+    {
+       "title",
+       (GCallback) &BrowserWidget_signal_title_callback,
+       (GCallback) &BrowserWidget_signal_title_callback
+    };
+
+    void BrowserWidget_signal_progress_callback(
+       GtkMozEmbed * self, gint p0, gint p1, void * data)
+    {
+       typedef SigC::Slot2<void, gint, gint> SlotType;
+
+       if (Glib::ObjectBase::_get_current_wrapper((GObject *)self))
+       {
+           try
+           {
+               if (SigC::SlotNode * const slot =
+                   Glib::SignalProxyNormal::data_to_slot(data))
+                   (*(SlotType::Proxy)(slot->proxy_))(p0, p1, slot);
+           }
+           catch(...)
+           {
+               Glib::exception_handlers_invoke();
+           }
+       }
+    }
+
+    const Glib::SignalProxyInfo BrowserWidget_signal_progress_info =
+    {
+       "progress",
+       (GCallback) &BrowserWidget_signal_progress_callback,
+       (GCallback) &BrowserWidget_signal_progress_callback
+    };
+
+    void BrowserWidget_signal_net_state_callback(
+       GtkMozEmbed * self, const char * p0, gint p1, guint p2, void * data)
+    {
+       typedef SigC::Slot3<void, const char *, gint, guint> SlotType;
+
+       if (Glib::ObjectBase::_get_current_wrapper((GObject *)self))
+       {
+           try
+           {
+               if (SigC::SlotNode * const slot =
+                   Glib::SignalProxyNormal::data_to_slot(data))
+                   (*(SlotType::Proxy)(slot->proxy_))(p0, p1, p2, slot);
+           }
+           catch(...)
+           {
+               Glib::exception_handlers_invoke();
+           }
+       }
+    }
+
+    const Glib::SignalProxyInfo BrowserWidget_signal_net_state_info =
+    {
+       "net_state_all",
+       (GCallback) &BrowserWidget_signal_net_state_callback,
+       (GCallback) &BrowserWidget_signal_net_state_callback
+    };
+
+    void BrowserWidget_signal_net_start_callback(GtkMozEmbed * self, void * data)
+    {
+       typedef SigC::Slot0<void> SlotType;
+
+       if (Glib::ObjectBase::_get_current_wrapper((GObject *)self))
+       {
+           try
+           {
+               if (SigC::SlotNode * const slot =
+                   Glib::SignalProxyNormal::data_to_slot(data))
+                   (*(SlotType::Proxy)(slot->proxy_))(slot);
+           }
+           catch(...)
+           {
+               Glib::exception_handlers_invoke();
+           }
+       }
+    }
+
+    const Glib::SignalProxyInfo BrowserWidget_signal_net_start_info =
+    {
+       "net_start",
+       (GCallback) &BrowserWidget_signal_net_start_callback,
+       (GCallback) &BrowserWidget_signal_net_start_callback
+    };
+
+    void BrowserWidget_signal_net_stop_callback(GtkMozEmbed * self, void * data)
+    {
+       typedef SigC::Slot0<void> SlotType;
+
+       if (Glib::ObjectBase::_get_current_wrapper((GObject *)self))
+       {
+           try
+           {
+               if (SigC::SlotNode * const slot =
+                   Glib::SignalProxyNormal::data_to_slot(data))
+                   (*(SlotType::Proxy)(slot->proxy_))(slot);
+           }
+           catch(...)
+           {
+               Glib::exception_handlers_invoke();
+           }
+       }
+    }
+
+    const Glib::SignalProxyInfo BrowserWidget_signal_net_stop_info =
+    {
+       "net_stop",
+       (GCallback) &BrowserWidget_signal_net_stop_callback,
+       (GCallback) &BrowserWidget_signal_net_stop_callback
+    };
+
+    void BrowserWidget_signal_new_window_callback(
+       GtkMozEmbed * self, GtkMozEmbed ** p0, guint p1, void * data)
+    {
+       typedef SigC::Slot1<BrowserWidget *, guint> SlotType;
+
+       if (Glib::ObjectBase::_get_current_wrapper((GObject *)self))
+       {
+           try
+           {
+               if (SigC::SlotNode * const slot =
+                   Glib::SignalProxyNormal::data_to_slot(data))
+               {
+                   if (BrowserWidget * result =
+                       (*(SlotType::Proxy)(slot->proxy_))(p1, slot))
+                   {
+                       *p0 = result->gobj();
+                       return;
+                   }
+               }
+           }
+           catch(...)
+           {
+               Glib::exception_handlers_invoke();
+           }
+       }
+
+       *p0 = NULL;
+       return;
+    }
+
+    const Glib::SignalProxyInfo BrowserWidget_signal_new_window_info =
+    {
+       "new_window",
+       (GCallback) &BrowserWidget_signal_new_window_callback,
+       (GCallback) &BrowserWidget_signal_new_window_callback
+    };
+
+    void BrowserWidget_signal_visibility_callback(
+       GtkMozEmbed * self, gboolean p0, void * data)
+    {
+       typedef SigC::Slot1<void, bool> SlotType;
+
+       if (Glib::ObjectBase::_get_current_wrapper((GObject *)self))
+       {
+           try
+           {
+               if (SigC::SlotNode * const slot =
+                   Glib::SignalProxyNormal::data_to_slot(data))
+                   (*(SlotType::Proxy)(slot->proxy_))(p0, slot);
+           }
+           catch(...)
+           {
+               Glib::exception_handlers_invoke();
+           }
+       }
+    }
+
+    const Glib::SignalProxyInfo BrowserWidget_signal_visibility_info =
+    {
+       "visibility",
+       (GCallback) &BrowserWidget_signal_visibility_callback,
+       (GCallback) &BrowserWidget_signal_visibility_callback
+    };
+
+    void BrowserWidget_signal_destroy_browser_callback(
+       GtkMozEmbed * self, void * data)
+    {
+       typedef SigC::Slot0<void> SlotType;
+
+       if (Glib::ObjectBase::_get_current_wrapper((GObject *)self))
+       {
+           try
+           {
+               if (SigC::SlotNode * const slot =
+                   Glib::SignalProxyNormal::data_to_slot(data))
+                   (*(SlotType::Proxy)(slot->proxy_))(slot);
+           }
+           catch(...)
+           {
+               Glib::exception_handlers_invoke();
+           }
+       }
+    }
+
+    const Glib::SignalProxyInfo BrowserWidget_signal_destroy_info =
+    {
+       "destroy_browser",
+       (GCallback) &BrowserWidget_signal_destroy_browser_callback,
+       (GCallback) &BrowserWidget_signal_destroy_browser_callback
+    };
+
+    gint BrowserWidget_signal_open_uri_callback(
+       GtkMozEmbed * self, const char * p0, void * data)
+    {
+       typedef SigC::Slot1<bool, const char *> SlotType;
+
+       if (Glib::ObjectBase::_get_current_wrapper((GObject *)self))
+       {
+           try
+           {
+               if (SigC::SlotNode * const slot =
+                   Glib::SignalProxyNormal::data_to_slot(data))
+                   return (*(SlotType::Proxy)(slot->proxy_))(p0, slot);
+           }
+           catch(...)
+           {
+               Glib::exception_handlers_invoke();
+           }
+       }
+
+       return 0;
+    }
+
+    const Glib::SignalProxyInfo BrowserWidget_signal_open_uri_info =
+    {
+       "open_uri",
+       (GCallback) &BrowserWidget_signal_open_uri_callback,
+       (GCallback) &BrowserWidget_signal_open_uri_callback
+    };
+
+} // namespace
+
+Glib::SignalProxy0<void> BrowserWidget::signal_link_message()
+{
+    return Glib::SignalProxy0<void>(this, &BrowserWidget_signal_link_message_info);
+}
+Glib::SignalProxy0<void> BrowserWidget::signal_js_status()
+{
+    return Glib::SignalProxy0<void>(this, &BrowserWidget_signal_js_status_info);
+}
+Glib::SignalProxy0<void> BrowserWidget::signal_location()
+{
+    return Glib::SignalProxy0<void>(this, &BrowserWidget_signal_location_info);
+}
+Glib::SignalProxy0<void> BrowserWidget::signal_title()
+{
+    return Glib::SignalProxy0<void>(this, &BrowserWidget_signal_title_info);
+}
+Glib::SignalProxy2<void, gint /*cur*/, gint /*max*/> BrowserWidget::signal_progress()
+{
+    return Glib::SignalProxy2<void, gint, gint>(
+       this, &BrowserWidget_signal_progress_info);
+}
+Glib::SignalProxy3<void, const char *, gint /*flags*/, guint /*status*/>
+BrowserWidget::signal_net_state()
+{
+    return Glib::SignalProxy3<void, const char *, gint, guint>(
+       this, &BrowserWidget_signal_net_state_info);
+}
+Glib::SignalProxy0<void> BrowserWidget::signal_net_start()
+{
+    return Glib::SignalProxy0<void>(this, &BrowserWidget_signal_net_start_info);
+}
+Glib::SignalProxy0<void> BrowserWidget::signal_net_stop()
+{
+    return Glib::SignalProxy0<void>(this, &BrowserWidget_signal_net_stop_info);
+}
+Glib::SignalProxy1<BrowserWidget *, guint /*chromemask*/> BrowserWidget::signal_new_window()
+{
+    return Glib::SignalProxy1<BrowserWidget *, guint>(
+       this, &BrowserWidget_signal_new_window_info);
+}
+Glib::SignalProxy1<void, bool /*visibility*/> BrowserWidget::signal_visibility()
+{
+    return Glib::SignalProxy1<void, bool>(
+       this, &BrowserWidget_signal_visibility_info);
+}
+Glib::SignalProxy0<void> BrowserWidget::signal_destroy()
+{
+    return Glib::SignalProxy0<void>(this, &BrowserWidget_signal_destroy_info);
+}
+Glib::SignalProxy1<bool, const char * /*uri*/> BrowserWidget::signal_open_uri()
+{
+    return Glib::SignalProxy1<bool, const char *>(
+       this, &BrowserWidget_signal_open_uri_info);
+}
+
+BrowserWidget::BrowserWidget(GObject * gobject, bool take_copy)
+{
+    assert(GTK_MOZ_EMBED(gobject));
+    gobject_ = gobject;
+    if (take_copy)
+       reference();
+}
+Glib::ObjectBase * BrowserWidget::wrap_new(GObject * gobject)
+{
+    return new BrowserWidget(gobject, false);
+}
+
+void BrowserWidget::init()
+{
+    gtk_moz_embed_set_comp_path(MOZ_LIB_DIR);
+    wrap_register(gtk_moz_embed_get_type(), wrap_new);
+}
+
+namespace Glib
+{
+    BrowserWidget * wrap(GtkMozEmbed * object, bool take_copy)
+    {
+       return dynamic_cast<BrowserWidget *>(
+           Glib::wrap_auto((GObject*)(object), take_copy));
+    }
+}
diff --git a/browserwidget.hpp b/browserwidget.hpp
new file mode 100644 (file)
index 0000000..1d1e85d
--- /dev/null
@@ -0,0 +1,69 @@
+// Copyright 2005 Ben Hutchings <ben@decadentplace.org.uk>.
+// See the file "COPYING" for licence details.
+
+#ifndef INC_BROWSERWIDGET_HPP
+#define INC_BROWSERWIDGET_HPP
+
+#include <glibmm/signalproxy.h>
+#include <gtkmm/bin.h>
+
+#include <gtkmozembed.h>
+#include <nsCOMPtr.h>
+
+class BrowserWidget;
+class nsIWebBrowser;
+
+namespace Glib
+{
+    BrowserWidget * wrap(GtkMozEmbed * object, bool take_copy = false);
+}
+
+class BrowserWidget : public Gtk::Bin
+{
+public:
+    BrowserWidget();
+    virtual ~BrowserWidget();
+    GtkMozEmbed * gobj();
+    const GtkMozEmbed * gobj() const;
+
+    void load_uri(const char * uri);
+    void load_uri(const std::string & uri);
+    void stop_load();
+    void go_back();
+    void go_forward();
+    void reload(gint32 flags = GTK_MOZ_EMBED_FLAG_RELOADNORMAL);
+
+    bool can_go_back() const;
+    bool can_go_forward() const;
+
+    std::string get_link_message() const;
+    std::string get_js_status() const;
+    std::string get_title() const;
+    std::string get_location() const;
+    already_AddRefed<nsIWebBrowser> get_browser();
+
+    Glib::SignalProxy0<void> signal_link_message();
+    Glib::SignalProxy0<void> signal_js_status();
+    Glib::SignalProxy0<void> signal_location();
+    Glib::SignalProxy0<void> signal_title();
+    Glib::SignalProxy2<void, gint /*cur*/, gint /*max*/> signal_progress();
+    Glib::SignalProxy3<void, const char *, gint /*flags*/, guint /*status*/>
+       signal_net_state();
+    Glib::SignalProxy0<void> signal_net_start();
+    Glib::SignalProxy0<void> signal_net_stop();
+    Glib::SignalProxy1<BrowserWidget *, guint /*chromemask*/> signal_new_window();
+    Glib::SignalProxy1<void, bool /*visibility*/> signal_visibility();
+    Glib::SignalProxy0<void> signal_destroy();
+    Glib::SignalProxy1<bool, const char * /*uri*/> signal_open_uri();
+
+    // This must be called after Gtk initialisation and before instantiation
+    // of BrowserWidget.
+    static void init();
+
+private:
+    BrowserWidget(GObject * gobject, bool take_copy);
+    static Glib::ObjectBase * wrap_new(GObject * gobject);
+    friend BrowserWidget * Glib::wrap(GtkMozEmbed * object, bool take_copy);
+};
+
+#endif // !INC_BROWSERWIDGET_HPP
diff --git a/childiterator.cpp b/childiterator.cpp
new file mode 100644 (file)
index 0000000..285b4a8
--- /dev/null
@@ -0,0 +1,46 @@
+// Copyright 2005 Ben Hutchings <ben@decadentplace.org.uk>.
+// See the file "COPYING" for licence details.
+
+#include "childiterator.hpp"
+
+#include <cassert>
+
+#include "xpcom_support.hpp"
+
+using xpcom_support::check;
+
+ChildIterator::ChildIterator()
+       : node_(0)
+{}
+
+ChildIterator::ChildIterator(nsIDOMNode * node)
+{
+    check(node->GetFirstChild(&node_));
+}
+
+ChildIterator::~ChildIterator()
+{
+    if (node_)
+       node_->Release();
+}
+
+already_AddRefed<nsIDOMNode> ChildIterator::operator*() const
+{
+    assert(node_);
+    node_->AddRef();
+    return node_;
+}
+
+ChildIterator & ChildIterator::operator++()
+{
+    nsIDOMNode * next;
+    check(node_->GetNextSibling(&next));
+    node_->Release();
+    node_ = next;
+    return *this;
+}
+
+bool ChildIterator::operator==(const ChildIterator & other) const
+{
+    return node_ == other.node_;
+}
diff --git a/childiterator.hpp b/childiterator.hpp
new file mode 100644 (file)
index 0000000..edc937c
--- /dev/null
@@ -0,0 +1,33 @@
+// Copyright 2005 Ben Hutchings <ben@decadentplace.org.uk>.
+// See the file "COPYING" for licence details.
+
+#ifndef INC_CHILDITERATOR_HPP
+#define INC_CHILDITERATOR_HPP
+
+#include <iterator>
+
+#include <nsCOMPtr.h>
+#include <nsIDOMNode.h>
+
+class ChildIterator
+    : public std::iterator<std::input_iterator_tag, nsCOMPtr<nsIDOMNode>,
+                          void, void, void>
+{
+public:
+    ChildIterator();
+    explicit ChildIterator(nsIDOMNode * node);
+    ~ChildIterator();
+
+    already_AddRefed<nsIDOMNode> operator*() const;
+    ChildIterator & operator++();
+    bool operator==(const ChildIterator &) const;
+    bool operator!=(const ChildIterator & other) const
+       {
+           return !(*this == other);
+       }
+
+private:
+    nsIDOMNode * node_;
+};
+
+#endif // !INC_CHILDITERATOR_HPP
diff --git a/dvd.hpp b/dvd.hpp
new file mode 100644 (file)
index 0000000..e2bd0f9
--- /dev/null
+++ b/dvd.hpp
@@ -0,0 +1,23 @@
+#ifndef INC_DVD_HPP
+#define INC_DVD_HPP
+
+namespace dvd
+{
+    // Maximum number of buttons in a menu.
+    // TODO: Check whether this is a dvdauthor limitation or part of the
+    // DVD Video spec.
+    const int menu_buttons_max = 36;
+
+    // Number of colours allowed in each button.
+    // Buttons can change colour when they are selected.
+    const int button_n_colours = 4;
+
+    // DVD virtual machine register size.
+    const int reg_bits = 16;
+
+    // Number by which button numbers must be multiplied when stored in
+    // system register 8.
+    const int reg_s8_button_mult = 0x400;
+}
+
+#endif // !INC_DVD_HPP
diff --git a/framebuffer.cpp b/framebuffer.cpp
new file mode 100644 (file)
index 0000000..eab879a
--- /dev/null
@@ -0,0 +1,178 @@
+// Copyright 2005 Ben Hutchings <ben@decadentplace.org.uk>.
+// See the file "COPYING" for licence details.
+
+#include <cassert>
+#include <cstdio>
+#include <cstring>
+#include <stdexcept>
+
+#include <sys/types.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <netinet/in.h>
+#include <sys/stat.h>
+#include <sys/utsname.h>
+#include <unistd.h>
+#include <wait.h>
+
+#include "framebuffer.hpp"
+#include "auto_fd.hpp"
+
+namespace
+{
+    int select_display_num()
+    {
+       // Minimum and maximum display numbers to use.  Xvnc and ssh's
+       // proxies start at 10, so we'll follow that convention.  We
+       // have to put a limit on iteration somewhere, and 100
+       // displays seems rather excessive so we'll stop there.
+       const int min_display_num = 10;
+       const int max_display_num = 99;
+
+       // Note that we have to leave it to the X server to create the
+       // lock file for the selected display number, which leaves a
+       // race condition.  We could perhaps read its error stream to
+       // detect the case where another server grabs the display
+       // number before it.
+       char lock_file_name[20];
+       for (int display_num = min_display_num;
+            display_num <= max_display_num;
+            ++display_num)
+       {
+           // Check whether a lock file exists for this display.  Really we
+           // should also check for stale locks, but this will probably do.
+           std::sprintf(lock_file_name, "/tmp/.X%d-lock", display_num);
+           if (access(lock_file_name, 0) == -1 && errno == ENOENT)
+               return display_num;
+       }
+
+       throw std::runtime_error("did not find a free X display");
+    }
+
+    void get_random_bytes(unsigned char * buf, std::size_t len)
+    {
+       auto_fd random_fd(open("/dev/urandom", O_RDONLY));
+       if (random_fd.get() == -1 || read(random_fd.get(), buf, len) != len)
+           throw std::runtime_error(std::strerror(errno));
+    }
+
+    auto_temp_file create_temp_auth_file(int display_num)
+    {
+       char auth_file_name[] = "/tmp/Xvfb-auth-XXXXXX";
+       auto_fd auth_file_fd(mkstemp(auth_file_name));
+       if (auth_file_fd.get() == -1)
+           throw std::runtime_error(std::strerror(errno));
+       auto_temp_file auth_file(auth_file_name);
+
+       // mkstemp may use lax permissions, so fix that before writing
+       // the auth data to it.
+       fchmod(auth_file_fd.get(), S_IREAD|S_IWRITE);
+       ftruncate(auth_file_fd.get(), 0);
+
+       // An xauth entry consists of the following fields.  All u16 fields
+       // are big-endian and unaligned.  Character arrays are not null-
+       // terminated.
+       // u16     address family (= 256 for local socket)
+       // u16     length of address
+       // char[]  address (= hostname)
+       // u16     length of display number
+       // char[]  display number
+       // u16     auth type name length
+       // char[]  auth type name (= "MIT-MAGIC-COOKIE-1")
+       // u16     length of auth data (= 16)
+       // char[]  auth data (= random bytes)
+       uint16_t family = htons(0x100);
+       write(auth_file_fd.get(), &family, sizeof(family));
+       utsname my_uname;
+       uname(&my_uname);
+       uint16_t len = htons(strlen(my_uname.nodename));
+       write(auth_file_fd.get(), &len, sizeof(len));
+       write(auth_file_fd.get(),
+             my_uname.nodename, strlen(my_uname.nodename));
+       char display[15];
+       std::sprintf(display, "%d", display_num);
+       len = htons(strlen(display));
+       write(auth_file_fd.get(), &len, sizeof(len));
+       write(auth_file_fd.get(), display, strlen(display));
+       static const char auth_type[] = "MIT-MAGIC-COOKIE-1";
+       len = htons(sizeof(auth_type) - 1);
+       write(auth_file_fd.get(), &len, sizeof(len));
+       write(auth_file_fd.get(), auth_type, sizeof(auth_type) - 1);
+       unsigned char auth_key[16];
+       get_random_bytes(auth_key, sizeof(auth_key));
+       len = htons(sizeof(auth_key));
+       write(auth_file_fd.get(), &len, sizeof(len));
+       write(auth_file_fd.get(), auth_key, sizeof(auth_key));
+
+       return auth_file;
+    }
+
+    // Run the X server with the specified auth file, dimensions and
+    // assigned display number.
+    auto_kill_proc spawn_x_server(int display_num,
+                                 const std::string & auth_file_name,
+                                 int width, int height, int depth)
+    {
+       char display[15];
+       std::sprintf(display, ":%d", display_num);
+       const char * auth_file_c_str = auth_file_name.c_str();
+       std::fflush(NULL);
+       auto_kill_proc server_proc(fork());
+       if (server_proc.get() == -1)
+           throw std::runtime_error(std::strerror(errno));
+
+       if (server_proc.get() == 0)
+       {
+           char dimensions[40];
+           std::sprintf(dimensions, "%dx%dx%d", width, height, depth);
+           execlp("Xvfb",
+                  "Xvfb",
+                  "-auth", auth_file_c_str,
+                  "-screen", "0", dimensions,
+                  display,
+                  NULL);
+           _exit(128 + errno);
+       }
+
+       // Wait for the lock file to appear or the server to exit.  We can't
+       // really wait on both of these, so poll at 1-second intervals.
+       char lock_file_name[20];
+       std::sprintf(lock_file_name, "/tmp/.X%d-lock", display_num);
+       for (;;)
+       {
+           if (access(lock_file_name, 0) == 0)
+               break;
+           if (errno != ENOENT) // huh?
+               throw std::runtime_error(std::strerror(errno));
+           if (waitpid(server_proc.get(), NULL, WNOHANG) == server_proc.get())
+           {
+               server_proc.release(); // pid is now invalid
+               // TODO: Get the exit status and decode it properly.
+               throw std::runtime_error("X server failed to create display");
+           }
+           sleep(1);
+       }
+
+       return server_proc;
+    }
+}
+
+FrameBuffer::FrameBuffer(int width, int height, int depth)
+       : display_num_(select_display_num()),
+         auth_file_(create_temp_auth_file(display_num_)),
+         server_proc_(spawn_x_server(display_num_,
+                                     get_x_authority(),
+                                     width, height, depth))
+{}
+
+std::string FrameBuffer::get_x_authority() const
+{
+    return auth_file_.get();
+}
+
+std::string FrameBuffer::get_x_display() const
+{
+    char display[15];
+    std::sprintf(display, ":%d", display_num_);
+    return display;
+}
diff --git a/framebuffer.hpp b/framebuffer.hpp
new file mode 100644 (file)
index 0000000..daf7659
--- /dev/null
@@ -0,0 +1,26 @@
+// Copyright 2005 Ben Hutchings <ben@decadentplace.org.uk>.
+// See the file "COPYING" for licence details.
+
+#ifndef INC_FRAMEBUFFER_HPP
+#define INC_FRAMEBUFFER_HPP
+
+#include <string>
+
+#include "auto_proc.hpp"
+#include "auto_temp_file.hpp"
+
+// Run Xvfb with a frame buffer of the given dimensions.
+class FrameBuffer
+{
+public:
+    FrameBuffer(int width, int height, int depth);
+    std::string get_x_authority() const;
+    std::string get_x_display() const;
+
+private:
+    int display_num_;
+    auto_temp_file auth_file_;
+    auto_kill_proc server_proc_;
+};
+
+#endif // !INC_FRAMEBUFFER_HPP
diff --git a/jquant2.c b/jquant2.c
new file mode 100644 (file)
index 0000000..3f46bac
--- /dev/null
+++ b/jquant2.c
@@ -0,0 +1,1471 @@
+/*
+ * Copyright 2005 Ben Hutchings.
+ *
+ * This is derived from jquant2.c and parts of jdmaster.c, jmorecfg.h,
+ * jpegint.h and jpeglib.h, from the Independent JPEG Group's software.
+ * Copyright (C) 1991-1998, Thomas G. Lane.
+ *
+ * This file contains 2-pass color quantization (color mapping) routines.
+ * These routines provide selection of a custom color map for an image,
+ * followed by mapping of the image to that color map, with optional
+ * Floyd-Steinberg dithering.
+ * It is also possible to use just the second pass to map to an arbitrary
+ * externally-given color map.
+ *
+ * Note: ordered dithering is not supported, since there isn't any fast
+ * way to compute intercolor distances; it's unclear that ordered dither's
+ * fundamental assumptions even hold with an irregularly spaced color map.
+ */
+
+/*
+ * These definitions cover what would normally be defind in jconfig.h,
+ * jmorecfg.h, jpegint.h and jpeglib.h.
+ */
+
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+
+#include "jquant2.h"
+
+/*
+ * This controls whether an additional alpha channel is expected in the
+ * input buffer.  If so, output color 0 is reserved for fully transparent
+ * pixels and the alpha channel is otherwise ignored.  That is, only
+ * binary transparency is really supported.
+ */
+#define HAS_ALPHA 1
+
+typedef uint16_t UINT16;
+typedef int16_t INT16;
+typedef int32_t INT32;
+typedef int boolean;
+
+#define BITS_IN_JSAMPLE 8
+#define MAXJSAMPLE 255
+#define CENTERJSAMPLE 128
+#define GETJSAMPLE(value) ((int) (value))
+
+typedef int JDIMENSION;
+
+#define RGB_RED 0
+#define RGB_GREEN 1
+#define RGB_BLUE 2
+
+#define FAR
+#define SIZEOF sizeof
+
+#define TRUE 1
+#define FALSE 0
+
+#ifndef NULL
+#define NULL 0
+#endif
+
+#define JMETHOD(return_type, name, param_list) return_type (* name) param_list
+#define METHODDEF(return_type) static return_type
+#define LOCAL(return_type) static return_type
+#define GLOBAL(return_type) return_type
+
+#define TRACEMS1(cinfo, b, message_num, param_1)
+/* FIXME */
+#define ERREXIT(cinfo, message_num) abort()
+#define ERREXIT1(cinfo, message_num, param_1) abort()
+
+#define SHIFT_TEMPS
+#define RIGHT_SHIFT(x,shft) ((x) >> (shft))
+
+#define jzero_far(base, length) memset(base, 0, length)
+#define MEMZERO(base, length) memset(base, 0, length)
+#define MEMCOPY(dest, source, length) memcpy(dest, source, length)
+
+struct jpeg_decompress_struct {
+
+  J_DITHER_MODE dither_mode;   /* type of color dithering to use */
+
+  /* Description of actual output image that will be returned to application.
+   * These fields are computed by jpeg_start_decompress().
+   * You can also use jpeg_calc_output_dimensions() to determine these values
+   * in advance of calling jpeg_start_decompress().
+   */
+  JDIMENSION output_width;     /* scaled image width */
+  JDIMENSION output_height;    /* scaled image height */
+
+  /* When quantizing colors, the output colormap is described by these fields.
+   * The application can supply a colormap by setting colormap non-NULL before
+   * calling jpeg_start_decompress; otherwise a colormap is created during
+   * jpeg_start_decompress or jpeg_start_output.
+   * The map has 3 rows and actual_number_of_colors columns.
+   */
+  int actual_number_of_colors; /* number of entries in use */
+  JSAMPARRAY colormap;         /* The color map as a 2-D pixel array */
+  JSAMPLE * sample_range_limit; /* table for fast range-limiting */
+
+  struct jpeg_color_quantizer * cquantize;
+};
+
+typedef struct jpeg_decompress_struct * j_decompress_ptr;
+typedef j_decompress_ptr j_common_ptr;
+
+struct jpeg_color_quantizer {
+  JMETHOD(void, start_pass, (j_decompress_ptr cinfo, boolean is_pre_scan));
+  JMETHOD(void, color_quantize, (j_decompress_ptr cinfo,
+                                JSAMPARRAY input_buf, JSAMPARRAY output_buf,
+                                int num_rows));
+  JMETHOD(void, finish_pass, (j_decompress_ptr cinfo));
+};
+
+/*
+ * This module implements the well-known Heckbert paradigm for color
+ * quantization.  Most of the ideas used here can be traced back to
+ * Heckbert's seminal paper
+ *   Heckbert, Paul.  "Color Image Quantization for Frame Buffer Display",
+ *   Proc. SIGGRAPH '82, Computer Graphics v.16 #3 (July 1982), pp 297-304.
+ *
+ * In the first pass over the image, we accumulate a histogram showing the
+ * usage count of each possible color.  To keep the histogram to a reasonable
+ * size, we reduce the precision of the input; typical practice is to retain
+ * 5 or 6 bits per color, so that 8 or 4 different input values are counted
+ * in the same histogram cell.
+ *
+ * Next, the color-selection step begins with a box representing the whole
+ * color space, and repeatedly splits the "largest" remaining box until we
+ * have as many boxes as desired colors.  Then the mean color in each
+ * remaining box becomes one of the possible output colors.
+ * 
+ * The second pass over the image maps each input pixel to the closest output
+ * color (optionally after applying a Floyd-Steinberg dithering correction).
+ * This mapping is logically trivial, but making it go fast enough requires
+ * considerable care.
+ *
+ * Heckbert-style quantizers vary a good deal in their policies for choosing
+ * the "largest" box and deciding where to cut it.  The particular policies
+ * used here have proved out well in experimental comparisons, but better ones
+ * may yet be found.
+ *
+ * In earlier versions of the IJG code, this module quantized in YCbCr color
+ * space, processing the raw upsampled data without a color conversion step.
+ * This allowed the color conversion math to be done only once per colormap
+ * entry, not once per pixel.  However, that optimization precluded other
+ * useful optimizations (such as merging color conversion with upsampling)
+ * and it also interfered with desired capabilities such as quantizing to an
+ * externally-supplied colormap.  We have therefore abandoned that approach.
+ * The present code works in the post-conversion color space, typically RGB.
+ *
+ * To improve the visual quality of the results, we actually work in scaled
+ * RGB space, giving G distances more weight than R, and R in turn more than
+ * B.  To do everything in integer math, we must use integer scale factors.
+ * The 2/3/1 scale factors used here correspond loosely to the relative
+ * weights of the colors in the NTSC grayscale equation.
+ * If you want to use this code to quantize a non-RGB color space, you'll
+ * probably need to change these scale factors.
+ */
+
+#define R_SCALE 2              /* scale R distances by this much */
+#define G_SCALE 3              /* scale G distances by this much */
+#define B_SCALE 1              /* and B by this much */
+
+/* Relabel R/G/B as components 0/1/2, respecting the RGB ordering defined
+ * in jmorecfg.h.  As the code stands, it will do the right thing for R,G,B
+ * and B,G,R orders.  If you define some other weird order in jmorecfg.h,
+ * you'll get compile errors until you extend this logic.  In that case
+ * you'll probably want to tweak the histogram sizes too.
+ */
+
+#if RGB_RED == 0
+#define C0_SCALE R_SCALE
+#endif
+#if RGB_BLUE == 0
+#define C0_SCALE B_SCALE
+#endif
+#if RGB_GREEN == 1
+#define C1_SCALE G_SCALE
+#endif
+#if RGB_RED == 2
+#define C2_SCALE R_SCALE
+#endif
+#if RGB_BLUE == 2
+#define C2_SCALE B_SCALE
+#endif
+
+
+/*
+ * First we have the histogram data structure and routines for creating it.
+ *
+ * The number of bits of precision can be adjusted by changing these symbols.
+ * We recommend keeping 6 bits for G and 5 each for R and B.
+ * If you have plenty of memory and cycles, 6 bits all around gives marginally
+ * better results; if you are short of memory, 5 bits all around will save
+ * some space but degrade the results.
+ * To maintain a fully accurate histogram, we'd need to allocate a "long"
+ * (preferably unsigned long) for each cell.  In practice this is overkill;
+ * we can get by with 16 bits per cell.  Few of the cell counts will overflow,
+ * and clamping those that do overflow to the maximum value will give close-
+ * enough results.  This reduces the recommended histogram size from 256Kb
+ * to 128Kb, which is a useful savings on PC-class machines.
+ * (In the second pass the histogram space is re-used for pixel mapping data;
+ * in that capacity, each cell must be able to store zero to the number of
+ * desired colors.  16 bits/cell is plenty for that too.)
+ * Since the JPEG code is intended to run in small memory model on 80x86
+ * machines, we can't just allocate the histogram in one chunk.  Instead
+ * of a true 3-D array, we use a row of pointers to 2-D arrays.  Each
+ * pointer corresponds to a C0 value (typically 2^5 = 32 pointers) and
+ * each 2-D array has 2^6*2^5 = 2048 or 2^6*2^6 = 4096 entries.  Note that
+ * on 80x86 machines, the pointer row is in near memory but the actual
+ * arrays are in far memory (same arrangement as we use for image arrays).
+ */
+
+#define MAXNUMCOLORS  (MAXJSAMPLE+1) /* maximum size of colormap */
+
+/* These will do the right thing for either R,G,B or B,G,R color order,
+ * but you may not like the results for other color orders.
+ */
+#define HIST_C0_BITS  5                /* bits of precision in R/B histogram */
+#define HIST_C1_BITS  6                /* bits of precision in G histogram */
+#define HIST_C2_BITS  5                /* bits of precision in B/R histogram */
+
+/* Number of elements along histogram axes. */
+#define HIST_C0_ELEMS  (1<<HIST_C0_BITS)
+#define HIST_C1_ELEMS  (1<<HIST_C1_BITS)
+#define HIST_C2_ELEMS  (1<<HIST_C2_BITS)
+
+/* These are the amounts to shift an input value to get a histogram index. */
+#define C0_SHIFT  (BITS_IN_JSAMPLE-HIST_C0_BITS)
+#define C1_SHIFT  (BITS_IN_JSAMPLE-HIST_C1_BITS)
+#define C2_SHIFT  (BITS_IN_JSAMPLE-HIST_C2_BITS)
+
+
+typedef UINT16 histcell;       /* histogram cell; prefer an unsigned type */
+
+typedef histcell FAR * histptr;        /* for pointers to histogram cells */
+
+typedef histcell hist1d[HIST_C2_ELEMS]; /* typedefs for the array */
+typedef hist1d FAR * hist2d;   /* type for the 2nd-level pointers */
+typedef hist2d * hist3d;       /* type for top-level pointer */
+
+
+/* Declarations for Floyd-Steinberg dithering.
+ *
+ * Errors are accumulated into the array fserrors[], at a resolution of
+ * 1/16th of a pixel count.  The error at a given pixel is propagated
+ * to its not-yet-processed neighbors using the standard F-S fractions,
+ *             ...     (here)  7/16
+ *             3/16    5/16    1/16
+ * We work left-to-right on even rows, right-to-left on odd rows.
+ *
+ * We can get away with a single array (holding one row's worth of errors)
+ * by using it to store the current row's errors at pixel columns not yet
+ * processed, but the next row's errors at columns already processed.  We
+ * need only a few extra variables to hold the errors immediately around the
+ * current column.  (If we are lucky, those variables are in registers, but
+ * even if not, they're probably cheaper to access than array elements are.)
+ *
+ * The fserrors[] array has (#columns + 2) entries; the extra entry at
+ * each end saves us from special-casing the first and last pixels.
+ * Each entry is three values long, one value for each color component.
+ *
+ * Note: on a wide image, we might not have enough room in a PC's near data
+ * segment to hold the error array; so it is allocated with alloc_large.
+ */
+
+#if BITS_IN_JSAMPLE == 8
+typedef INT16 FSERROR;         /* 16 bits should be enough */
+typedef int LOCFSERROR;                /* use 'int' for calculation temps */
+#else
+typedef INT32 FSERROR;         /* may need more than 16 bits */
+typedef INT32 LOCFSERROR;      /* be sure calculation temps are big enough */
+#endif
+
+typedef FSERROR FAR *FSERRPTR; /* pointer to error array (in FAR storage!) */
+
+
+/* Private subobject */
+
+typedef struct {
+  struct jpeg_color_quantizer pub; /* public fields */
+
+  /* Space for the eventually created colormap is stashed here */
+  JSAMPROW sv_colormap[3];     /* colormap allocated at init time */
+  int desired;                 /* desired # of colors = size of colormap */
+
+  /* Variables for accumulating image statistics */
+  hist3d histogram;            /* pointer to the histogram */
+
+  boolean needs_zeroed;                /* TRUE if next pass must zero histogram */
+
+  /* Variables for Floyd-Steinberg dithering */
+  FSERRPTR fserrors;           /* accumulated errors */
+  boolean on_odd_row;          /* flag to remember which row we are on */
+  int * error_limiter;         /* table for clamping the applied error */
+} my_cquantizer;
+
+typedef my_cquantizer * my_cquantize_ptr;
+
+
+/*
+ * Prescan some rows of pixels.
+ * In this module the prescan simply updates the histogram, which has been
+ * initialized to zeroes by start_pass.
+ * An output_buf parameter is required by the method signature, but no data
+ * is actually output (in fact the buffer controller is probably passing a
+ * NULL pointer).
+ */
+
+METHODDEF(void)
+prescan_quantize (j_decompress_ptr cinfo, JSAMPARRAY input_buf,
+                 JSAMPARRAY output_buf, int num_rows)
+{
+  my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+  register JSAMPROW ptr;
+  register histptr histp;
+  register hist3d histogram = cquantize->histogram;
+  int row;
+  JDIMENSION col;
+  JDIMENSION width = cinfo->output_width;
+
+  for (row = 0; row < num_rows; row++) {
+    ptr = input_buf[row];
+    for (col = width; col > 0; col--) {
+      /* ignore transparent pixels */
+      if (!HAS_ALPHA || GETJSAMPLE(ptr[3]) != 0) {
+       /* get pixel value and index into the histogram */
+       histp = & histogram[GETJSAMPLE(ptr[0]) >> C0_SHIFT]
+                          [GETJSAMPLE(ptr[1]) >> C1_SHIFT]
+                          [GETJSAMPLE(ptr[2]) >> C2_SHIFT];
+       /* increment, check for overflow and undo increment if so. */
+       if (++(*histp) <= 0)
+         (*histp)--;
+      }
+      ptr += 3+HAS_ALPHA;
+    }
+  }
+}
+
+
+/*
+ * Next we have the really interesting routines: selection of a colormap
+ * given the completed histogram.
+ * These routines work with a list of "boxes", each representing a rectangular
+ * subset of the input color space (to histogram precision).
+ */
+
+typedef struct {
+  /* The bounds of the box (inclusive); expressed as histogram indexes */
+  int c0min, c0max;
+  int c1min, c1max;
+  int c2min, c2max;
+  /* The volume (actually 2-norm) of the box */
+  INT32 volume;
+  /* The number of nonzero histogram cells within this box */
+  long colorcount;
+} box;
+
+typedef box * boxptr;
+
+
+LOCAL(boxptr)
+find_biggest_color_pop (boxptr boxlist, int numboxes)
+/* Find the splittable box with the largest color population */
+/* Returns NULL if no splittable boxes remain */
+{
+  register boxptr boxp;
+  register int i;
+  register long maxc = 0;
+  boxptr which = NULL;
+  
+  for (i = 0, boxp = boxlist; i < numboxes; i++, boxp++) {
+    if (boxp->colorcount > maxc && boxp->volume > 0) {
+      which = boxp;
+      maxc = boxp->colorcount;
+    }
+  }
+  return which;
+}
+
+
+LOCAL(boxptr)
+find_biggest_volume (boxptr boxlist, int numboxes)
+/* Find the splittable box with the largest (scaled) volume */
+/* Returns NULL if no splittable boxes remain */
+{
+  register boxptr boxp;
+  register int i;
+  register INT32 maxv = 0;
+  boxptr which = NULL;
+  
+  for (i = 0, boxp = boxlist; i < numboxes; i++, boxp++) {
+    if (boxp->volume > maxv) {
+      which = boxp;
+      maxv = boxp->volume;
+    }
+  }
+  return which;
+}
+
+
+LOCAL(void)
+update_box (j_decompress_ptr cinfo, boxptr boxp)
+/* Shrink the min/max bounds of a box to enclose only nonzero elements, */
+/* and recompute its volume and population */
+{
+  my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+  hist3d histogram = cquantize->histogram;
+  histptr histp;
+  int c0,c1,c2;
+  int c0min,c0max,c1min,c1max,c2min,c2max;
+  INT32 dist0,dist1,dist2;
+  long ccount;
+  
+  c0min = boxp->c0min;  c0max = boxp->c0max;
+  c1min = boxp->c1min;  c1max = boxp->c1max;
+  c2min = boxp->c2min;  c2max = boxp->c2max;
+  
+  if (c0max > c0min)
+    for (c0 = c0min; c0 <= c0max; c0++)
+      for (c1 = c1min; c1 <= c1max; c1++) {
+       histp = & histogram[c0][c1][c2min];
+       for (c2 = c2min; c2 <= c2max; c2++)
+         if (*histp++ != 0) {
+           boxp->c0min = c0min = c0;
+           goto have_c0min;
+         }
+      }
+ have_c0min:
+  if (c0max > c0min)
+    for (c0 = c0max; c0 >= c0min; c0--)
+      for (c1 = c1min; c1 <= c1max; c1++) {
+       histp = & histogram[c0][c1][c2min];
+       for (c2 = c2min; c2 <= c2max; c2++)
+         if (*histp++ != 0) {
+           boxp->c0max = c0max = c0;
+           goto have_c0max;
+         }
+      }
+ have_c0max:
+  if (c1max > c1min)
+    for (c1 = c1min; c1 <= c1max; c1++)
+      for (c0 = c0min; c0 <= c0max; c0++) {
+       histp = & histogram[c0][c1][c2min];
+       for (c2 = c2min; c2 <= c2max; c2++)
+         if (*histp++ != 0) {
+           boxp->c1min = c1min = c1;
+           goto have_c1min;
+         }
+      }
+ have_c1min:
+  if (c1max > c1min)
+    for (c1 = c1max; c1 >= c1min; c1--)
+      for (c0 = c0min; c0 <= c0max; c0++) {
+       histp = & histogram[c0][c1][c2min];
+       for (c2 = c2min; c2 <= c2max; c2++)
+         if (*histp++ != 0) {
+           boxp->c1max = c1max = c1;
+           goto have_c1max;
+         }
+      }
+ have_c1max:
+  if (c2max > c2min)
+    for (c2 = c2min; c2 <= c2max; c2++)
+      for (c0 = c0min; c0 <= c0max; c0++) {
+       histp = & histogram[c0][c1min][c2];
+       for (c1 = c1min; c1 <= c1max; c1++, histp += HIST_C2_ELEMS)
+         if (*histp != 0) {
+           boxp->c2min = c2min = c2;
+           goto have_c2min;
+         }
+      }
+ have_c2min:
+  if (c2max > c2min)
+    for (c2 = c2max; c2 >= c2min; c2--)
+      for (c0 = c0min; c0 <= c0max; c0++) {
+       histp = & histogram[c0][c1min][c2];
+       for (c1 = c1min; c1 <= c1max; c1++, histp += HIST_C2_ELEMS)
+         if (*histp != 0) {
+           boxp->c2max = c2max = c2;
+           goto have_c2max;
+         }
+      }
+ have_c2max:
+
+  /* Update box volume.
+   * We use 2-norm rather than real volume here; this biases the method
+   * against making long narrow boxes, and it has the side benefit that
+   * a box is splittable iff norm > 0.
+   * Since the differences are expressed in histogram-cell units,
+   * we have to shift back to JSAMPLE units to get consistent distances;
+   * after which, we scale according to the selected distance scale factors.
+   */
+  dist0 = ((c0max - c0min) << C0_SHIFT) * C0_SCALE;
+  dist1 = ((c1max - c1min) << C1_SHIFT) * C1_SCALE;
+  dist2 = ((c2max - c2min) << C2_SHIFT) * C2_SCALE;
+  boxp->volume = dist0*dist0 + dist1*dist1 + dist2*dist2;
+  
+  /* Now scan remaining volume of box and compute population */
+  ccount = 0;
+  for (c0 = c0min; c0 <= c0max; c0++)
+    for (c1 = c1min; c1 <= c1max; c1++) {
+      histp = & histogram[c0][c1][c2min];
+      for (c2 = c2min; c2 <= c2max; c2++, histp++)
+       if (*histp != 0) {
+         ccount++;
+       }
+    }
+  boxp->colorcount = ccount;
+}
+
+
+LOCAL(int)
+median_cut (j_decompress_ptr cinfo, boxptr boxlist, int numboxes,
+           int desired_colors)
+/* Repeatedly select and split the largest box until we have enough boxes */
+{
+  int n,lb;
+  int c0,c1,c2,cmax;
+  register boxptr b1,b2;
+
+  while (numboxes < desired_colors) {
+    /* Select box to split.
+     * Current algorithm: by population for first half, then by volume.
+     */
+    if (numboxes*2 <= desired_colors) {
+      b1 = find_biggest_color_pop(boxlist, numboxes);
+    } else {
+      b1 = find_biggest_volume(boxlist, numboxes);
+    }
+    if (b1 == NULL)            /* no splittable boxes left! */
+      break;
+    b2 = &boxlist[numboxes];   /* where new box will go */
+    /* Copy the color bounds to the new box. */
+    b2->c0max = b1->c0max; b2->c1max = b1->c1max; b2->c2max = b1->c2max;
+    b2->c0min = b1->c0min; b2->c1min = b1->c1min; b2->c2min = b1->c2min;
+    /* Choose which axis to split the box on.
+     * Current algorithm: longest scaled axis.
+     * See notes in update_box about scaling distances.
+     */
+    c0 = ((b1->c0max - b1->c0min) << C0_SHIFT) * C0_SCALE;
+    c1 = ((b1->c1max - b1->c1min) << C1_SHIFT) * C1_SCALE;
+    c2 = ((b1->c2max - b1->c2min) << C2_SHIFT) * C2_SCALE;
+    /* We want to break any ties in favor of green, then red, blue last.
+     * This code does the right thing for R,G,B or B,G,R color orders only.
+     */
+#if RGB_RED == 0
+    cmax = c1; n = 1;
+    if (c0 > cmax) { cmax = c0; n = 0; }
+    if (c2 > cmax) { n = 2; }
+#else
+    cmax = c1; n = 1;
+    if (c2 > cmax) { cmax = c2; n = 2; }
+    if (c0 > cmax) { n = 0; }
+#endif
+    /* Choose split point along selected axis, and update box bounds.
+     * Current algorithm: split at halfway point.
+     * (Since the box has been shrunk to minimum volume,
+     * any split will produce two nonempty subboxes.)
+     * Note that lb value is max for lower box, so must be < old max.
+     */
+    switch (n) {
+    case 0:
+      lb = (b1->c0max + b1->c0min) / 2;
+      b1->c0max = lb;
+      b2->c0min = lb+1;
+      break;
+    case 1:
+      lb = (b1->c1max + b1->c1min) / 2;
+      b1->c1max = lb;
+      b2->c1min = lb+1;
+      break;
+    case 2:
+      lb = (b1->c2max + b1->c2min) / 2;
+      b1->c2max = lb;
+      b2->c2min = lb+1;
+      break;
+    }
+    /* Update stats for boxes */
+    update_box(cinfo, b1);
+    update_box(cinfo, b2);
+    numboxes++;
+  }
+  return numboxes;
+}
+
+
+LOCAL(void)
+compute_color (j_decompress_ptr cinfo, boxptr boxp, int icolor)
+/* Compute representative color for a box, put it in colormap[icolor] */
+{
+  /* Current algorithm: mean weighted by pixels (not colors) */
+  /* Note it is important to get the rounding correct! */
+  my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+  hist3d histogram = cquantize->histogram;
+  histptr histp;
+  int c0,c1,c2;
+  int c0min,c0max,c1min,c1max,c2min,c2max;
+  long count;
+  long total = 0;
+  long c0total = 0;
+  long c1total = 0;
+  long c2total = 0;
+  
+  c0min = boxp->c0min;  c0max = boxp->c0max;
+  c1min = boxp->c1min;  c1max = boxp->c1max;
+  c2min = boxp->c2min;  c2max = boxp->c2max;
+  
+  for (c0 = c0min; c0 <= c0max; c0++)
+    for (c1 = c1min; c1 <= c1max; c1++) {
+      histp = & histogram[c0][c1][c2min];
+      for (c2 = c2min; c2 <= c2max; c2++) {
+       if ((count = *histp++) != 0) {
+         total += count;
+         c0total += ((c0 << C0_SHIFT) + ((1<<C0_SHIFT)>>1)) * count;
+         c1total += ((c1 << C1_SHIFT) + ((1<<C1_SHIFT)>>1)) * count;
+         c2total += ((c2 << C2_SHIFT) + ((1<<C2_SHIFT)>>1)) * count;
+       }
+      }
+    }
+
+  if (total) {
+    cinfo->colormap[0][icolor] = (JSAMPLE) ((c0total + (total>>1)) / total);
+    cinfo->colormap[1][icolor] = (JSAMPLE) ((c1total + (total>>1)) / total);
+    cinfo->colormap[2][icolor] = (JSAMPLE) ((c2total + (total>>1)) / total);
+  } else {
+    cinfo->colormap[0][icolor] = 0;
+    cinfo->colormap[1][icolor] = 0;
+    cinfo->colormap[2][icolor] = 0;
+  }
+}
+
+
+LOCAL(void)
+select_colors (j_decompress_ptr cinfo, int desired_colors)
+/* Master routine for color selection */
+{
+  boxptr boxlist;
+  int numboxes;
+  int i;
+
+  /* Allocate workspace for box list */
+  boxlist = (boxptr) malloc(desired_colors * SIZEOF(box));
+  /* Initialize one box containing whole space */
+  numboxes = 1;
+  boxlist[0].c0min = 0;
+  boxlist[0].c0max = MAXJSAMPLE >> C0_SHIFT;
+  boxlist[0].c1min = 0;
+  boxlist[0].c1max = MAXJSAMPLE >> C1_SHIFT;
+  boxlist[0].c2min = 0;
+  boxlist[0].c2max = MAXJSAMPLE >> C2_SHIFT;
+  /* Shrink it to actually-used volume and set its statistics */
+  update_box(cinfo, & boxlist[0]);
+  /* Perform median-cut to produce final box list */
+  numboxes = median_cut(cinfo, boxlist, numboxes, desired_colors);
+  /* Compute the representative color for each box, fill colormap */
+  for (i = 0; i < numboxes; i++)
+    compute_color(cinfo, & boxlist[i], i);
+  cinfo->actual_number_of_colors = numboxes;
+  TRACEMS1(cinfo, 1, JTRC_QUANT_SELECTED, numboxes);
+}
+
+
+/*
+ * These routines are concerned with the time-critical task of mapping input
+ * colors to the nearest color in the selected colormap.
+ *
+ * We re-use the histogram space as an "inverse color map", essentially a
+ * cache for the results of nearest-color searches.  All colors within a
+ * histogram cell will be mapped to the same colormap entry, namely the one
+ * closest to the cell's center.  This may not be quite the closest entry to
+ * the actual input color, but it's almost as good.  A zero in the cache
+ * indicates we haven't found the nearest color for that cell yet; the array
+ * is cleared to zeroes before starting the mapping pass.  When we find the
+ * nearest color for a cell, its colormap index plus one is recorded in the
+ * cache for future use.  The pass2 scanning routines call fill_inverse_cmap
+ * when they need to use an unfilled entry in the cache.
+ *
+ * Our method of efficiently finding nearest colors is based on the "locally
+ * sorted search" idea described by Heckbert and on the incremental distance
+ * calculation described by Spencer W. Thomas in chapter III.1 of Graphics
+ * Gems II (James Arvo, ed.  Academic Press, 1991).  Thomas points out that
+ * the distances from a given colormap entry to each cell of the histogram can
+ * be computed quickly using an incremental method: the differences between
+ * distances to adjacent cells themselves differ by a constant.  This allows a
+ * fairly fast implementation of the "brute force" approach of computing the
+ * distance from every colormap entry to every histogram cell.  Unfortunately,
+ * it needs a work array to hold the best-distance-so-far for each histogram
+ * cell (because the inner loop has to be over cells, not colormap entries).
+ * The work array elements have to be INT32s, so the work array would need
+ * 256Kb at our recommended precision.  This is not feasible in DOS machines.
+ *
+ * To get around these problems, we apply Thomas' method to compute the
+ * nearest colors for only the cells within a small subbox of the histogram.
+ * The work array need be only as big as the subbox, so the memory usage
+ * problem is solved.  Furthermore, we need not fill subboxes that are never
+ * referenced in pass2; many images use only part of the color gamut, so a
+ * fair amount of work is saved.  An additional advantage of this
+ * approach is that we can apply Heckbert's locality criterion to quickly
+ * eliminate colormap entries that are far away from the subbox; typically
+ * three-fourths of the colormap entries are rejected by Heckbert's criterion,
+ * and we need not compute their distances to individual cells in the subbox.
+ * The speed of this approach is heavily influenced by the subbox size: too
+ * small means too much overhead, too big loses because Heckbert's criterion
+ * can't eliminate as many colormap entries.  Empirically the best subbox
+ * size seems to be about 1/512th of the histogram (1/8th in each direction).
+ *
+ * Thomas' article also describes a refined method which is asymptotically
+ * faster than the brute-force method, but it is also far more complex and
+ * cannot efficiently be applied to small subboxes.  It is therefore not
+ * useful for programs intended to be portable to DOS machines.  On machines
+ * with plenty of memory, filling the whole histogram in one shot with Thomas'
+ * refined method might be faster than the present code --- but then again,
+ * it might not be any faster, and it's certainly more complicated.
+ */
+
+
+/* log2(histogram cells in update box) for each axis; this can be adjusted */
+#define BOX_C0_LOG  (HIST_C0_BITS-3)
+#define BOX_C1_LOG  (HIST_C1_BITS-3)
+#define BOX_C2_LOG  (HIST_C2_BITS-3)
+
+#define BOX_C0_ELEMS  (1<<BOX_C0_LOG) /* # of hist cells in update box */
+#define BOX_C1_ELEMS  (1<<BOX_C1_LOG)
+#define BOX_C2_ELEMS  (1<<BOX_C2_LOG)
+
+#define BOX_C0_SHIFT  (C0_SHIFT + BOX_C0_LOG)
+#define BOX_C1_SHIFT  (C1_SHIFT + BOX_C1_LOG)
+#define BOX_C2_SHIFT  (C2_SHIFT + BOX_C2_LOG)
+
+
+/*
+ * The next three routines implement inverse colormap filling.  They could
+ * all be folded into one big routine, but splitting them up this way saves
+ * some stack space (the mindist[] and bestdist[] arrays need not coexist)
+ * and may allow some compilers to produce better code by registerizing more
+ * inner-loop variables.
+ */
+
+LOCAL(int)
+find_nearby_colors (j_decompress_ptr cinfo, int minc0, int minc1, int minc2,
+                   JSAMPLE colorlist[])
+/* Locate the colormap entries close enough to an update box to be candidates
+ * for the nearest entry to some cell(s) in the update box.  The update box
+ * is specified by the center coordinates of its first cell.  The number of
+ * candidate colormap entries is returned, and their colormap indexes are
+ * placed in colorlist[].
+ * This routine uses Heckbert's "locally sorted search" criterion to select
+ * the colors that need further consideration.
+ */
+{
+  int numcolors = cinfo->actual_number_of_colors;
+  int maxc0, maxc1, maxc2;
+  int centerc0, centerc1, centerc2;
+  int i, x, ncolors;
+  INT32 minmaxdist, min_dist, max_dist, tdist;
+  INT32 mindist[MAXNUMCOLORS]; /* min distance to colormap entry i */
+
+  /* Compute true coordinates of update box's upper corner and center.
+   * Actually we compute the coordinates of the center of the upper-corner
+   * histogram cell, which are the upper bounds of the volume we care about.
+   * Note that since ">>" rounds down, the "center" values may be closer to
+   * min than to max; hence comparisons to them must be "<=", not "<".
+   */
+  maxc0 = minc0 + ((1 << BOX_C0_SHIFT) - (1 << C0_SHIFT));
+  centerc0 = (minc0 + maxc0) >> 1;
+  maxc1 = minc1 + ((1 << BOX_C1_SHIFT) - (1 << C1_SHIFT));
+  centerc1 = (minc1 + maxc1) >> 1;
+  maxc2 = minc2 + ((1 << BOX_C2_SHIFT) - (1 << C2_SHIFT));
+  centerc2 = (minc2 + maxc2) >> 1;
+
+  /* For each color in colormap, find:
+   *  1. its minimum squared-distance to any point in the update box
+   *     (zero if color is within update box);
+   *  2. its maximum squared-distance to any point in the update box.
+   * Both of these can be found by considering only the corners of the box.
+   * We save the minimum distance for each color in mindist[];
+   * only the smallest maximum distance is of interest.
+   */
+  minmaxdist = 0x7FFFFFFFL;
+
+  for (i = 0; i < numcolors; i++) {
+    /* We compute the squared-c0-distance term, then add in the other two. */
+    x = GETJSAMPLE(cinfo->colormap[0][i]);
+    if (x < minc0) {
+      tdist = (x - minc0) * C0_SCALE;
+      min_dist = tdist*tdist;
+      tdist = (x - maxc0) * C0_SCALE;
+      max_dist = tdist*tdist;
+    } else if (x > maxc0) {
+      tdist = (x - maxc0) * C0_SCALE;
+      min_dist = tdist*tdist;
+      tdist = (x - minc0) * C0_SCALE;
+      max_dist = tdist*tdist;
+    } else {
+      /* within cell range so no contribution to min_dist */
+      min_dist = 0;
+      if (x <= centerc0) {
+       tdist = (x - maxc0) * C0_SCALE;
+       max_dist = tdist*tdist;
+      } else {
+       tdist = (x - minc0) * C0_SCALE;
+       max_dist = tdist*tdist;
+      }
+    }
+
+    x = GETJSAMPLE(cinfo->colormap[1][i]);
+    if (x < minc1) {
+      tdist = (x - minc1) * C1_SCALE;
+      min_dist += tdist*tdist;
+      tdist = (x - maxc1) * C1_SCALE;
+      max_dist += tdist*tdist;
+    } else if (x > maxc1) {
+      tdist = (x - maxc1) * C1_SCALE;
+      min_dist += tdist*tdist;
+      tdist = (x - minc1) * C1_SCALE;
+      max_dist += tdist*tdist;
+    } else {
+      /* within cell range so no contribution to min_dist */
+      if (x <= centerc1) {
+       tdist = (x - maxc1) * C1_SCALE;
+       max_dist += tdist*tdist;
+      } else {
+       tdist = (x - minc1) * C1_SCALE;
+       max_dist += tdist*tdist;
+      }
+    }
+
+    x = GETJSAMPLE(cinfo->colormap[2][i]);
+    if (x < minc2) {
+      tdist = (x - minc2) * C2_SCALE;
+      min_dist += tdist*tdist;
+      tdist = (x - maxc2) * C2_SCALE;
+      max_dist += tdist*tdist;
+    } else if (x > maxc2) {
+      tdist = (x - maxc2) * C2_SCALE;
+      min_dist += tdist*tdist;
+      tdist = (x - minc2) * C2_SCALE;
+      max_dist += tdist*tdist;
+    } else {
+      /* within cell range so no contribution to min_dist */
+      if (x <= centerc2) {
+       tdist = (x - maxc2) * C2_SCALE;
+       max_dist += tdist*tdist;
+      } else {
+       tdist = (x - minc2) * C2_SCALE;
+       max_dist += tdist*tdist;
+      }
+    }
+
+    mindist[i] = min_dist;     /* save away the results */
+    if (max_dist < minmaxdist)
+      minmaxdist = max_dist;
+  }
+
+  /* Now we know that no cell in the update box is more than minmaxdist
+   * away from some colormap entry.  Therefore, only colors that are
+   * within minmaxdist of some part of the box need be considered.
+   */
+  ncolors = 0;
+  for (i = 0; i < numcolors; i++) {
+    if (mindist[i] <= minmaxdist)
+      colorlist[ncolors++] = (JSAMPLE) i;
+  }
+  return ncolors;
+}
+
+
+LOCAL(void)
+find_best_colors (j_decompress_ptr cinfo, int minc0, int minc1, int minc2,
+                 int numcolors, JSAMPLE colorlist[], JSAMPLE bestcolor[])
+/* Find the closest colormap entry for each cell in the update box,
+ * given the list of candidate colors prepared by find_nearby_colors.
+ * Return the indexes of the closest entries in the bestcolor[] array.
+ * This routine uses Thomas' incremental distance calculation method to
+ * find the distance from a colormap entry to successive cells in the box.
+ */
+{
+  int ic0, ic1, ic2;
+  int i, icolor;
+  register INT32 * bptr;       /* pointer into bestdist[] array */
+  JSAMPLE * cptr;              /* pointer into bestcolor[] array */
+  INT32 dist0, dist1;          /* initial distance values */
+  register INT32 dist2;                /* current distance in inner loop */
+  INT32 xx0, xx1;              /* distance increments */
+  register INT32 xx2;
+  INT32 inc0, inc1, inc2;      /* initial values for increments */
+  /* This array holds the distance to the nearest-so-far color for each cell */
+  INT32 bestdist[BOX_C0_ELEMS * BOX_C1_ELEMS * BOX_C2_ELEMS];
+
+  /* Initialize best-distance for each cell of the update box */
+  bptr = bestdist;
+  for (i = BOX_C0_ELEMS*BOX_C1_ELEMS*BOX_C2_ELEMS-1; i >= 0; i--)
+    *bptr++ = 0x7FFFFFFFL;
+  
+  /* For each color selected by find_nearby_colors,
+   * compute its distance to the center of each cell in the box.
+   * If that's less than best-so-far, update best distance and color number.
+   */
+  
+  /* Nominal steps between cell centers ("x" in Thomas article) */
+#define STEP_C0  ((1 << C0_SHIFT) * C0_SCALE)
+#define STEP_C1  ((1 << C1_SHIFT) * C1_SCALE)
+#define STEP_C2  ((1 << C2_SHIFT) * C2_SCALE)
+  
+  for (i = 0; i < numcolors; i++) {
+    icolor = GETJSAMPLE(colorlist[i]);
+    /* Compute (square of) distance from minc0/c1/c2 to this color */
+    inc0 = (minc0 - GETJSAMPLE(cinfo->colormap[0][icolor])) * C0_SCALE;
+    dist0 = inc0*inc0;
+    inc1 = (minc1 - GETJSAMPLE(cinfo->colormap[1][icolor])) * C1_SCALE;
+    dist0 += inc1*inc1;
+    inc2 = (minc2 - GETJSAMPLE(cinfo->colormap[2][icolor])) * C2_SCALE;
+    dist0 += inc2*inc2;
+    /* Form the initial difference increments */
+    inc0 = inc0 * (2 * STEP_C0) + STEP_C0 * STEP_C0;
+    inc1 = inc1 * (2 * STEP_C1) + STEP_C1 * STEP_C1;
+    inc2 = inc2 * (2 * STEP_C2) + STEP_C2 * STEP_C2;
+    /* Now loop over all cells in box, updating distance per Thomas method */
+    bptr = bestdist;
+    cptr = bestcolor;
+    xx0 = inc0;
+    for (ic0 = BOX_C0_ELEMS-1; ic0 >= 0; ic0--) {
+      dist1 = dist0;
+      xx1 = inc1;
+      for (ic1 = BOX_C1_ELEMS-1; ic1 >= 0; ic1--) {
+       dist2 = dist1;
+       xx2 = inc2;
+       for (ic2 = BOX_C2_ELEMS-1; ic2 >= 0; ic2--) {
+         if (dist2 < *bptr) {
+           *bptr = dist2;
+           *cptr = (JSAMPLE) icolor;
+         }
+         dist2 += xx2;
+         xx2 += 2 * STEP_C2 * STEP_C2;
+         bptr++;
+         cptr++;
+       }
+       dist1 += xx1;
+       xx1 += 2 * STEP_C1 * STEP_C1;
+      }
+      dist0 += xx0;
+      xx0 += 2 * STEP_C0 * STEP_C0;
+    }
+  }
+}
+
+
+LOCAL(void)
+fill_inverse_cmap (j_decompress_ptr cinfo, int c0, int c1, int c2)
+/* Fill the inverse-colormap entries in the update box that contains */
+/* histogram cell c0/c1/c2.  (Only that one cell MUST be filled, but */
+/* we can fill as many others as we wish.) */
+{
+  my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+  hist3d histogram = cquantize->histogram;
+  int minc0, minc1, minc2;     /* lower left corner of update box */
+  int ic0, ic1, ic2;
+  register JSAMPLE * cptr;     /* pointer into bestcolor[] array */
+  register histptr cachep;     /* pointer into main cache array */
+  /* This array lists the candidate colormap indexes. */
+  JSAMPLE colorlist[MAXNUMCOLORS];
+  int numcolors;               /* number of candidate colors */
+  /* This array holds the actually closest colormap index for each cell. */
+  JSAMPLE bestcolor[BOX_C0_ELEMS * BOX_C1_ELEMS * BOX_C2_ELEMS];
+
+  /* Convert cell coordinates to update box ID */
+  c0 >>= BOX_C0_LOG;
+  c1 >>= BOX_C1_LOG;
+  c2 >>= BOX_C2_LOG;
+
+  /* Compute true coordinates of update box's origin corner.
+   * Actually we compute the coordinates of the center of the corner
+   * histogram cell, which are the lower bounds of the volume we care about.
+   */
+  minc0 = (c0 << BOX_C0_SHIFT) + ((1 << C0_SHIFT) >> 1);
+  minc1 = (c1 << BOX_C1_SHIFT) + ((1 << C1_SHIFT) >> 1);
+  minc2 = (c2 << BOX_C2_SHIFT) + ((1 << C2_SHIFT) >> 1);
+  
+  /* Determine which colormap entries are close enough to be candidates
+   * for the nearest entry to some cell in the update box.
+   */
+  numcolors = find_nearby_colors(cinfo, minc0, minc1, minc2, colorlist);
+
+  /* Determine the actually nearest colors. */
+  find_best_colors(cinfo, minc0, minc1, minc2, numcolors, colorlist,
+                  bestcolor);
+
+  /* Save the best color numbers (plus 1) in the main cache array */
+  c0 <<= BOX_C0_LOG;           /* convert ID back to base cell indexes */
+  c1 <<= BOX_C1_LOG;
+  c2 <<= BOX_C2_LOG;
+  cptr = bestcolor;
+  for (ic0 = 0; ic0 < BOX_C0_ELEMS; ic0++) {
+    for (ic1 = 0; ic1 < BOX_C1_ELEMS; ic1++) {
+      cachep = & histogram[c0+ic0][c1+ic1][c2];
+      for (ic2 = 0; ic2 < BOX_C2_ELEMS; ic2++) {
+       *cachep++ = (histcell) (GETJSAMPLE(*cptr++) + 1);
+      }
+    }
+  }
+}
+
+
+/*
+ * Map some rows of pixels to the output colormapped representation.
+ */
+
+METHODDEF(void)
+pass2_no_dither (j_decompress_ptr cinfo,
+                JSAMPARRAY input_buf, JSAMPARRAY output_buf, int num_rows)
+/* This version performs no dithering */
+{
+  my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+  hist3d histogram = cquantize->histogram;
+  register JSAMPROW inptr, outptr;
+  register histptr cachep;
+  register int c0, c1, c2;
+  int row;
+  JDIMENSION col;
+  JDIMENSION width = cinfo->output_width;
+
+  for (row = 0; row < num_rows; row++) {
+    inptr = input_buf[row];
+    outptr = output_buf[row];
+    for (col = width; col > 0; col--) {
+      /* get pixel value and index into the cache */
+      c0 = GETJSAMPLE(*inptr++) >> C0_SHIFT;
+      c1 = GETJSAMPLE(*inptr++) >> C1_SHIFT;
+      c2 = GETJSAMPLE(*inptr++) >> C2_SHIFT;
+      if (HAS_ALPHA && *inptr++ == 0) {
+       *outptr++ = 0;
+      } else {
+       cachep = & histogram[c0][c1][c2];
+       /* If we have not seen this color before, find nearest colormap entry */
+       /* and update the cache */
+       if (*cachep == 0)
+         fill_inverse_cmap(cinfo, c0,c1,c2);
+       /* Now emit the colormap index for this cell */
+       *outptr++ = (JSAMPLE) (*cachep - 1 + HAS_ALPHA);
+      }
+    }
+  }
+}
+
+
+METHODDEF(void)
+pass2_fs_dither (j_decompress_ptr cinfo,
+                JSAMPARRAY input_buf, JSAMPARRAY output_buf, int num_rows)
+/* This version performs Floyd-Steinberg dithering */
+{
+  my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+  hist3d histogram = cquantize->histogram;
+  register LOCFSERROR cur0, cur1, cur2;        /* current error or pixel value */
+  LOCFSERROR belowerr0, belowerr1, belowerr2; /* error for pixel below cur */
+  LOCFSERROR bpreverr0, bpreverr1, bpreverr2; /* error for below/prev col */
+  register FSERRPTR errorptr;  /* => fserrors[] at column before current */
+  JSAMPROW inptr;              /* => current input pixel */
+  JSAMPROW outptr;             /* => current output pixel */
+  histptr cachep;
+  int dir;                     /* +1 or -1 depending on direction */
+  int dir_comp;                        /* 3*dir or 4*dir, for advancing inptr */
+  int dir3;                    /* 3*dir, for advancing errorptr */
+  int row;
+  JDIMENSION col;
+  JDIMENSION width = cinfo->output_width;
+  JSAMPLE *range_limit = cinfo->sample_range_limit;
+  int *error_limit = cquantize->error_limiter;
+  JSAMPROW colormap0 = cinfo->colormap[0];
+  JSAMPROW colormap1 = cinfo->colormap[1];
+  JSAMPROW colormap2 = cinfo->colormap[2];
+  SHIFT_TEMPS
+
+  for (row = 0; row < num_rows; row++) {
+    inptr = input_buf[row];
+    outptr = output_buf[row];
+    if (cquantize->on_odd_row) {
+      /* work right to left in this row */
+      inptr += (width-1) * (3+HAS_ALPHA); /* so point to rightmost pixel */
+      outptr += width-1;
+      dir = -1;
+      dir3 = -3;
+      dir_comp = -3-HAS_ALPHA;
+      errorptr = cquantize->fserrors + (width+1)*3; /* => entry after last column */
+      cquantize->on_odd_row = FALSE; /* flip for next time */
+    } else {
+      /* work left to right in this row */
+      dir = 1;
+      dir3 = 3;
+      dir_comp = 3+HAS_ALPHA;
+      errorptr = cquantize->fserrors; /* => entry before first real column */
+      cquantize->on_odd_row = TRUE; /* flip for next time */
+    }
+    /* Preset error values: no error propagated to first pixel from left */
+    cur0 = cur1 = cur2 = 0;
+    /* and no error propagated to row below yet */
+    belowerr0 = belowerr1 = belowerr2 = 0;
+    bpreverr0 = bpreverr1 = bpreverr2 = 0;
+
+    for (col = width; col > 0; col--) {
+      if (HAS_ALPHA && inptr[3] == 0) {
+       /* Output transparent pixel and reset error values. */
+       *outptr = 0;
+       cur0 = cur1 = cur2 = 0;
+      } else {
+       /* curN holds the error propagated from the previous pixel on the
+        * current line.  Add the error propagated from the previous line
+        * to form the complete error correction term for this pixel, and
+        * round the error term (which is expressed * 16) to an integer.
+        * RIGHT_SHIFT rounds towards minus infinity, so adding 8 is correct
+        * for either sign of the error value.
+        * Note: errorptr points to *previous* column's array entry.
+        */
+       cur0 = RIGHT_SHIFT(cur0 + errorptr[dir_comp+0] + 8, 4);
+       cur1 = RIGHT_SHIFT(cur1 + errorptr[dir_comp+1] + 8, 4);
+       cur2 = RIGHT_SHIFT(cur2 + errorptr[dir_comp+2] + 8, 4);
+       /* Limit the error using transfer function set by init_error_limit.
+        * See comments with init_error_limit for rationale.
+        */
+       cur0 = error_limit[cur0];
+       cur1 = error_limit[cur1];
+       cur2 = error_limit[cur2];
+       /* Form pixel value + error, and range-limit to 0..MAXJSAMPLE.
+        * The maximum error is +- MAXJSAMPLE (or less with error limiting);
+        * this sets the required size of the range_limit array.
+        */
+       cur0 += GETJSAMPLE(inptr[0]);
+       cur1 += GETJSAMPLE(inptr[1]);
+       cur2 += GETJSAMPLE(inptr[2]);
+       cur0 = GETJSAMPLE(range_limit[cur0]);
+       cur1 = GETJSAMPLE(range_limit[cur1]);
+       cur2 = GETJSAMPLE(range_limit[cur2]);
+       /* Index into the cache with adjusted pixel value */
+       cachep = & histogram[cur0>>C0_SHIFT][cur1>>C1_SHIFT][cur2>>C2_SHIFT];
+       /* If we have not seen this color before, find nearest colormap */
+       /* entry and update the cache */
+       if (*cachep == 0)
+         fill_inverse_cmap(cinfo, cur0>>C0_SHIFT,cur1>>C1_SHIFT,cur2>>C2_SHIFT);
+       /* Now emit the colormap index for this cell */
+       { register int pixcode = *cachep - 1;
+         *outptr = (JSAMPLE) pixcode + HAS_ALPHA;
+         /* Compute representation error for this pixel */
+         cur0 -= GETJSAMPLE(colormap0[pixcode]);
+         cur1 -= GETJSAMPLE(colormap1[pixcode]);
+         cur2 -= GETJSAMPLE(colormap2[pixcode]);
+       }
+       /* Compute error fractions to be propagated to adjacent pixels.
+        * Add these into the running sums, and simultaneously shift the
+        * next-line error sums left by 1 column.
+        */
+       { register LOCFSERROR bnexterr, delta;
+
+         bnexterr = cur0;      /* Process component 0 */
+         delta = cur0 * 2;
+         cur0 += delta;                /* form error * 3 */
+         errorptr[0] = (FSERROR) (bpreverr0 + cur0);
+         cur0 += delta;                /* form error * 5 */
+         bpreverr0 = belowerr0 + cur0;
+         belowerr0 = bnexterr;
+         cur0 += delta;                /* form error * 7 */
+         bnexterr = cur1;      /* Process component 1 */
+         delta = cur1 * 2;
+         cur1 += delta;                /* form error * 3 */
+         errorptr[1] = (FSERROR) (bpreverr1 + cur1);
+         cur1 += delta;                /* form error * 5 */
+         bpreverr1 = belowerr1 + cur1;
+         belowerr1 = bnexterr;
+         cur1 += delta;                /* form error * 7 */
+         bnexterr = cur2;      /* Process component 2 */
+         delta = cur2 * 2;
+         cur2 += delta;                /* form error * 3 */
+         errorptr[2] = (FSERROR) (bpreverr2 + cur2);
+         cur2 += delta;                /* form error * 5 */
+         bpreverr2 = belowerr2 + cur2;
+         belowerr2 = bnexterr;
+         cur2 += delta;                /* form error * 7 */
+       }
+       /* At this point curN contains the 7/16 error value to be propagated
+        * to the next pixel on the current line, and all the errors for the
+        * next line have been shifted over.  We are therefore ready to move on.
+        */
+      }
+      inptr += dir_comp;       /* Advance pixel pointers to next column */
+      outptr += dir;
+      errorptr += dir3;                /* advance errorptr to current column */
+    }
+    /* Post-loop cleanup: we must unload the final error values into the
+     * final fserrors[] entry.  Note we need not unload belowerrN because
+     * it is for the dummy column before or after the actual array.
+     */
+    errorptr[0] = (FSERROR) bpreverr0; /* unload prev errs into array */
+    errorptr[1] = (FSERROR) bpreverr1;
+    errorptr[2] = (FSERROR) bpreverr2;
+  }
+}
+
+
+/*
+ * Initialize the error-limiting transfer function (lookup table).
+ * The raw F-S error computation can potentially compute error values of up to
+ * +- MAXJSAMPLE.  But we want the maximum correction applied to a pixel to be
+ * much less, otherwise obviously wrong pixels will be created.  (Typical
+ * effects include weird fringes at color-area boundaries, isolated bright
+ * pixels in a dark area, etc.)  The standard advice for avoiding this problem
+ * is to ensure that the "corners" of the color cube are allocated as output
+ * colors; then repeated errors in the same direction cannot cause cascading
+ * error buildup.  However, that only prevents the error from getting
+ * completely out of hand; Aaron Giles reports that error limiting improves
+ * the results even with corner colors allocated.
+ * A simple clamping of the error values to about +- MAXJSAMPLE/8 works pretty
+ * well, but the smoother transfer function used below is even better.  Thanks
+ * to Aaron Giles for this idea.
+ */
+
+LOCAL(void)
+init_error_limit (j_decompress_ptr cinfo)
+/* Allocate and fill in the error_limiter table */
+{
+  my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+  int * table;
+  int in, out;
+
+  table = (int *) malloc((MAXJSAMPLE*2+1) * SIZEOF(int));
+  table += MAXJSAMPLE;         /* so can index -MAXJSAMPLE .. +MAXJSAMPLE */
+  cquantize->error_limiter = table;
+
+#define STEPSIZE ((MAXJSAMPLE+1)/16)
+  /* Map errors 1:1 up to +- MAXJSAMPLE/16 */
+  out = 0;
+  for (in = 0; in < STEPSIZE; in++, out++) {
+    table[in] = out; table[-in] = -out;
+  }
+  /* Map errors 1:2 up to +- 3*MAXJSAMPLE/16 */
+  for (; in < STEPSIZE*3; in++, out += (in&1) ? 0 : 1) {
+    table[in] = out; table[-in] = -out;
+  }
+  /* Clamp the rest to final out value (which is (MAXJSAMPLE+1)/8) */
+  for (; in <= MAXJSAMPLE; in++) {
+    table[in] = out; table[-in] = -out;
+  }
+#undef STEPSIZE
+}
+
+
+/*
+ * Finish up at the end of each pass.
+ */
+
+METHODDEF(void)
+finish_pass1 (j_decompress_ptr cinfo)
+{
+  my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+
+  /* Select the representative colors and fill in cinfo->colormap */
+  cinfo->colormap = cquantize->sv_colormap;
+  select_colors(cinfo, cquantize->desired);
+  /* Force next pass to zero the color index table */
+  cquantize->needs_zeroed = TRUE;
+}
+
+
+METHODDEF(void)
+finish_pass2 (j_decompress_ptr cinfo)
+{
+  /* no work */
+}
+
+
+/*
+ * Initialize for each processing pass.
+ */
+
+METHODDEF(void)
+start_pass_2_quant (j_decompress_ptr cinfo, boolean is_pre_scan)
+{
+  my_cquantize_ptr cquantize = (my_cquantize_ptr) cinfo->cquantize;
+  hist3d histogram = cquantize->histogram;
+  int i;
+
+  if (is_pre_scan) {
+    /* Set up method pointers */
+    cquantize->pub.color_quantize = prescan_quantize;
+    cquantize->pub.finish_pass = finish_pass1;
+    cquantize->needs_zeroed = TRUE; /* Always zero histogram */
+  } else {
+    /* Set up method pointers */
+    if (cinfo->dither_mode == JDITHER_FS)
+      cquantize->pub.color_quantize = pass2_fs_dither;
+    else
+      cquantize->pub.color_quantize = pass2_no_dither;
+    cquantize->pub.finish_pass = finish_pass2;
+
+    /* Make sure color count is acceptable */
+    i = cinfo->actual_number_of_colors;
+    if (i < 1)
+      ERREXIT1(cinfo, JERR_QUANT_FEW_COLORS, 1);
+    if (i > MAXNUMCOLORS)
+      ERREXIT1(cinfo, JERR_QUANT_MANY_COLORS, MAXNUMCOLORS);
+
+    if (cinfo->dither_mode == JDITHER_FS) {
+      size_t arraysize = (size_t) ((cinfo->output_width + 2) *
+                                  (3 * SIZEOF(FSERROR)));
+      /* Allocate Floyd-Steinberg workspace if we didn't already. */
+      if (cquantize->fserrors == NULL)
+       cquantize->fserrors = (FSERRPTR) malloc(arraysize);
+      /* Initialize the propagated errors to zero. */
+      jzero_far((void FAR *) cquantize->fserrors, arraysize);
+      /* Make the error-limit table if we didn't already. */
+      if (cquantize->error_limiter == NULL)
+       init_error_limit(cinfo);
+      cquantize->on_odd_row = FALSE;
+    }
+
+  }
+  /* Zero the histogram or inverse color map, if necessary */
+  if (cquantize->needs_zeroed) {
+    for (i = 0; i < HIST_C0_ELEMS; i++) {
+      jzero_far((void FAR *) histogram[i],
+               HIST_C1_ELEMS*HIST_C2_ELEMS * SIZEOF(histcell));
+    }
+    cquantize->needs_zeroed = FALSE;
+  }
+}
+
+
+/*
+ * Several decompression processes need to range-limit values to the range
+ * 0..MAXJSAMPLE; the input value may fall somewhat outside this range
+ * due to noise introduced by quantization, roundoff error, etc.  These
+ * processes are inner loops and need to be as fast as possible.  On most
+ * machines, particularly CPUs with pipelines or instruction prefetch,
+ * a (subscript-check-less) C table lookup
+ *             x = sample_range_limit[x];
+ * is faster than explicit tests
+ *             if (x < 0)  x = 0;
+ *             else if (x > MAXJSAMPLE)  x = MAXJSAMPLE;
+ * These processes all use a common table prepared by the routine below.
+ *
+ * For most steps we can mathematically guarantee that the initial value
+ * of x is within MAXJSAMPLE+1 of the legal range, so a table running from
+ * -(MAXJSAMPLE+1) to 2*MAXJSAMPLE+1 is sufficient.  But for the initial
+ * limiting step (just after the IDCT), a wildly out-of-range value is 
+ * possible if the input data is corrupt.  To avoid any chance of indexing
+ * off the end of memory and getting a bad-pointer trap, we perform the
+ * post-IDCT limiting thus:
+ *             x = range_limit[x & MASK];
+ * where MASK is 2 bits wider than legal sample data, ie 10 bits for 8-bit
+ * samples.  Under normal circumstances this is more than enough range and
+ * a correct output will be generated; with bogus input data the mask will
+ * cause wraparound, and we will safely generate a bogus-but-in-range output.
+ * For the post-IDCT step, we want to convert the data from signed to unsigned
+ * representation by adding CENTERJSAMPLE at the same time that we limit it.
+ * So the post-IDCT limiting table ends up looking like this:
+ *   CENTERJSAMPLE,CENTERJSAMPLE+1,...,MAXJSAMPLE,
+ *   MAXJSAMPLE (repeat 2*(MAXJSAMPLE+1)-CENTERJSAMPLE times),
+ *   0          (repeat 2*(MAXJSAMPLE+1)-CENTERJSAMPLE times),
+ *   0,1,...,CENTERJSAMPLE-1
+ * Negative inputs select values from the upper half of the table after
+ * masking.
+ *
+ * We can save some space by overlapping the start of the post-IDCT table
+ * with the simpler range limiting table.  The post-IDCT table begins at
+ * sample_range_limit + CENTERJSAMPLE.
+ *
+ * Note that the table is allocated in near data space on PCs; it's small
+ * enough and used often enough to justify this.
+ */
+
+LOCAL(void)
+prepare_range_limit_table (j_decompress_ptr cinfo)
+/* Allocate and fill in the sample_range_limit table */
+{
+  JSAMPLE * table;
+  int i;
+
+  table = (JSAMPLE *) malloc(
+    (5 * (MAXJSAMPLE+1) + CENTERJSAMPLE) * SIZEOF(JSAMPLE));
+  table += (MAXJSAMPLE+1);     /* allow negative subscripts of simple table */
+  cinfo->sample_range_limit = table;
+  /* First segment of "simple" table: limit[x] = 0 for x < 0 */
+  MEMZERO(table - (MAXJSAMPLE+1), (MAXJSAMPLE+1) * SIZEOF(JSAMPLE));
+  /* Main part of "simple" table: limit[x] = x */
+  for (i = 0; i <= MAXJSAMPLE; i++)
+    table[i] = (JSAMPLE) i;
+  table += CENTERJSAMPLE;      /* Point to where post-IDCT table starts */
+  /* End of simple table, rest of first half of post-IDCT table */
+  for (i = CENTERJSAMPLE; i < 2*(MAXJSAMPLE+1); i++)
+    table[i] = MAXJSAMPLE;
+  /* Second half of post-IDCT table */
+  MEMZERO(table + (2 * (MAXJSAMPLE+1)),
+         (2 * (MAXJSAMPLE+1) - CENTERJSAMPLE) * SIZEOF(JSAMPLE));
+  MEMCOPY(table + (4 * (MAXJSAMPLE+1) - CENTERJSAMPLE),
+         cinfo->sample_range_limit, CENTERJSAMPLE * SIZEOF(JSAMPLE));
+}
+
+
+void quantize (JSAMPARRAY input_buf,
+              JSAMPARRAY output_buf,
+              int width, int height,
+              J_DITHER_MODE dither_mode,
+              int desired /*number_of_colors*/,
+              unsigned int * output_colors)
+{
+  struct jpeg_decompress_struct cinfo_buf = {}, * cinfo;
+  my_cquantizer cquantize_buf = {}, * cquantize;
+  int i;
+  int pass_flag;
+
+  cinfo = &cinfo_buf;
+  cinfo->dither_mode = dither_mode;
+  cinfo->output_width = width;
+  cinfo->output_height = height;
+  prepare_range_limit_table(cinfo);
+
+  cquantize = &cquantize_buf;
+  cinfo->cquantize = (struct jpeg_color_quantizer *) cquantize;
+  cquantize->pub.start_pass = start_pass_2_quant;
+  cquantize->fserrors = NULL;  /* flag optional arrays not allocated */
+  cquantize->error_limiter = NULL;
+
+  /* Allocate the histogram/inverse colormap storage */
+  cquantize->histogram = (hist3d) malloc(HIST_C0_ELEMS * SIZEOF(hist2d));
+  for (i = 0; i < HIST_C0_ELEMS; i++) {
+    cquantize->histogram[i] = (hist2d) malloc(
+       HIST_C1_ELEMS*HIST_C2_ELEMS * SIZEOF(histcell));
+  }
+
+  /* Allocate storage for the completed colormap, if required.
+   */
+  if (desired < 1+HAS_ALPHA)
+    ERREXIT1(cinfo, JERR_QUANT_FEW_COLORS, 1+HAS_ALPHA);
+  /* Make sure colormap indexes can be represented by JSAMPLEs */
+  if (desired > MAXNUMCOLORS)
+    ERREXIT1(cinfo, JERR_QUANT_MANY_COLORS, MAXNUMCOLORS);
+  desired -= HAS_ALPHA;
+  cquantize->sv_colormap[0] = malloc((JDIMENSION) desired * (JDIMENSION) 3);
+  cquantize->sv_colormap[1] = cquantize->sv_colormap[0] + (JDIMENSION) desired;
+  cquantize->sv_colormap[2] = cquantize->sv_colormap[1] + (JDIMENSION) desired;
+  cquantize->desired = desired;
+
+  for (pass_flag = 1; pass_flag >= 0; --pass_flag) {
+    start_pass_2_quant(cinfo, pass_flag);
+    cquantize->pub.color_quantize(cinfo, input_buf, output_buf, height);
+    cquantize->pub.finish_pass(cinfo);
+  }
+
+  if (HAS_ALPHA)
+    output_colors[0] = 0;
+  for (i = 0; i != desired; ++i) {
+    output_colors[HAS_ALPHA+i] = (0xFF000000
+                                 + (cquantize->sv_colormap[2][i] << 16)
+                                 + (cquantize->sv_colormap[1][i] << 8)
+                                 + cquantize->sv_colormap[0][i]);
+  }                              
+
+  if (cinfo->sample_range_limit)
+    free(cinfo->sample_range_limit - (MAXJSAMPLE+1));
+  free(cquantize->sv_colormap[0]);
+  if (cquantize->histogram) {
+    for (i = 0; i < HIST_C0_ELEMS; i++)
+      free(cquantize->histogram[i]);
+    free(cquantize->histogram);
+  }
+  free(cquantize->fserrors);
+  if (cquantize->error_limiter)
+    free(cquantize->error_limiter - MAXJSAMPLE);
+}
diff --git a/jquant2.h b/jquant2.h
new file mode 100644 (file)
index 0000000..6d01b84
--- /dev/null
+++ b/jquant2.h
@@ -0,0 +1,28 @@
+#ifndef INC_JQUANT2_H
+#define INC_JQUANT2_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef enum {
+       JDITHER_NONE,           /* no dithering */
+       JDITHER_FS              /* Floyd-Steinberg error diffusion dither */
+} J_DITHER_MODE;
+
+typedef unsigned char JSAMPLE;
+typedef JSAMPLE * JSAMPROW;
+typedef JSAMPROW * JSAMPARRAY;
+
+void quantize (JSAMPARRAY input_buf,
+              JSAMPARRAY output_buf,
+              int width, int height,
+              J_DITHER_MODE dither_mode,
+              int desired_number_of_colors,
+              unsigned int * output_colors);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* !INC_JQUANT2_H */
diff --git a/linkiterator.cpp b/linkiterator.cpp
new file mode 100644 (file)
index 0000000..b751c1d
--- /dev/null
@@ -0,0 +1,52 @@
+// Copyright 2005 Ben Hutchings <ben@decadentplace.org.uk>.
+// See the file "COPYING" for licence details.
+
+#include "linkiterator.hpp"
+
+#include <cassert>
+
+#include <nsIDOMHTMLCollection.h>
+#include <nsIDOMHTMLDocument.h>
+
+LinkIterator::LinkIterator()
+{}
+
+LinkIterator::LinkIterator(nsIDOMDocument * document)
+{
+    nsCOMPtr<nsIDOMHTMLDocument> htmlDoc(do_QueryInterface(document));
+    if (!htmlDoc)
+       return;
+
+    htmlDoc->GetLinks(getter_AddRefs(collection_));
+    assert(collection_);
+
+    index_ = 0;
+    length_ = 0;
+    collection_->GetLength(&length_);
+    if (length_ == 0)
+       collection_ = 0;
+}
+
+already_AddRefed<nsIDOMNode> LinkIterator::operator*() const
+{
+    assert(collection_);
+    nsIDOMNode * result = 0;
+    collection_->Item(index_, &result);
+    assert(result);
+    return dont_AddRef(result);
+}
+
+LinkIterator & LinkIterator::operator++()
+{
+    assert(collection_);
+    ++index_;
+    if (index_ == length_)
+       collection_ = 0;
+    return *this;
+}
+
+bool LinkIterator::operator==(const LinkIterator & other) const
+{
+    return (collection_ == other.collection_
+           && (!collection_ || index_ == other.index_));
+}
diff --git a/linkiterator.hpp b/linkiterator.hpp
new file mode 100644 (file)
index 0000000..2f93a2f
--- /dev/null
@@ -0,0 +1,36 @@
+// Copyright 2005 Ben Hutchings <ben@decadentplace.org.uk>.
+// See the file "COPYING" for licence details.
+
+#ifndef INC_LINKITERATOR_HPP
+#define INC_LINKITERATOR_HPP
+
+#include <iterator>
+
+#include <nsCOMPtr.h>
+#include <nsIDOMHTMLCollection.h>
+#include <nsIDOMNode.h>
+
+class nsIDOMDocument;
+
+class LinkIterator
+    : public std::iterator<std::input_iterator_tag, nsCOMPtr<nsIDOMNode>,
+                          void, void, void>
+{
+public:
+    LinkIterator();
+    explicit LinkIterator(nsIDOMDocument * document);
+
+    already_AddRefed<nsIDOMNode> operator*() const;
+    LinkIterator & operator++();
+    bool operator==(const LinkIterator &) const;
+    bool operator!=(const LinkIterator & other) const
+       {
+           return !(*this == other);
+       }
+
+private:
+    nsCOMPtr<nsIDOMHTMLCollection> collection_;
+    unsigned int index_, length_;
+};
+
+#endif // !INC_LINKITERATOR_HPP
diff --git a/pixbufs.cpp b/pixbufs.cpp
new file mode 100644 (file)
index 0000000..39a569c
--- /dev/null
@@ -0,0 +1,112 @@
+// Copyright 2005 Ben Hutchings <ben@decadentplace.org.uk>.
+// See the file "COPYING" for licence details.
+
+#include "pixbufs.hpp"
+
+#include <cassert>
+
+#include <gdkmm/pixbuf.h>
+
+#include "auto_array.hpp"
+#include "jquant2.h"
+
+// Find pixel differences between an "old" and "new" RGB Pixbuf
+// (or RGBA, but the alpha component will be ignored) and copy the
+// differing pixels from the new one to a third RGBA Pixbuf at the
+// specified offset with full opacity.
+// The width and height of the old and new Pixbufs must be equal
+// and match the specified dimensions.  The width and height of
+// the third Pixbuf must be large enough to store a rectangle of
+// those dimensions at the specified offset.
+void diff_rgb_pixbufs(Glib::RefPtr<Gdk::Pixbuf> old_buf,
+                     Glib::RefPtr<Gdk::Pixbuf> new_buf,
+                     Glib::RefPtr<Gdk::Pixbuf> diff_buf,
+                     int offset_x, int offset_y,
+                     int width, int height)
+{
+    assert(old_buf->get_colorspace() == Gdk::COLORSPACE_RGB);
+    assert(new_buf->get_colorspace() == Gdk::COLORSPACE_RGB);
+    assert(diff_buf->get_colorspace() == Gdk::COLORSPACE_RGB
+          && diff_buf->get_has_alpha());
+    int old_bpr = old_buf->get_rowstride();
+    int old_bpp = old_buf->get_n_channels();
+    assert(old_bpp >= 3);
+    assert(old_buf->get_width() == width);
+    assert(old_buf->get_height() == height);
+    int new_bpr = new_buf->get_rowstride();
+    int new_bpp = new_buf->get_n_channels();
+    assert(new_bpp >= 3);
+    assert(new_buf->get_width() == width);
+    assert(new_buf->get_height() == height);
+    int diff_bpr = diff_buf->get_rowstride();
+    int diff_bpp = diff_buf->get_n_channels();
+    assert(diff_bpp == 4);
+    assert(diff_buf->get_width() >= offset_x + width);
+    assert(diff_buf->get_height() >= offset_y + height);
+
+    const guint8 * old_p = old_buf->get_pixels();
+    const guint8 * new_p = new_buf->get_pixels();
+    guint8 * diff_p = (diff_buf->get_pixels()
+                      + diff_bpr * offset_y
+                      + diff_bpp * offset_x);
+
+    for (int y = 0; y != height; ++y)
+    {
+       for (int x = 0; x != width; ++x)
+       {
+           if (old_p[0] != new_p[0]
+               || old_p[1] != new_p[1]
+               || old_p[2] != new_p[2])
+           {
+               diff_p[0] = new_p[0];
+               diff_p[1] = new_p[1];
+               diff_p[2] = new_p[2];
+               diff_p[3] = 0xFF; // fully opaque
+           }
+           old_p += old_bpp;
+           new_p += new_bpp;
+           diff_p += diff_bpp;
+       }
+       old_p += old_bpr - old_bpp * width;
+       new_p += new_bpr - new_bpp * width;
+       diff_p += diff_bpr - diff_bpp * width;
+    }
+}
+
+// Quantise an RGBA Pixbuf to the specified number of colours, including
+// one transparent colour.  Currently uses Floyd-Steinberg dithering.
+void quantise_rgba_pixbuf(Glib::RefPtr<Gdk::Pixbuf> buf, int n_colours)
+{
+    assert(buf->get_colorspace() == Gdk::COLORSPACE_RGB
+          && buf->get_has_alpha());
+    int bpr = buf->get_rowstride();
+    assert(buf->get_n_channels() == 4);
+    int width = buf->get_width();
+    int height = buf->get_height();
+
+    unsigned char * buf_p = buf->get_pixels();
+    auto_array<unsigned char *> rows(new unsigned char *[height]);
+    for (int y = 0; y != height; ++y)
+       rows[y] = &buf_p[y * bpr];
+    auto_array<unsigned char> quant_buf_p(
+       new unsigned char[width * height]);
+    auto_array<unsigned char *> quant_rows(
+       new unsigned char *[height]);
+    for (int y = 0; y != height; ++y)
+       quant_rows[y] = &quant_buf_p[y * width];
+    auto_array<unsigned int> colours(new unsigned int[n_colours]);
+
+    quantize(rows.get(), quant_rows.get(), width, height,
+            JDITHER_FS, n_colours, colours.get());
+
+    // Pixbuf doesn't support quantised images, so convert back to RGBA.
+    for (int y = 0; y != height; ++y)
+       for (int x = 0; x != width; ++x)
+       {
+           unsigned int colour = colours[quant_rows[y][x]];
+           rows[y][4*x]   = colour;
+           rows[y][4*x+1] = colour >> 8;
+           rows[y][4*x+2] = colour >> 16;
+           rows[y][4*x+3] = colour >> 24;
+       }
+}
diff --git a/pixbufs.hpp b/pixbufs.hpp
new file mode 100644 (file)
index 0000000..f2be337
--- /dev/null
@@ -0,0 +1,32 @@
+// Copyright 2005 Ben Hutchings <ben@decadentplace.org.uk>.
+// See the file "COPYING" for licence details.
+
+#ifndef INC_PIXBUFS_HPP
+#define INC_PIXBUFS_HPP
+
+#include <glibmm/refptr.h>
+
+namespace Gdk
+{
+    class Pixbuf;
+}
+
+// Find pixel differences between an "old" and "new" RGB Pixbuf
+// (or RGBA, but the alpha component will be ignored) and copy the
+// differing pixels from the new one to a third RGBA Pixbuf at the
+// specified offset with full opacity.
+// The width and height of the old and new Pixbufs must be equal
+// and match the specified dimensions.  The width and height of
+// the third Pixbuf must be large enough to store a rectangle of
+// those dimensions at the specified offset.
+void diff_rgb_pixbufs(Glib::RefPtr<Gdk::Pixbuf> old_buf,
+                     Glib::RefPtr<Gdk::Pixbuf> new_buf,
+                     Glib::RefPtr<Gdk::Pixbuf> diff_buf,
+                     int offset_x, int offset_y,
+                     int width, int height);
+
+// Quantise an RGBA Pixbuf to the specified number of colours, including
+// one transparent colour.  Currently uses Floyd-Steinberg dithering.
+void quantise_rgba_pixbuf(Glib::RefPtr<Gdk::Pixbuf> buf, int n_colours);
+
+#endif // !INC_PIXBUFS_HPP
diff --git a/stylesheets.cpp b/stylesheets.cpp
new file mode 100644 (file)
index 0000000..aa34643
--- /dev/null
@@ -0,0 +1,50 @@
+// Copyright 2005 Ben Hutchings <ben@decadentplace.org.uk>.
+// See the file "COPYING" for licence details.
+
+#include "stylesheets.hpp"
+
+#include <nsContentCID.h>
+#include <nsICSSLoader.h>
+#include <nsICSSStyleSheet.h>
+#include <nsIPresShell.h>
+#include <nsIServiceManagerUtils.h>
+#include <nsIURI.h>
+#include <nsNetUtil.h>
+
+#include "xpcom_support.hpp"
+
+using xpcom_support::check;
+
+// Load a CSS from an (absolute) URI.
+// TODO: Support loading from an absolute, or better, relative filename.
+already_AddRefed<nsIStyleSheet> load_css(const char * uri)
+{
+    nsCOMPtr<nsICSSLoader> css_loader;
+    static const nsCID css_loader_cid = NS_CSS_LOADER_CID;
+    check(CallGetService<nsICSSLoader>(css_loader_cid,
+                                      getter_AddRefs(css_loader)));
+
+    nsCOMPtr<nsIURI> style_sheet_uri;
+    check(NS_NewURI(getter_AddRefs(style_sheet_uri), nsCString(uri)));
+
+    nsICSSStyleSheet * style_sheet;
+    check(css_loader->LoadAgentSheet(style_sheet_uri, &style_sheet));
+    return style_sheet;
+}
+
+// Apply a style-sheet to a given presentation shell as the top-priority
+// agent style-sheet and disable the preferences-derived style rules.
+void apply_style_sheet(nsIStyleSheet * style_sheet, nsIPresShell * pres_shell)
+{
+    nsCOMArray<nsIStyleSheet> style_sheets;
+    check(pres_shell->GetAgentStyleSheets(style_sheets));
+    check(style_sheets.InsertObjectAt(style_sheet, 0));
+    check(pres_shell->SetAgentStyleSheets(style_sheets));
+
+    check(pres_shell->EnablePrefStyleRules(false));
+
+    // Update the display
+    check(pres_shell->ReconstructStyleData());
+    check(pres_shell->FlushPendingNotifications(true));
+}
+
diff --git a/stylesheets.hpp b/stylesheets.hpp
new file mode 100644 (file)
index 0000000..aa9d942
--- /dev/null
@@ -0,0 +1,20 @@
+// Copyright 2005 Ben Hutchings <ben@decadentplace.org.uk>.
+// See the file "COPYING" for licence details.
+
+#ifndef INC_STYLESHEETS_HPP
+#define INC_STYLESHEETS_HPP
+
+#include <nsCOMPtr.h>
+#include <nsIStyleSheet.h>
+
+class nsIPresShell;
+
+// Load a CSS from an (absolute) URI.
+// TODO: Support loading from an absolute, or better, relative filename.
+already_AddRefed<nsIStyleSheet> load_css(const char * uri);
+
+// Apply a style-sheet to a given presentation shell as the top-priority
+// agent style-sheet and disable the preferences-derived style rules.
+void apply_style_sheet(nsIStyleSheet *, nsIPresShell *);
+
+#endif // !INC_STYLESHEETS_HPP
diff --git a/video.hpp b/video.hpp
new file mode 100644 (file)
index 0000000..7b76b17
--- /dev/null
+++ b/video.hpp
@@ -0,0 +1,12 @@
+#ifndef INC_VIDEO_HPP
+#define INC_VIDEO_HPP
+
+namespace video
+{
+    const int pal_oscan_width = 720;
+    const int pal_oscan_height = 576;
+    const int ntsc_oscan_width = 720;
+    const int ntsc_oscan_height = 480;
+}
+
+#endif // !INC_VIDEO_HPP
diff --git a/webdvd.cpp b/webdvd.cpp
new file mode 100644 (file)
index 0000000..8e586e2
--- /dev/null
@@ -0,0 +1,795 @@
+// Copyright 2005 Ben Hutchings <ben@decadentplace.org.uk>.
+// See the file "COPYING" for licence details.
+
+#include <cassert>
+#include <exception>
+#include <fstream>
+#include <iomanip>
+#include <iostream>
+#include <memory>
+#include <queue>
+#include <set>
+#include <string>
+
+#include <stdlib.h>
+#include <unistd.h>
+
+#include <gdkmm/pixbuf.h>
+#include <gtkmm/main.h>
+#include <gtkmm/window.h>
+
+#include <nsGUIEvent.h>
+#include <nsIBoxObject.h>
+#include <nsIContent.h>
+#include <nsIDocShell.h>
+#include <nsIDOMAbstractView.h>
+#include <nsIDOMDocumentEvent.h>
+#include <nsIDOMDocumentView.h>
+#include <nsIDOMElement.h>
+#include <nsIDOMEventTarget.h>
+#include <nsIDOMHTMLDocument.h>
+#include <nsIDOMMouseEvent.h>
+#include <nsIDOMNSDocument.h>
+#include <nsIDOMWindow.h>
+#include <nsIEventStateManager.h>
+#include <nsIInterfaceRequestorUtils.h>
+#include <nsIURI.h> // required before nsILink.h
+#include <nsILink.h>
+#include <nsIPresContext.h>
+#include <nsIPresShell.h>
+#include <nsIWebBrowser.h>
+#include <nsString.h>
+
+#include "browserwidget.hpp"
+#include "childiterator.hpp"
+#include "dvd.hpp"
+#include "framebuffer.hpp"
+#include "linkiterator.hpp"
+#include "pixbufs.hpp"
+#include "stylesheets.hpp"
+#include "video.hpp"
+#include "xpcom_support.hpp"
+
+using xpcom_support::check;
+
+namespace
+{
+    struct rectangle
+    {
+       int left, top;     // inclusive
+       int right, bottom; // exclusive
+
+       rectangle operator|=(const rectangle & other)
+           {
+               if (other.empty())
+               {
+                   // use current extents unchanged
+               }
+               else if (empty())
+               {
+                   // use other extents
+                   *this = other;
+               }
+               else
+               {
+                   // find rectangle enclosing both extents
+                   left = std::min(left, other.left);
+                   top = std::min(top, other.top);
+                   right = std::max(right, other.right);
+                   bottom = std::max(bottom, other.bottom);
+               }
+
+               return *this;
+           }
+
+       rectangle operator&=(const rectangle & other)
+           {
+               // find rectangle enclosed in both extents
+               left = std::max(left, other.left);
+               top = std::max(top, other.top);
+               right = std::max(left, std::min(right, other.right));
+               bottom = std::max(top, std::min(bottom, other.bottom));
+               return *this;
+           }
+
+       bool empty() const
+           {
+               return left == right || bottom == top;
+           }
+    };
+
+    rectangle get_elem_rect(nsIDOMNSDocument * ns_doc,
+                           nsIDOMElement * elem)
+    {
+       rectangle result;
+
+       nsCOMPtr<nsIBoxObject> box;
+       check(ns_doc->GetBoxObjectFor(elem, getter_AddRefs(box)));
+       int width, height;
+       check(box->GetScreenX(&result.left));
+       check(box->GetScreenY(&result.top));
+       check(box->GetWidth(&width));
+       check(box->GetHeight(&height));
+       result.right = result.left + width;
+       result.bottom = result.top + height;
+
+       for (ChildIterator it = ChildIterator(elem), end; it != end; ++it)
+       {
+           nsCOMPtr<nsIDOMNode> child_node(*it);
+           PRUint16 child_type;
+           if (check(child_node->GetNodeType(&child_type)),
+               child_type == nsIDOMNode::ELEMENT_NODE)
+           {
+               nsCOMPtr<nsIDOMElement> child_elem(
+                   do_QueryInterface(child_node));
+               result |= get_elem_rect(ns_doc, child_elem);
+           }
+       }
+
+       return result;
+    }
+
+    class WebDvdWindow : public Gtk::Window
+    {
+    public:
+       WebDvdWindow(int width, int height);
+       void add_page(const std::string & uri);
+
+    private:
+       void add_video(const std::string & uri);
+       void load_next_page();
+       void on_net_state_change(const char * uri, gint flags, guint status);
+       void save_screenshot();
+       void process_links(nsIPresShell * pres_shell,
+                          nsIPresContext * pres_context,
+                          nsIDOMWindow * dom_window);
+       void generate_dvdauthor_file();
+
+       enum ResourceType { page_resource, video_resource };
+       typedef std::pair<ResourceType, int> ResourceEntry;
+       int width_, height_;
+       BrowserWidget browser_widget_;
+       nsCOMPtr<nsIStyleSheet> stylesheet_;
+       std::queue<std::string> page_queue_;
+       std::map<std::string, ResourceEntry> resource_map_;
+       std::vector<std::vector<std::string> > page_links_;
+       std::vector<std::string> video_paths_;
+       bool loading_;
+       int pending_req_count_;
+       struct link_state;
+       std::auto_ptr<link_state> link_state_;
+    };
+
+    WebDvdWindow::WebDvdWindow(int width, int height)
+           : width_(width), height_(height),
+             stylesheet_(load_css("file://" WEBDVD_LIB_DIR "/webdvd.css")),
+             loading_(false),
+             pending_req_count_(0)
+    {
+       set_default_size(width, height);
+       add(browser_widget_);
+       browser_widget_.show();
+       browser_widget_.signal_net_state().connect(
+           SigC::slot(*this, &WebDvdWindow::on_net_state_change));
+    }
+
+    void WebDvdWindow::add_page(const std::string & uri)
+    {
+       if (resource_map_.insert(
+               std::make_pair(uri, ResourceEntry(page_resource, 0)))
+           .second)
+       {
+           page_queue_.push(uri);
+           if (!loading_)
+               load_next_page();
+       }
+    }
+
+    void WebDvdWindow::add_video(const std::string & uri)
+    {
+       if (resource_map_.insert(
+               std::make_pair(uri, ResourceEntry(video_resource,
+                                                 video_paths_.size() + 1)))
+           .second)
+       {
+           // FIXME: Should accept some slightly different URI prefixes
+           // (e.g. file://localhost/) and decode any URI-escaped
+           // characters in the path.
+           assert(uri.compare(0, 8, "file:///") == 0);
+           video_paths_.push_back(uri.substr(7));
+       }
+    }
+
+    void WebDvdWindow::load_next_page()
+    {
+       loading_ = true;
+
+       assert(!page_queue_.empty());
+       const std::string & uri = page_queue_.front();
+       std::cout << "loading " << uri << std::endl;
+
+       std::size_t page_count = page_links_.size();
+       resource_map_[uri].second = ++page_count;
+       page_links_.resize(page_count);
+       browser_widget_.load_uri(uri);
+    }
+
+    void WebDvdWindow::on_net_state_change(const char * uri,
+                                          gint flags, guint status)
+    {
+       enum {
+           process_nothing,
+           process_new_page,
+           process_current_link
+       } action = process_nothing;
+
+       if (flags & GTK_MOZ_EMBED_FLAG_IS_REQUEST)
+       {
+           if (flags & GTK_MOZ_EMBED_FLAG_START)
+               ++pending_req_count_;
+           if (flags & GTK_MOZ_EMBED_FLAG_STOP)
+           {
+               assert(pending_req_count_ != 0);
+               --pending_req_count_;
+           }
+           if (pending_req_count_ == 0 && link_state_.get())
+               action = process_current_link;
+       }
+           
+       if (flags & GTK_MOZ_EMBED_FLAG_STOP
+           && flags & GTK_MOZ_EMBED_FLAG_IS_WINDOW)
+           action = process_new_page;
+
+       if (action != process_nothing)
+       {
+           assert(loading_ && !page_queue_.empty());
+           assert(pending_req_count_ == 0);
+
+           try
+           {
+               check(status);
+
+               nsCOMPtr<nsIWebBrowser> browser(
+                   browser_widget_.get_browser());
+               nsCOMPtr<nsIDocShell> doc_shell(do_GetInterface(browser));
+               assert(doc_shell);
+               nsCOMPtr<nsIPresShell> pres_shell;
+               check(doc_shell->GetPresShell(getter_AddRefs(pres_shell)));
+               nsCOMPtr<nsIPresContext> pres_context;
+               check(doc_shell->GetPresContext(
+                         getter_AddRefs(pres_context)));
+               nsCOMPtr<nsIDOMWindow> dom_window;
+               check(browser->GetContentDOMWindow(
+                         getter_AddRefs(dom_window)));
+
+               if (action == process_new_page)
+               {
+                   apply_style_sheet(stylesheet_, pres_shell);
+                   save_screenshot();
+               }
+               process_links(pres_shell, pres_context, dom_window);
+               if (!link_state_.get())
+               {
+                   page_queue_.pop();
+                   if (page_queue_.empty())
+                   {
+                       generate_dvdauthor_file();
+                       Gtk::Main::quit();
+                   }
+                   else
+                       load_next_page();
+               }
+           }
+           catch (std::exception & e)
+           {
+               std::cerr << "Fatal error";
+               if (!page_queue_.empty())
+                   std::cerr << " while processing <" << page_queue_.front()
+                             << ">";
+               std::cerr << ": " << e.what() << "\n";
+               Gtk::Main::quit();
+           }
+       }
+    }
+
+    void WebDvdWindow::save_screenshot()
+    {
+       char filename[20];
+       std::sprintf(filename, "page_%06d_back.png", page_links_.size());
+       Glib::RefPtr<Gdk::Window> window(get_window());
+       assert(window);
+       window->process_updates(true);
+       std::cout << "saving " << filename << std::endl;
+       Gdk::Pixbuf::create(Glib::RefPtr<Gdk::Drawable>(window),
+                           window->get_colormap(),
+                           0, 0, 0, 0, width_, height_)
+           ->save(filename, "png");
+    }
+
+    struct WebDvdWindow::link_state
+    {
+       Glib::RefPtr<Gdk::Pixbuf> diff_pixbuf;
+
+       std::ofstream spumux_file;
+
+       int link_num;
+       LinkIterator links_it, links_end;
+
+       rectangle link_rect;
+       bool link_changing;
+       Glib::RefPtr<Gdk::Pixbuf> norm_pixbuf;
+    };
+
+    void WebDvdWindow::process_links(nsIPresShell * pres_shell,
+                                    nsIPresContext * pres_context,
+                                    nsIDOMWindow * dom_window)
+    {
+       Glib::RefPtr<Gdk::Window> window(get_window());
+       assert(window);
+
+       nsCOMPtr<nsIDOMDocument> basic_doc;
+       check(dom_window->GetDocument(getter_AddRefs(basic_doc)));
+       nsCOMPtr<nsIDOMNSDocument> ns_doc(do_QueryInterface(basic_doc));
+       assert(ns_doc);
+       nsCOMPtr<nsIEventStateManager> event_state_man(
+           pres_context->EventStateManager()); // does not AddRef
+       assert(event_state_man);
+       nsCOMPtr<nsIDOMDocumentEvent> event_factory(
+           do_QueryInterface(basic_doc));
+       assert(event_factory);
+       nsCOMPtr<nsIDOMDocumentView> doc_view(do_QueryInterface(basic_doc));
+       assert(doc_view);
+       nsCOMPtr<nsIDOMAbstractView> view;
+       check(doc_view->GetDefaultView(getter_AddRefs(view)));
+
+       // Set up or recover our iteration state.
+       std::auto_ptr<link_state> state(link_state_);
+       if (!state.get())
+       {
+           state.reset(new link_state);
+
+           state->diff_pixbuf = Gdk::Pixbuf::create(Gdk::COLORSPACE_RGB,
+                                                    true, // has_alpha
+                                                    8, // bits_per_sample
+                                                    width_, height_);
+
+           char spumux_filename[20];
+           std::sprintf(spumux_filename,
+                        "page_%06d.spumux", page_links_.size());
+           state->spumux_file.open(spumux_filename);
+           state->spumux_file <<
+               "<subpictures>\n"
+               "  <stream>\n"
+               "    <spu force='yes' start='00:00:00.00'\n"
+               "        highlight='page_" << std::setfill('0')
+                              << std::setw(6) << page_links_.size()
+                              << "_links.png'\n"
+               "        select='page_" << std::setfill('0')
+                              << std::setw(6) << page_links_.size()
+                              << "_links.png'>\n";
+
+           state->link_num = 0;
+           state->links_it = LinkIterator(basic_doc);
+           state->link_changing = false;
+       }
+           
+       rectangle window_rect = { 0, 0, width_, height_ };
+
+       for (/* no initialisation */;
+            state->links_it != state->links_end;
+            ++state->links_it)
+       {
+           nsCOMPtr<nsIDOMNode> node(*state->links_it);
+
+           // Find the link URI.
+           nsCOMPtr<nsILink> link(do_QueryInterface(node));
+           assert(link);
+           nsCOMPtr<nsIURI> uri;
+           check(link->GetHrefURI(getter_AddRefs(uri)));
+           std::string uri_string;
+           {
+               nsCString uri_ns_string;
+               check(uri->GetSpec(uri_ns_string));
+               uri_string.assign(uri_ns_string.BeginReading(),
+                                 uri_ns_string.EndReading());
+           }
+           std::string uri_sans_fragment(uri_string, 0, uri_string.find('#'));
+
+           // Is this a new link?
+           if (!state->link_changing)
+           {
+               // Find a rectangle enclosing the link and clip it to the
+               // window.
+               nsCOMPtr<nsIDOMElement> elem(do_QueryInterface(node));
+               assert(elem);
+               state->link_rect = get_elem_rect(ns_doc, elem);
+               state->link_rect &= window_rect;
+
+               if (state->link_rect.empty())
+               {
+                   std::cerr << "Ignoring invisible link to "
+                             << uri_string << "\n";
+                   continue;
+               }
+
+               ++state->link_num;
+
+               if (state->link_num >= dvd::menu_buttons_max)
+               {
+                   if (state->link_num == dvd::menu_buttons_max)
+                       std::cerr << "No more than " << dvd::menu_buttons_max
+                                 << " buttons can be placed on a page\n";
+                   std::cerr << "Ignoring link to " << uri_string << "\n";
+                   continue;
+               }
+
+               // Check whether this is a link to a video or a page then
+               // add it to the known resources if not already seen.
+               nsCString path;
+               check(uri->GetPath(path));
+               // FIXME: This is a bit of a hack.  Perhaps we could decide
+               // later based on the MIME type determined by Mozilla?
+               if (path.Length() > 4
+                   && std::strcmp(path.EndReading() - 4, ".vob") == 0)
+               {
+                   PRBool is_file;
+                   check(uri->SchemeIs("file", &is_file));
+                   if (!is_file)
+                   {
+                       std::cerr << "Links to video must use the file:"
+                                 << " scheme\n";
+                       continue;
+                   }
+                   add_video(uri_sans_fragment);
+               }
+               else
+               {
+                   add_page(uri_sans_fragment);
+               }
+
+               nsCOMPtr<nsIContent> content(do_QueryInterface(node));
+               assert(content);
+               nsCOMPtr<nsIDOMEventTarget> event_target(
+                   do_QueryInterface(node));
+               assert(event_target);
+
+               state->norm_pixbuf = Gdk::Pixbuf::create(
+                   Glib::RefPtr<Gdk::Drawable>(window),
+                   window->get_colormap(),
+                   state->link_rect.left,
+                   state->link_rect.top,
+                   0,
+                   0,
+                   state->link_rect.right - state->link_rect.left,
+                   state->link_rect.bottom - state->link_rect.top);
+
+               nsCOMPtr<nsIDOMEvent> event;
+               check(event_factory->CreateEvent(
+                         NS_ConvertASCIItoUTF16("MouseEvents"),
+                         getter_AddRefs(event)));
+               nsCOMPtr<nsIDOMMouseEvent> mouse_event(
+                   do_QueryInterface(event));
+               assert(mouse_event);
+               check(mouse_event->InitMouseEvent(
+                         NS_ConvertASCIItoUTF16("mouseover"),
+                         true,  // can bubble
+                         true,  // cancelable
+                         view,
+                         0,     // detail: mouse click count
+                         state->link_rect.left, // screenX
+                         state->link_rect.top,  // screenY
+                         state->link_rect.left, // clientX
+                         state->link_rect.top,  // clientY
+                         false, false, false, false, // qualifiers
+                         0,     // button: left (or primary)
+                         0));   // related target
+               PRBool dummy;
+               check(event_target->DispatchEvent(mouse_event,
+                                                 &dummy));
+               check(event_state_man->SetContentState(content,
+                                                      NS_EVENT_STATE_HOVER));
+
+               pres_shell->FlushPendingNotifications(true);
+
+               // We may have to exit and wait for image loading
+               // to complete, at which point we will be called
+               // again.
+               if (pending_req_count_ > 0)
+               {
+                   state->link_changing = true;
+                   link_state_ = state;
+                   return;
+               }
+           }
+
+           window->process_updates(true);
+
+           Glib::RefPtr<Gdk::Pixbuf> changed_pixbuf(
+               Gdk::Pixbuf::create(
+                   Glib::RefPtr<Gdk::Drawable>(window),
+                   window->get_colormap(),
+                   state->link_rect.left,
+                   state->link_rect.top,
+                   0,
+                   0,
+                   state->link_rect.right - state->link_rect.left,
+                   state->link_rect.bottom - state->link_rect.top));
+           diff_rgb_pixbufs(
+               state->norm_pixbuf,
+               changed_pixbuf,
+               state->diff_pixbuf,
+               state->link_rect.left,
+               state->link_rect.top,
+               state->link_rect.right - state->link_rect.left,
+               state->link_rect.bottom - state->link_rect.top);
+
+           state->spumux_file <<
+               "      <button x0='" << state->link_rect.left << "'"
+               " y0='" << state->link_rect.top << "'"
+               " x1='" << state->link_rect.right - 1 << "'"
+               " y1='" << state->link_rect.bottom - 1 << "'/>\n";
+
+           // Add to the page's links, ignoring any fragment (for now).
+           page_links_.back().push_back(uri_sans_fragment);
+       }
+
+       quantise_rgba_pixbuf(state->diff_pixbuf, dvd::button_n_colours);
+
+       char filename[25];
+       std::sprintf(filename, "page_%06d_links.png", page_links_.size());
+       std::cout << "saving " << filename << std::endl;
+       state->diff_pixbuf->save(filename, "png");
+
+       state->spumux_file <<
+           "    </spu>\n"
+           "  </stream>\n"
+           "</subpictures>\n";
+    }
+
+    void generate_page_dispatch(std::ostream &, int indent,
+                               int first_page, int last_page);
+
+    void WebDvdWindow::generate_dvdauthor_file()
+    {
+       std::ofstream file("webdvd.dvdauthor");
+
+       // We generate code that uses registers in the following way:
+       //
+       // g0:     link destination (when jumping to menu 1), then scratch
+       // g1:     current location
+       // g2-g11: location history (g2 = most recent)
+       // g12:    location that last linked to a video
+       //
+       // All locations are divided into two bitfields: the least
+       // significant 10 bits are a page/menu number and the most
+       // significant 6 bits are a link/button number.  This is
+       // chosen for compatibility with the encoding of the s8
+       // (button) register.
+       //
+       static const int link_mult = dvd::reg_s8_button_mult;
+       static const int page_mask = link_mult - 1;
+       static const int link_mask = (1 << dvd::reg_bits) - link_mult;
+
+       file <<
+           "<dvdauthor>\n"
+           "  <vmgm>\n"
+           "    <menus>\n";
+           
+       for (std::size_t page_num = 1;
+            page_num <= page_links_.size();
+            ++page_num)
+       {
+           std::vector<std::string> & page_links =
+               page_links_[page_num - 1];
+
+           if (page_num == 1)
+           {
+               // This is the first page (root menu) which needs to
+               // include initialisation and dispatch code.
+       
+               file <<
+                   "      <pgc entry='title'>\n"
+                   "        <pre>\n"
+                   // Has the location been set yet?
+                   "          if (g1 eq 0)\n"
+                   "          {\n"
+                   // Initialise the current location to first link on
+                   // this page.
+                   "            g1 = " << 1 * link_mult + 1 << ";\n"
+                   "          }\n"
+                   "          else\n"
+                   "          {\n"
+                   // Has the user selected a link?
+                   "            if (g0 ne 0)\n"
+                   "            {\n"
+                   // First update the history.
+                    // Does link go to the last page in the history?
+                   "              if (((g0 ^ g2) &amp; " << page_mask
+                    << ") == 0)\n"
+                    // It does; we treat this as going back and pop the old
+                   // location off the history stack into the current
+                   // location.  Clear the free stack slot.
+                   "              {\n"
+                   "                g1 = g2; g2 = g3; g3 = g4; g4 = g5;\n"
+                   "                g5 = g6; g6 = g7; g7 = g8; g8 = g9;\n"
+                   "                g9 = g10; g10 = g11; g11 = 0;\n"
+                   "              }\n"
+                    "              else\n"
+                   // Link goes to some other page, so push current
+                   // location onto the history stack and set the current
+                   // location to be exactly the target location.
+                   "              {\n"
+                   "                g11 = g10; g10 = g9; g9 = g8; g8 = g7;\n"
+                   "                g7 = g6; g6 = g5; g5 = g4; g4 = g3;\n"
+                   "                g3 = g2; g2 = g1; g1 = g0;\n"
+                   "              }\n"
+                   "            }\n"
+                   // Find the target page number.
+                   "            g0 = g1 &amp; " << page_mask << ";\n";
+               // There seems to be no way to perform a computed jump,
+               // so we generate all possible jumps and a binary search
+               // to select the correct one.
+               generate_page_dispatch(file, 12, 1, page_links_.size());
+               file <<
+                   "          }\n";
+           }
+           else // page_num != 1
+           {
+               file <<
+                   "      <pgc>\n"
+                   "        <pre>\n";
+           }
+
+           file <<
+               // Clear link indicator and highlight the
+               // appropriate link/button.
+               "          g0 = 0; s8 = g1 &amp; " << link_mask << ";\n"
+               "        </pre>\n"
+               "        <vob file='page_"
+                << std::setfill('0') << std::setw(6) << page_num
+                << ".vob'/>\n";
+
+           for (std::size_t link_num = 1;
+                link_num <= page_links.size();
+                ++link_num)
+           {
+               file <<
+                   "        <button>"
+                   // Update current location.
+                   " g1 = " << link_num * link_mult + page_num << ";";
+
+               // Jump to appropriate resource.
+               const ResourceEntry & resource_loc =
+                   resource_map_[page_links[link_num - 1]];
+               if (resource_loc.first == page_resource)
+                   file <<
+                       " g0 = " << 1 * link_mult + resource_loc.second << ";"
+                       " jump menu 1;";
+               else if (resource_loc.first == video_resource)
+                   file << " jump title " << resource_loc.second << ";";
+
+               file <<  " </button>\n";
+           }
+
+           file << "      </pgc>\n";
+       }
+
+       file <<
+           "    </menus>\n"
+           "  </vmgm>\n";
+
+       // Generate a titleset for each video.  This appears to make
+       // jumping to titles a whole lot simpler.
+       for (std::size_t video_num = 1;
+            video_num <= video_paths_.size();
+            ++video_num)
+       {
+           file <<
+               "  <titleset>\n"
+               // Generate a dummy menu so that the menu button on the
+               // remote control will work.
+               "    <menus>\n"
+               "      <pgc entry='root'>\n"
+               "        <pre> jump vmgm menu; </pre>\n"
+               "      </pgc>\n"
+               "    </menus>\n"
+               "    <titles>\n"
+               "      <pgc>\n"
+               // Record calling page/menu.
+               "        <pre> g12 = g1; </pre>\n"
+               // FIXME: Should XML-escape the path
+               "        <vob file='" << video_paths_[video_num - 1]
+                << "'/>\n"
+               // If page/menu location has not been changed during the
+               // video, change the location to be the following
+               // link/button when returning to it.  In any case,
+               // return to a page/menu.
+               "        <post> if (g1 eq g12) g1 = g1 + " << link_mult
+                << "; call menu; </post>\n"
+               "      </pgc>\n"
+               "    </titles>\n"
+               "  </titleset>\n";
+       }
+
+       file <<
+           "</dvdauthor>\n";
+    }
+
+    void generate_page_dispatch(std::ostream & file, int indent,
+                               int first_page, int last_page)
+    {
+       if (first_page == 1 && last_page == 1)
+       {
+           // The dispatch code is *on* page 1 so we must not dispatch to
+           // page 1 since that would cause an infinite loop.  This case
+           // should be unreachable if there is more than one page due
+           // to the following case.
+       }
+       else if (first_page == 1 && last_page == 2)
+       {
+           // dvdauthor doesn't allow empty blocks or null statements so
+           // when selecting between pages 1 and 2 we don't use an "else"
+           // part.  We must use braces so that a following "else" will
+           // match the right "if".
+           file << std::setw(indent) << "" << "{\n"
+                << std::setw(indent) << "" << "if (g0 eq 2)\n"
+                << std::setw(indent + 2) << "" << "jump menu 2;\n"
+                << std::setw(indent) << "" << "}\n";
+       }
+       else if (first_page == last_page)
+       {
+           file << std::setw(indent) << ""
+                << "jump menu " << first_page << ";\n";
+       }
+       else
+       {
+           int middle = (first_page + last_page) / 2;
+           file << std::setw(indent) << "" << "if (g0 le " << middle << ")\n";
+           generate_page_dispatch(file, indent + 2, first_page, middle);
+           file << std::setw(indent) << "" << "else\n";
+           generate_page_dispatch(file, indent + 2, middle + 1, last_page);
+       }
+    }
+
+} // namespace
+
+int main(int argc, char ** argv)
+{
+    // Get dimensions
+    int width = video::pal_oscan_width, height = video::pal_oscan_height;
+    for (int i = 1; i < argc - 1; ++i)
+       if (std::strcmp(argv[i], "-geometry") == 0)
+       {
+           std::sscanf(argv[i + 1], "%dx%d", &width, &height);
+           break;
+       }
+    // A depth of 24 results in 8 bits each for RGB components, which
+    // translates into "enough" bits for YUV components.
+    const int depth = 24;
+
+    try
+    {
+       // Spawn Xvfb and set env variables so that Xlib will use it
+       FrameBuffer fb(width, height, depth);
+       setenv("XAUTHORITY", fb.get_x_authority().c_str(), true);
+       setenv("DISPLAY", fb.get_x_display().c_str(), true);
+
+       // Initialise Gtk and Mozilla
+       Gtk::Main kit(argc, argv);
+       BrowserWidget::init();
+
+       WebDvdWindow window(width, height);
+       for (int argi = 1; argi < argc; ++argi)
+           window.add_page(argv[argi]);
+       if (argc < 2)
+           window.add_page("about:");
+       Gtk::Main::run(window);
+    }
+    catch (std::exception & e)
+    {
+       std::cerr << "Fatal error: " << e.what() << "\n";
+       return EXIT_FAILURE;
+    }
+
+    return EXIT_SUCCESS;
+}
diff --git a/webdvd.css b/webdvd.css
new file mode 100644 (file)
index 0000000..05159f5
--- /dev/null
@@ -0,0 +1,25 @@
+body {
+  /* Sans-serif fonts will be much more readable than serif on a TV. */
+  font-family: sans-serif;
+  /* Let the background overscan, but not the content. */
+  padding: 60px 50px 60px 50px;
+}
+a:link, a:visited {
+  color: blue;
+  /* Don't underline links because underlining tends to flicker on TVs. */
+  text-decoration: none;
+  /* Buttons have to be rectangular (AFAIK). */
+  white-space: nowrap;
+}
+/* The hover state must be made obvious since DVD players have no pointer. */
+a:hover {
+  color: red;
+}
+/* The active state should provide visual feedback, but is not so critical. */
+a:active {
+  color: purple;
+}
+/* Don't show focus rectangles. */
+*|*:-moz-any-link:focus {
+  -moz-outline: none !important;
+}
diff --git a/xpcom_support.cpp b/xpcom_support.cpp
new file mode 100644 (file)
index 0000000..6576665
--- /dev/null
@@ -0,0 +1,38 @@
+// Copyright 2005 Ben Hutchings <ben@decadentplace.org.uk>.
+// See the file "COPYING" for licence details.
+
+#include <cassert>
+#include <memory>
+#include <stdexcept>
+
+#include "xpcom_support.hpp"
+
+namespace xpcom_support
+{
+    void throw_exception(nsresult error)
+    {
+       assert(NS_ERROR_GET_SEVERITY(error) == NS_ERROR_SEVERITY_ERROR);
+
+       // TODO: look up error message
+       char message[30];
+       std::sprintf(message, "XPCOM error %08x", error);
+
+       switch (error)
+       {
+       case NS_ERROR_OUT_OF_MEMORY:
+           throw std::bad_alloc();
+
+       case NS_ERROR_NOT_INITIALIZED:
+       case NS_ERROR_ALREADY_INITIALIZED:
+       case NS_ERROR_INVALID_POINTER:
+       case NS_ERROR_ILLEGAL_VALUE:
+       case NS_BASE_STREAM_CLOSED:
+       case NS_BASE_STREAM_ILLEGAL_ARGS:
+           assert(!"internal error detected by XPCOM function");
+           throw std::logic_error(message);
+
+       default:
+           throw std::runtime_error(message);
+       }
+    }
+}
diff --git a/xpcom_support.hpp b/xpcom_support.hpp
new file mode 100644 (file)
index 0000000..9d42692
--- /dev/null
@@ -0,0 +1,23 @@
+// Copyright 2005 Ben Hutchings <ben@decadentplace.org.uk>.
+// See the file "COPYING" for licence details.
+
+#ifndef INC_XPCOM_SUPPORT_HPP
+#define INC_XPCOM_SUPPORT_HPP
+
+#include <stdexcept>
+
+#include <nsError.h>
+
+namespace xpcom_support
+{
+    void throw_exception(nsresult error);
+
+    inline nsresult check(nsresult result)
+    {
+       if (NS_FAILED(result))
+           throw_exception(result);
+       return result;
+    }
+}
+
+#endif // !INC_XPCOM_SUPPORT_HPP