From 410c2b9017bc26a7e79269c1f7fc606ad89249bb Mon Sep 17 00:00:00 2001 From: Ben Hutchings Date: Thu, 3 Nov 2005 02:22:37 +0000 Subject: [PATCH] Imported version 0.1 --- COPYING | 355 +++++++++++ INSTALL | 17 + Makefile | 74 +++ README | 113 ++++ TODO | 6 + auto_array.hpp | 79 +++ auto_fd.hpp | 30 + auto_handle.hpp | 83 +++ auto_proc.cpp | 32 + auto_proc.hpp | 30 + auto_temp_file.hpp | 28 + browserwidget.cpp | 532 ++++++++++++++++ browserwidget.hpp | 69 +++ childiterator.cpp | 46 ++ childiterator.hpp | 33 + dvd.hpp | 23 + framebuffer.cpp | 178 ++++++ framebuffer.hpp | 26 + jquant2.c | 1471 ++++++++++++++++++++++++++++++++++++++++++++ jquant2.h | 28 + linkiterator.cpp | 52 ++ linkiterator.hpp | 36 ++ pixbufs.cpp | 112 ++++ pixbufs.hpp | 32 + stylesheets.cpp | 50 ++ stylesheets.hpp | 20 + video.hpp | 12 + webdvd.cpp | 795 ++++++++++++++++++++++++ webdvd.css | 25 + xpcom_support.cpp | 38 ++ xpcom_support.hpp | 23 + 31 files changed, 4448 insertions(+) create mode 100644 COPYING create mode 100644 INSTALL create mode 100644 Makefile create mode 100644 README create mode 100644 TODO create mode 100644 auto_array.hpp create mode 100644 auto_fd.hpp create mode 100644 auto_handle.hpp create mode 100644 auto_proc.cpp create mode 100644 auto_proc.hpp create mode 100644 auto_temp_file.hpp create mode 100644 browserwidget.cpp create mode 100644 browserwidget.hpp create mode 100644 childiterator.cpp create mode 100644 childiterator.hpp create mode 100644 dvd.hpp create mode 100644 framebuffer.cpp create mode 100644 framebuffer.hpp create mode 100644 jquant2.c create mode 100644 jquant2.h create mode 100644 linkiterator.cpp create mode 100644 linkiterator.hpp create mode 100644 pixbufs.cpp create mode 100644 pixbufs.hpp create mode 100644 stylesheets.cpp create mode 100644 stylesheets.hpp create mode 100644 video.hpp create mode 100644 webdvd.cpp create mode 100644 webdvd.css create mode 100644 xpcom_support.cpp create mode 100644 xpcom_support.hpp diff --git a/COPYING b/COPYING new file mode 100644 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. + + 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.) + +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. + + 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. + + 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 + + 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. + + + Copyright (C) + + 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. + + , 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 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 diff --git a/Makefile b/Makefile new file mode 100644 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 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 . +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 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 index 0000000..6567be2 --- /dev/null +++ b/auto_array.hpp @@ -0,0 +1,79 @@ +// Copyright 2005 Ben Hutchings . +// See the file "COPYING" for licence details. + +#ifndef INC_AUTO_ARRAY_HPP +#define INC_AUTO_ARRAY_HPP + +#include + +// Like auto_ptr, but for arrays + +template +class auto_array_ref; + +template +class auto_array +{ + typedef auto_array_ref 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 +class auto_array_ref +{ + typedef auto_array 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 index 0000000..d89cd8c --- /dev/null +++ b/auto_fd.hpp @@ -0,0 +1,30 @@ +// Copyright 2005 Ben Hutchings . +// See the file "COPYING" for licence details. + +#ifndef INC_AUTO_FD_HPP +#define INC_AUTO_FD_HPP + +#include "auto_handle.hpp" + +#include + +#include + +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 auto_fd; + +#endif // !INC_AUTO_FD_HPP diff --git a/auto_handle.hpp b/auto_handle.hpp new file mode 100644 index 0000000..b9d0b6b --- /dev/null +++ b/auto_handle.hpp @@ -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 +class auto_handle_ref; + +template +class auto_handle + // Use inheritance so we can benefit from the empty base optimisation + : private closer_type, private factory_type +{ + typedef auto_handle_ref 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 +class auto_handle_ref +{ + typedef auto_handle 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 index 0000000..1e7d70c --- /dev/null +++ b/auto_proc.cpp @@ -0,0 +1,32 @@ +// Copyright 2005 Ben Hutchings . +// See the file "COPYING" for licence details. + +#include + +#include +#include +#include +#include + +#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 index 0000000..3dd2b66 --- /dev/null +++ b/auto_proc.hpp @@ -0,0 +1,30 @@ +// Copyright 2005 Ben Hutchings . +// See the file "COPYING" for licence details. + +#ifndef INC_AUTO_PROC_HPP +#define INC_AUTO_PROC_HPP + +#include + +#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 + auto_kill_proc; + +struct auto_wait_proc_closer +{ + void operator()(pid_t pid) const; +}; +typedef auto_handle + auto_wait_proc; + +#endif // !INC_AUTO_PROC_HPP diff --git a/auto_temp_file.hpp b/auto_temp_file.hpp new file mode 100644 index 0000000..5aa3961 --- /dev/null +++ b/auto_temp_file.hpp @@ -0,0 +1,28 @@ +// Copyright 2005 Ben Hutchings . +// See the file "COPYING" for licence details. + +#ifndef INC_AUTO_TEMP_FILE_HPP +#define INC_AUTO_TEMP_FILE_HPP + +#include "auto_handle.hpp" + +#include + +#include + +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 + auto_temp_file; + +#endif // !INC_AUTO_TEMP_FILE_HPP diff --git a/browserwidget.cpp b/browserwidget.cpp new file mode 100644 index 0000000..77c1846 --- /dev/null +++ b/browserwidget.cpp @@ -0,0 +1,532 @@ +// Copyright 2005 Ben Hutchings . +// See the file "COPYING" for licence details. + +#include "browserwidget.hpp" + +#include + +#include + +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(gobj())); +} +bool BrowserWidget::can_go_forward() const +{ + return gtk_moz_embed_can_go_forward(const_cast(gobj())); +} + +namespace +{ + template + 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 str( + gtk_moz_embed_get_link_message(const_cast(gobj()))); + return std::string(str.get()); +} +std::string BrowserWidget::get_js_status() const +{ + c_scoped_ptr str( + gtk_moz_embed_get_js_status(const_cast(gobj()))); + return std::string(str.get()); +} +std::string BrowserWidget::get_title() const +{ + c_scoped_ptr str( + gtk_moz_embed_get_title(const_cast(gobj()))); + return std::string(str.get()); +} +std::string BrowserWidget::get_location() const +{ + c_scoped_ptr str( + gtk_moz_embed_get_location(const_cast(gobj()))); + return std::string(str.get()); +} +already_AddRefed 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 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 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 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 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 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 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 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 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 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 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 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 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 BrowserWidget::signal_link_message() +{ + return Glib::SignalProxy0(this, &BrowserWidget_signal_link_message_info); +} +Glib::SignalProxy0 BrowserWidget::signal_js_status() +{ + return Glib::SignalProxy0(this, &BrowserWidget_signal_js_status_info); +} +Glib::SignalProxy0 BrowserWidget::signal_location() +{ + return Glib::SignalProxy0(this, &BrowserWidget_signal_location_info); +} +Glib::SignalProxy0 BrowserWidget::signal_title() +{ + return Glib::SignalProxy0(this, &BrowserWidget_signal_title_info); +} +Glib::SignalProxy2 BrowserWidget::signal_progress() +{ + return Glib::SignalProxy2( + this, &BrowserWidget_signal_progress_info); +} +Glib::SignalProxy3 +BrowserWidget::signal_net_state() +{ + return Glib::SignalProxy3( + this, &BrowserWidget_signal_net_state_info); +} +Glib::SignalProxy0 BrowserWidget::signal_net_start() +{ + return Glib::SignalProxy0(this, &BrowserWidget_signal_net_start_info); +} +Glib::SignalProxy0 BrowserWidget::signal_net_stop() +{ + return Glib::SignalProxy0(this, &BrowserWidget_signal_net_stop_info); +} +Glib::SignalProxy1 BrowserWidget::signal_new_window() +{ + return Glib::SignalProxy1( + this, &BrowserWidget_signal_new_window_info); +} +Glib::SignalProxy1 BrowserWidget::signal_visibility() +{ + return Glib::SignalProxy1( + this, &BrowserWidget_signal_visibility_info); +} +Glib::SignalProxy0 BrowserWidget::signal_destroy() +{ + return Glib::SignalProxy0(this, &BrowserWidget_signal_destroy_info); +} +Glib::SignalProxy1 BrowserWidget::signal_open_uri() +{ + return Glib::SignalProxy1( + 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( + Glib::wrap_auto((GObject*)(object), take_copy)); + } +} diff --git a/browserwidget.hpp b/browserwidget.hpp new file mode 100644 index 0000000..1d1e85d --- /dev/null +++ b/browserwidget.hpp @@ -0,0 +1,69 @@ +// Copyright 2005 Ben Hutchings . +// See the file "COPYING" for licence details. + +#ifndef INC_BROWSERWIDGET_HPP +#define INC_BROWSERWIDGET_HPP + +#include +#include + +#include +#include + +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 get_browser(); + + Glib::SignalProxy0 signal_link_message(); + Glib::SignalProxy0 signal_js_status(); + Glib::SignalProxy0 signal_location(); + Glib::SignalProxy0 signal_title(); + Glib::SignalProxy2 signal_progress(); + Glib::SignalProxy3 + signal_net_state(); + Glib::SignalProxy0 signal_net_start(); + Glib::SignalProxy0 signal_net_stop(); + Glib::SignalProxy1 signal_new_window(); + Glib::SignalProxy1 signal_visibility(); + Glib::SignalProxy0 signal_destroy(); + Glib::SignalProxy1 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 index 0000000..285b4a8 --- /dev/null +++ b/childiterator.cpp @@ -0,0 +1,46 @@ +// Copyright 2005 Ben Hutchings . +// See the file "COPYING" for licence details. + +#include "childiterator.hpp" + +#include + +#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 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 index 0000000..edc937c --- /dev/null +++ b/childiterator.hpp @@ -0,0 +1,33 @@ +// Copyright 2005 Ben Hutchings . +// See the file "COPYING" for licence details. + +#ifndef INC_CHILDITERATOR_HPP +#define INC_CHILDITERATOR_HPP + +#include + +#include +#include + +class ChildIterator + : public std::iterator, + void, void, void> +{ +public: + ChildIterator(); + explicit ChildIterator(nsIDOMNode * node); + ~ChildIterator(); + + already_AddRefed 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 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 index 0000000..eab879a --- /dev/null +++ b/framebuffer.cpp @@ -0,0 +1,178 @@ +// Copyright 2005 Ben Hutchings . +// See the file "COPYING" for licence details. + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#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 index 0000000..daf7659 --- /dev/null +++ b/framebuffer.hpp @@ -0,0 +1,26 @@ +// Copyright 2005 Ben Hutchings . +// See the file "COPYING" for licence details. + +#ifndef INC_FRAMEBUFFER_HPP +#define INC_FRAMEBUFFER_HPP + +#include + +#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 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 +#include +#include + +#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<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<>1)) * count; + c1total += ((c1 << C1_SHIFT) + ((1<>1)) * count; + c2total += ((c2 << C2_SHIFT) + ((1<>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<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 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 index 0000000..b751c1d --- /dev/null +++ b/linkiterator.cpp @@ -0,0 +1,52 @@ +// Copyright 2005 Ben Hutchings . +// See the file "COPYING" for licence details. + +#include "linkiterator.hpp" + +#include + +#include +#include + +LinkIterator::LinkIterator() +{} + +LinkIterator::LinkIterator(nsIDOMDocument * document) +{ + nsCOMPtr 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 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 index 0000000..2f93a2f --- /dev/null +++ b/linkiterator.hpp @@ -0,0 +1,36 @@ +// Copyright 2005 Ben Hutchings . +// See the file "COPYING" for licence details. + +#ifndef INC_LINKITERATOR_HPP +#define INC_LINKITERATOR_HPP + +#include + +#include +#include +#include + +class nsIDOMDocument; + +class LinkIterator + : public std::iterator, + void, void, void> +{ +public: + LinkIterator(); + explicit LinkIterator(nsIDOMDocument * document); + + already_AddRefed operator*() const; + LinkIterator & operator++(); + bool operator==(const LinkIterator &) const; + bool operator!=(const LinkIterator & other) const + { + return !(*this == other); + } + +private: + nsCOMPtr collection_; + unsigned int index_, length_; +}; + +#endif // !INC_LINKITERATOR_HPP diff --git a/pixbufs.cpp b/pixbufs.cpp new file mode 100644 index 0000000..39a569c --- /dev/null +++ b/pixbufs.cpp @@ -0,0 +1,112 @@ +// Copyright 2005 Ben Hutchings . +// See the file "COPYING" for licence details. + +#include "pixbufs.hpp" + +#include + +#include + +#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 old_buf, + Glib::RefPtr new_buf, + Glib::RefPtr 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 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 rows(new unsigned char *[height]); + for (int y = 0; y != height; ++y) + rows[y] = &buf_p[y * bpr]; + auto_array quant_buf_p( + new unsigned char[width * height]); + auto_array quant_rows( + new unsigned char *[height]); + for (int y = 0; y != height; ++y) + quant_rows[y] = &quant_buf_p[y * width]; + auto_array 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 index 0000000..f2be337 --- /dev/null +++ b/pixbufs.hpp @@ -0,0 +1,32 @@ +// Copyright 2005 Ben Hutchings . +// See the file "COPYING" for licence details. + +#ifndef INC_PIXBUFS_HPP +#define INC_PIXBUFS_HPP + +#include + +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 old_buf, + Glib::RefPtr new_buf, + Glib::RefPtr 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 buf, int n_colours); + +#endif // !INC_PIXBUFS_HPP diff --git a/stylesheets.cpp b/stylesheets.cpp new file mode 100644 index 0000000..aa34643 --- /dev/null +++ b/stylesheets.cpp @@ -0,0 +1,50 @@ +// Copyright 2005 Ben Hutchings . +// See the file "COPYING" for licence details. + +#include "stylesheets.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#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 load_css(const char * uri) +{ + nsCOMPtr css_loader; + static const nsCID css_loader_cid = NS_CSS_LOADER_CID; + check(CallGetService(css_loader_cid, + getter_AddRefs(css_loader))); + + nsCOMPtr 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 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 index 0000000..aa9d942 --- /dev/null +++ b/stylesheets.hpp @@ -0,0 +1,20 @@ +// Copyright 2005 Ben Hutchings . +// See the file "COPYING" for licence details. + +#ifndef INC_STYLESHEETS_HPP +#define INC_STYLESHEETS_HPP + +#include +#include + +class nsIPresShell; + +// Load a CSS from an (absolute) URI. +// TODO: Support loading from an absolute, or better, relative filename. +already_AddRefed 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 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 index 0000000..8e586e2 --- /dev/null +++ b/webdvd.cpp @@ -0,0 +1,795 @@ +// Copyright 2005 Ben Hutchings . +// See the file "COPYING" for licence details. + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // required before nsILink.h +#include +#include +#include +#include +#include + +#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 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 child_node(*it); + PRUint16 child_type; + if (check(child_node->GetNodeType(&child_type)), + child_type == nsIDOMNode::ELEMENT_NODE) + { + nsCOMPtr 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 ResourceEntry; + int width_, height_; + BrowserWidget browser_widget_; + nsCOMPtr stylesheet_; + std::queue page_queue_; + std::map resource_map_; + std::vector > page_links_; + std::vector video_paths_; + bool loading_; + int pending_req_count_; + struct link_state; + std::auto_ptr 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 browser( + browser_widget_.get_browser()); + nsCOMPtr doc_shell(do_GetInterface(browser)); + assert(doc_shell); + nsCOMPtr pres_shell; + check(doc_shell->GetPresShell(getter_AddRefs(pres_shell))); + nsCOMPtr pres_context; + check(doc_shell->GetPresContext( + getter_AddRefs(pres_context))); + nsCOMPtr 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 window(get_window()); + assert(window); + window->process_updates(true); + std::cout << "saving " << filename << std::endl; + Gdk::Pixbuf::create(Glib::RefPtr(window), + window->get_colormap(), + 0, 0, 0, 0, width_, height_) + ->save(filename, "png"); + } + + struct WebDvdWindow::link_state + { + Glib::RefPtr diff_pixbuf; + + std::ofstream spumux_file; + + int link_num; + LinkIterator links_it, links_end; + + rectangle link_rect; + bool link_changing; + Glib::RefPtr norm_pixbuf; + }; + + void WebDvdWindow::process_links(nsIPresShell * pres_shell, + nsIPresContext * pres_context, + nsIDOMWindow * dom_window) + { + Glib::RefPtr window(get_window()); + assert(window); + + nsCOMPtr basic_doc; + check(dom_window->GetDocument(getter_AddRefs(basic_doc))); + nsCOMPtr ns_doc(do_QueryInterface(basic_doc)); + assert(ns_doc); + nsCOMPtr event_state_man( + pres_context->EventStateManager()); // does not AddRef + assert(event_state_man); + nsCOMPtr event_factory( + do_QueryInterface(basic_doc)); + assert(event_factory); + nsCOMPtr doc_view(do_QueryInterface(basic_doc)); + assert(doc_view); + nsCOMPtr view; + check(doc_view->GetDefaultView(getter_AddRefs(view))); + + // Set up or recover our iteration state. + std::auto_ptr 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 << + "\n" + " \n" + " \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 node(*state->links_it); + + // Find the link URI. + nsCOMPtr link(do_QueryInterface(node)); + assert(link); + nsCOMPtr 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 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 content(do_QueryInterface(node)); + assert(content); + nsCOMPtr event_target( + do_QueryInterface(node)); + assert(event_target); + + state->norm_pixbuf = Gdk::Pixbuf::create( + Glib::RefPtr(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 event; + check(event_factory->CreateEvent( + NS_ConvertASCIItoUTF16("MouseEvents"), + getter_AddRefs(event))); + nsCOMPtr 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 changed_pixbuf( + Gdk::Pixbuf::create( + Glib::RefPtr(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 << + " \n"; + } + + file << " \n"; + } + + file << + " \n" + " \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 << + " \n" + // Generate a dummy menu so that the menu button on the + // remote control will work. + " \n" + " \n" + "
 jump vmgm menu; 
\n" + "
\n" + "
\n" + " \n" + " \n" + // Record calling page/menu. + "
 g12 = g1; 
\n" + // FIXME: Should XML-escape the path + " \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. + " if (g1 eq g12) g1 = g1 + " << link_mult + << "; call menu; \n" + "
\n" + "
\n" + "
\n"; + } + + file << + "\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 index 0000000..05159f5 --- /dev/null +++ b/webdvd.css @@ -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 index 0000000..6576665 --- /dev/null +++ b/xpcom_support.cpp @@ -0,0 +1,38 @@ +// Copyright 2005 Ben Hutchings . +// See the file "COPYING" for licence details. + +#include +#include +#include + +#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 index 0000000..9d42692 --- /dev/null +++ b/xpcom_support.hpp @@ -0,0 +1,23 @@ +// Copyright 2005 Ben Hutchings . +// See the file "COPYING" for licence details. + +#ifndef INC_XPCOM_SUPPORT_HPP +#define INC_XPCOM_SUPPORT_HPP + +#include + +#include + +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 -- 2.39.2