Browse Source

handle larger, nested indexes

David Rose 23 years ago
parent
commit
d99b1ff360

+ 2 - 1
pandaapp/src/indexify/fontSamples.cxx

@@ -20,9 +20,10 @@
 #include "pnmTextMaker.h"
 #include "default_font.h"
 #include "pnmImage.h"
-
 #include "notify.h"
 
+#include <stdio.h>
+
 ////////////////////////////////////////////////////////////////////
 //     Function: FontSamples::Constructor
 //       Access: Public

+ 27 - 64
pandaapp/src/indexify/indexImage.cxx

@@ -96,8 +96,8 @@ generate_images(const Filename &archive_dir, PNMTextMaker *text_maker) {
   PNMImage index_image;
 
   Filename reduced_dir(archive_dir, "reduced/" + _dir->get_basename());
-
-  Filename output_filename(archive_dir, _name);
+  Filename thumbnail_dir(archive_dir, "thumbs");
+  Filename output_filename(thumbnail_dir, _name);
   output_filename.set_extension("jpg");
 
   Photos::const_iterator pi;
@@ -161,9 +161,11 @@ generate_images(const Filename &archive_dir, PNMTextMaker *text_maker) {
     Photo *photo = _dir->get_photo(pinfo._photo_index);
     Filename photo_filename(_dir->get_dir(), photo->get_basename());
     Filename reduced_filename(reduced_dir, photo->get_basename());
+    photo_filename.standardize();
+    reduced_filename.standardize();
     PNMImage reduced_image;
 
-    if (!dummy_mode &&
+    if (!dummy_mode && photo_filename != reduced_filename &&
 	(force_regenerate || 
 	 reduced_filename.compare_timestamps(photo_filename) < 0)) {
       // If the reduced filename does not exist or is older than the
@@ -182,6 +184,10 @@ generate_images(const Filename &archive_dir, PNMTextMaker *text_maker) {
       
       // Generate a reduced image for the photo.
       compute_reduction(photo_image, reduced_image, reduced_width, reduced_height);
+
+      photo->_reduced_x_size = reduced_image.get_x_size();
+      photo->_reduced_y_size = reduced_image.get_y_size();
+
       reduced_image.quick_filter_from(photo_image);
       reduced_filename.make_dir();
       nout << "Writing " << reduced_filename << "\n";
@@ -190,9 +196,6 @@ generate_images(const Filename &archive_dir, PNMTextMaker *text_maker) {
 	return false;
       }
 
-      photo->_reduced_x_size = reduced_image.get_x_size();
-      photo->_reduced_y_size = reduced_image.get_y_size();
-
     } else {
       // If the reduced image already exists and is newer than the
       // source image, use it.
@@ -262,7 +265,8 @@ generate_images(const Filename &archive_dir, PNMTextMaker *text_maker) {
 		   pinfo._x_place + x_center, pinfo._y_place + y_center,
 		   thumbnail_image.get_x_size(), thumbnail_image.get_y_size());
       }
-      
+
+      thumbnail_image.set_color_type(index_image.get_color_type());
       index_image.copy_sub_image(thumbnail_image, 
 				 pinfo._x_place + x_center, 
 				 pinfo._y_place + y_center);
@@ -277,6 +281,7 @@ generate_images(const Filename &archive_dir, PNMTextMaker *text_maker) {
 
   if (generate_index_image) {
     nout << "Writing " << output_filename << "\n";
+    output_filename.make_dir();
     if (!index_image.write(output_filename)) {
       nout << "Unable to write.\n";
       return false;
@@ -304,21 +309,22 @@ generate_html(ostream &root_html, const Filename &archive_dir,
               const Filename &roll_dir_root) {
   root_html
     << "<a name=\"" << _name << "\">\n"
-    << "<img src=\"" << _index_basename
+    << "<img src=\"../thumbs/" << _index_basename
     << "\" width=" << _index_x_size << " height=" << _index_y_size
     << " ismap usemap=\"#" << _name << "\">\n"
     << "<map name=\"" << _name << "\"><br>\n";
 
+  Filename html_dir(archive_dir, "html");
+
   Photos::const_iterator pi;
   for (pi = _photos.begin(); pi != _photos.end(); ++pi) {
     const PhotoInfo &pinfo = (*pi);
     int photo_index = pinfo._photo_index;
     Photo *photo = _dir->get_photo(pinfo._photo_index);
-    Filename html_relname("html/" + _dir->get_basename(), 
-                          photo->get_basename());
+    Filename html_relname(_dir->get_basename(), photo->get_basename());
     html_relname.set_extension("htm");
 
-    Filename html_filename(archive_dir, html_relname);
+    Filename html_filename(html_dir, html_relname);
     html_filename.make_dir();
     html_filename.set_text();
     ofstream reduced_html;
@@ -382,15 +388,18 @@ write(ostream &out, int indent_level) const {
 bool IndexImage::
 generate_reduced_html(ostream &html, Photo *photo, int photo_index, int pi,
                       const Filename &roll_dir_root) {
-  Filename full_dir =
+  Filename full_dir;
+  if (roll_dir_root.empty()) {
+    full_dir = Filename("../..", _dir->get_dir());
+  } else {
     compose_href("../..", roll_dir_root, _dir->get_basename());
+  }
   Filename full(full_dir, photo->get_basename());
 
   Filename reduced_dir("../../reduced", _dir->get_basename());
   Filename reduced(reduced_dir, photo->get_basename());
 
-  Filename root_html("../..", "index.htm");
-  string up_href = root_html.get_fullpath() + "#" + _name;
+  string up_href = "../" + _dir->get_basename() + ".htm#" + _name;
 
   Filename prev_photo_filename;
   Filename next_photo_filename;
@@ -485,7 +494,9 @@ generate_reduced_html(ostream &html, Photo *photo, int photo_index, int pi,
     << " height=" << photo->_reduced_y_size << " alt=\"" << photo->get_name()
     << "\"></a></p>\n";
 
-  if (!omit_full_links) {
+  if (!omit_full_links && 
+      (photo->_full_x_size != photo->_reduced_x_size || 
+       photo->_full_y_size != photo->_reduced_y_size)) {
     html
       << "<p><a href=\"" << full << "\">View full size image ("
       << photo->_full_x_size << " x " << photo->_full_y_size << ")</a></p>";
@@ -503,7 +514,7 @@ generate_reduced_html(ostream &html, Photo *photo, int photo_index, int pi,
 
 ////////////////////////////////////////////////////////////////////
 //     Function: IndexImage::generate_nav_buttons
-//       Access: Private, Static
+//       Access: Private
 //  Description: Outputs the HTML code to generate the next, prev,
 //               up buttons when viewing each reduced image.
 ////////////////////////////////////////////////////////////////////
@@ -570,54 +581,6 @@ generate_nav_buttons(ostream &html, const Filename &prev_photo_filename,
   html << "</p>\n";
 }
 
-////////////////////////////////////////////////////////////////////
-//     Function: IndexImage::compose_href
-//       Access: Private
-//  Description: Combines a user-supplied prefix with a relative
-//               directory to generate the appropriate href to the
-//               file.
-//
-//               rel_dir is the relative path to archive_dir.
-//               user_prefix is the string the user indicated as the
-//               relative or absolute path to the file's parent
-//               directory from archive_dir.  basename is the name
-//               of the file, or empty if the filename is part of
-//               user_prefix.
-////////////////////////////////////////////////////////////////////
-Filename IndexImage::
-compose_href(const Filename &rel_dir, const Filename &user_prefix,
-             const Filename &basename) {
-  Filename result;
-
-  if (user_prefix.empty()) {
-    result = rel_dir;
-
-  } else {
-    // Check to see if the user prefix begins with a URL designator,
-    // like http:// or ftp://.
-    size_t ui = 0;
-    while (ui < user_prefix.length() && isalpha(user_prefix[ui])) {
-      ui++;
-    }
-    bool is_url = (user_prefix.get_fullpath().substr(ui, 3) == "://");
-    
-    if (!is_url && user_prefix.is_local()) {
-      Filename rel_user_dir(rel_dir, user_prefix);
-      result = rel_user_dir;
-      result.standardize();
-      
-    } else {
-      result = user_prefix;
-    }
-  }
-
-  if (basename.empty()) {
-    return result;
-  } else {
-    return Filename(result, basename);
-  }
-}
-
 ////////////////////////////////////////////////////////////////////
 //     Function: IndexImage::compute_reduction
 //       Access: Private, Static

+ 0 - 3
pandaapp/src/indexify/indexImage.h

@@ -56,9 +56,6 @@ private:
                             const Filename &next_photo_filename, 
                             const string &up_href);
 
-  Filename compose_href(const Filename &rel_dir, const Filename &user_prefix,
-                        const Filename &basename = Filename());
-
   static void compute_reduction(const PNMImageHeader &source_image,
                                 PNMImage &dest_image,
                                 int x_size, int y_size);

+ 114 - 0
pandaapp/src/indexify/indexParameters.cxx

@@ -44,6 +44,7 @@ Filename up_icon;
 
 bool force_regenerate = false;
 bool format_rose = false;
+bool sort_date = false;
 bool dummy_mode = false;
 bool draw_frames = false;
 bool omit_roll_headers = false;
@@ -87,3 +88,116 @@ finalize_parameters() {
     thumb_interior_height = thumb_height;
   }
 }
+
+////////////////////////////////////////////////////////////////////
+//     Function: compose_href
+//  Description: Combines a user-supplied prefix with a relative
+//               directory to generate the appropriate href to the
+//               file.
+//
+//               rel_dir is the relative path to archive_dir.
+//               user_prefix is the string the user indicated as the
+//               relative or absolute path to the file's parent
+//               directory from archive_dir.  basename is the name
+//               of the file, or empty if the filename is part of
+//               user_prefix.
+////////////////////////////////////////////////////////////////////
+Filename
+compose_href(const Filename &rel_dir, const Filename &user_prefix,
+             const Filename &basename) {
+  Filename result;
+
+  if (user_prefix.empty()) {
+    result = rel_dir;
+
+  } else {
+    // Check to see if the user prefix begins with a URL designator,
+    // like http:// or ftp://.
+    size_t ui = 0;
+    while (ui < user_prefix.length() && isalpha(user_prefix[ui])) {
+      ui++;
+    }
+    bool is_url = (user_prefix.get_fullpath().substr(ui, 3) == "://");
+    
+    if (!is_url && user_prefix.is_local()) {
+      Filename rel_user_dir(rel_dir, user_prefix);
+      result = rel_user_dir;
+      result.standardize();
+      
+    } else {
+      result = user_prefix;
+    }
+  }
+
+  if (basename.empty()) {
+    return result;
+  } else {
+    return Filename(result, basename);
+  }
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: escape_html
+//  Description: Returns the input string with all invalid characters
+//               for HTML code replaced by their HTML equivalents.
+////////////////////////////////////////////////////////////////////
+string
+escape_html(const string &input) {
+  static const struct {
+    const char *name;
+    int code;
+  } tokens[] = {
+    { "amp", '&' }, { "lt", '<' }, { "gt", '>' }, { "quot", '"' },
+    { "nbsp", 160 },
+
+    { "iexcl", 161 }, { "cent", 162 }, { "pound", 163 }, { "curren", 164 },
+    { "yen", 165 }, { "brvbar", 166 }, { "brkbar", 166 }, { "sect", 167 },
+    { "uml", 168 }, { "die", 168 }, { "copy", 169 }, { "ordf", 170 },
+    { "laquo", 171 }, { "not", 172 }, { "shy", 173 }, { "reg", 174 },
+    { "macr", 175 }, { "hibar", 175 }, { "deg", 176 }, { "plusmn", 177 },
+    { "sup2", 178 }, { "sup3", 179 }, { "acute", 180 }, { "micro", 181 },
+    { "para", 182 }, { "middot", 183 }, { "cedil", 184 }, { "sup1", 185 },
+    { "ordm", 186 }, { "raquo", 187 }, { "frac14", 188 }, { "frac12", 189 },
+    { "frac34", 190 }, { "iquest", 191 }, { "Agrave", 192 }, { "Aacute", 193 },
+    { "Acirc", 194 }, { "Atilde", 195 }, { "Auml", 196 }, { "Aring", 197 },
+    { "AElig", 198 }, { "Ccedil", 199 }, { "Egrave", 200 }, { "Eacute", 201 },
+    { "Ecirc", 202 }, { "Euml", 203 }, { "Igrave", 204 }, { "Iacute", 205 },
+    { "Icirc", 206 }, { "Iuml", 207 }, { "ETH", 208 }, { "Dstrok", 208 },
+    { "Ntilde", 209 }, { "Ograve", 210 }, { "Oacute", 211 }, { "Ocirc", 212 },
+    { "Otilde", 213 }, { "Ouml", 214 }, { "times", 215 }, { "Oslash", 216 },
+    { "Ugrave", 217 }, { "Uacute", 218 }, { "Ucirc", 219 }, { "Uuml", 220 },
+    { "Yacute", 221 }, { "THORN", 222 }, { "szlig", 223 }, { "agrave", 224 },
+    { "aacute", 225 }, { "acirc", 226 }, { "atilde", 227 }, { "auml", 228 },
+    { "aring", 229 }, { "aelig", 230 }, { "ccedil", 231 }, { "egrave", 232 },
+    { "eacute", 233 }, { "ecirc", 234 }, { "euml", 235 }, { "igrave", 236 },
+    { "iacute", 237 }, { "icirc", 238 }, { "iuml", 239 }, { "eth", 240 },
+    { "ntilde", 241 }, { "ograve", 242 }, { "oacute", 243 }, { "ocirc", 244 },
+    { "otilde", 245 }, { "ouml", 246 }, { "divide", 247 }, { "oslash", 248 },
+    { "ugrave", 249 }, { "uacute", 250 }, { "ucirc", 251 }, { "uuml", 252 },
+    { "yacute", 253 }, { "thorn", 254 }, { "yuml", 255 },
+
+    { NULL, 0 },
+  };
+
+  string result;
+  for (string::const_iterator ii = input.begin();
+       ii != input.end();
+       ++ii) {
+    int code = (unsigned char)(*ii);
+    bool found_match = false;
+    for (int i = 0; !found_match && tokens[i].name != NULL; i++) {
+      if (code == tokens[i].code) {
+	result += '&';
+	result += tokens[i].name;
+	result += ';';
+	found_match = true;
+      }
+    }
+
+    if (!found_match) {
+      result += (*ii);
+    }
+  }
+
+  return result;
+}

+ 8 - 0
pandaapp/src/indexify/indexParameters.h

@@ -83,6 +83,9 @@ extern bool force_regenerate;
 // True to use the Rose formatting convention for roll directory names.
 extern bool format_rose;
 
+// True to sort roll directory names by date.  Useful only with -r.
+extern bool sort_date;
+
 // True to place dummy thumbnails instead of loading actual images.
 extern bool dummy_mode;
 
@@ -127,6 +130,11 @@ extern int actual_index_width;
 extern int thumb_interior_width;
 extern int thumb_interior_height;
 
+Filename compose_href(const Filename &rel_dir, const Filename &user_prefix,
+		      const Filename &basename = Filename());
+
+string escape_html(const string &input);
+
 #endif
 
 

+ 111 - 18
pandaapp/src/indexify/indexify.cxx

@@ -25,6 +25,8 @@
 #include "indexParameters.h"
 #include "string_utils.h"
 
+#include <algorithm>
+
 ////////////////////////////////////////////////////////////////////
 //     Function: Indexify::Constructor
 //       Access: Public
@@ -34,6 +36,7 @@ Indexify::
 Indexify() {
   clear_runlines();
   add_runline("[opts] roll1-dir roll2-dir [roll3-dir ...]");
+  add_runline("[opts] full/*");
 
   set_program_description
     ("This program reads a collection of directories containing photo "
@@ -47,18 +50,24 @@ Indexify() {
      "should be within the same parent directory.  Each directory is "
      "considered a \"roll\", which may or may not correspond to a physical "
      "roll of film, and the photos within each directory are grouped "
-     "correspondingly on the generated HTML pages.\n\n"
+     "correspondingly on the generated HTML pages.  One common special case "
+     "is in which all the roll directories are found in the subdirectory "
+     "named \"full\" (e.g., the second example above) or \"reduced\".  This "
+     "keeps the root directory nice and clean.\n\n"
 
      "If a file exists by the same name as an image file but with the "
      "extension \"cm\", that file is taken to be a HTML comment about that "
      "particular image and is inserted the HTML page for that image.  "
      "Similarly, if there is a file within a roll directory with the same "
      "name as the directory itself (but with the extension \"cm\"), that file "
-     "is inserted into the front page to introduce that particular roll.\n\n"
+     "is inserted into the front page to introduce that particular roll.  "
+     "Finally, a file with the name of the directory, but with the extension "
+     "\"ds\" may contain a brief one-line description of the directory, for "
+     "the toplevel index page.\n\n"
 
      "Normally, all image files with the specified extension (normally "
      "\"jpg\") within a roll directory are included in the index, and sorted "
-     "into alphabetical (or numeric) order.  If you wish to specify a "
+     "into alphabetical (or numerical) order.  If you wish to specify a "
      "different order, or use only a subset of the images in a directory, "
      "create a file in the roll directory with the same name as the "
      "directory itself, and the extension \"ls\".  This file should "
@@ -81,8 +90,8 @@ Indexify() {
 
   add_option
     ("r", "relative-dir", 0,
-     "When -a is specifies to place the generate html files in a directory "
-     "other than the one above the actual roll directories, you may need "
+     "When -a is specified to place the generated html files in a directory "
+     "other than the default, you may need "
      "to specify how the html files will address the roll directories.  This "
      "parameter specifies the relative path to the directory above the roll "
      "directories, from the directory named by -a.",
@@ -103,6 +112,12 @@ Indexify() {
      "name will be reformatted to m-yy/s for output.",
      &Indexify::dispatch_none, &format_rose);
 
+  add_option
+    ("s", "", 0,
+     "When used in conjunction with -r, requests sorting of the roll "
+     "directory names by date.",
+     &Indexify::dispatch_none, &sort_date);
+
   add_option
     ("d", "", 0,
      "Run in \"dummy\" mode; don't load any images, but instead just "
@@ -261,7 +276,8 @@ handle_args(ProgramBase::Args &args) {
     filename.standardize();
     if (filename.is_directory()) {
       string basename = filename.get_basename();
-      if (basename == "icons" || basename == "html" || basename == "reduced") {
+      if (basename == "icons" || basename == "html" || 
+	  basename == "reduced" || basename == "thumbs") {
 	nout << "Ignoring " << filename << "; indexify-generated directory.\n";
 
       } else {
@@ -287,6 +303,13 @@ handle_args(ProgramBase::Args &args) {
   return true;
 }
 
+class SortRollDirs {
+public:
+  bool operator () (const RollDirectory *a, const RollDirectory *b) const {
+    return a->sort_date_before(*b);
+  }
+};
+
 ////////////////////////////////////////////////////////////////////
 //     Function: Indexify::post_command_line
 //       Access: Protected, Virtual
@@ -307,6 +330,12 @@ post_command_line() {
   if (_archive_dir.empty()) {
     // Choose a default archive directory, above the first roll directory.
     _archive_dir = _roll_dirs.front()->get_dir().get_dirname();
+    string parent_dirname = _archive_dir.get_basename();
+    if (parent_dirname == "full" || parent_dirname == "reduced") {
+      // As a special case, if the subdirectory name is "full" or
+      // "reduced", use the directory above that.
+      _archive_dir = _archive_dir.get_dirname();
+    }
     if (_archive_dir.empty()) {
       _archive_dir = ".";
     }
@@ -317,12 +346,17 @@ post_command_line() {
     _roll_dir_root.standardize();
   }
 
+  // Sort the roll directories, if specified.
+  if (format_rose && sort_date) {
+    sort(_roll_dirs.begin(), _roll_dirs.end(), SortRollDirs());
+  }
+
   if (_front_title.empty()) {
     // Supply a default title.
     if (_roll_dirs.size() == 1) {
       _front_title = _roll_dirs.front()->get_name();
     } else {
-      _front_title = _roll_dirs.front()->get_name() + " to " + _roll_dirs.back()->get_name();
+      _front_title = _roll_dirs.front()->get_name() + " through " + _roll_dirs.back()->get_name();
     }
   }
     
@@ -479,17 +513,26 @@ run() {
   }
 
   // Then go back and generate the HTML.
+  for (di = _roll_dirs.begin(); di != _roll_dirs.end(); ++di) {
+    RollDirectory *roll_dir = (*di);
+    if (!roll_dir->generate_html(_archive_dir, _roll_dir_root)) {
+      nout << "Failure.\n";
+      exit(1);
+    }
+  }
 
-  Filename html_filename(_archive_dir, "index.htm");
-  nout << "Generating " << html_filename << "\n";
-  html_filename.set_text();
-  ofstream root_html;
-  if (!html_filename.open_write(root_html)) {
-    nout << "Unable to write to " << html_filename << "\n";
+  // Generate the complete index that browses all the roll directories
+  // at once.
+  Filename complete_filename(_archive_dir, "html/complete.htm");
+  nout << "Generating " << complete_filename << "\n";
+  complete_filename.set_text();
+  ofstream complete_html;
+  if (!complete_filename.open_write(complete_html)) {
+    nout << "Unable to write to " << complete_filename << "\n";
     exit(1);
   }
 
-  root_html
+  complete_html
     << "<html>\n"
     << "<head>\n"
     << "<title>" << _front_title << "</title>\n"
@@ -499,13 +542,63 @@ run() {
 
   for (di = _roll_dirs.begin(); di != _roll_dirs.end(); ++di) {
     RollDirectory *roll_dir = (*di);
-    if (!roll_dir->generate_html(root_html, _archive_dir, _roll_dir_root)) {
-      nout << "Failure.\n";
-      exit(1);
+    complete_html
+      << roll_dir->get_comment_html()
+      << roll_dir->get_index_html();
+  }
+
+  complete_html << "<p>\n";
+  if (!up_icon.empty()) {
+    // Use an icon to go up.
+    Filename up_icon_href = compose_href("..", up_icon);
+    complete_html 
+      << "<a href=\"../index.htm\"><img src=\"" << up_icon_href
+      << "\" alt=\"return to index\"></a>\n";
+  } else {
+    // No up icon; use text to go up.
+    complete_html
+      << "<br><a href=\"../index.htm\">Return to index</a>\n";
+  }
+  complete_html << "</p>\n";
+
+  complete_html
+    << "</body>\n"
+    << "</html>\n";
+
+  // And finally, generate the index HTML file that sits on the top of
+  // all of this.
+  Filename index_filename(_archive_dir, "index.htm");
+  nout << "Generating " << index_filename << "\n";
+  index_filename.set_text();
+  ofstream index_html;
+  if (!index_filename.open_write(index_html)) {
+    nout << "Unable to write to " << index_filename << "\n";
+    exit(1);
+  }
+
+  index_html
+    << "<html>\n"
+    << "<head>\n"
+    << "<title>" << _front_title << "</title>\n"
+    << "</head>\n"
+    << "<body>\n"
+    << "<h1>" << _front_title << "</h1>\n"
+    << "<ul>\n";
+
+  for (di = _roll_dirs.begin(); di != _roll_dirs.end(); ++di) {
+    RollDirectory *roll_dir = (*di);
+    if (!roll_dir->is_empty()) {
+      index_html
+	<< "<a name=\"" << roll_dir->get_basename() << "\">\n"
+	<< "<a href=\"html/" << roll_dir->get_basename() << ".htm\"><li>"
+	<< roll_dir->get_name() << " " << escape_html(roll_dir->get_desc())
+	<< "</li></a>\n";
     }
   }
 
-  root_html
+  index_html
+    << "</ul>\n"
+    << "<a href=\"html/complete.htm\">(complete archive)</a>\n"
     << "</body>\n"
     << "</html>\n";
 }

+ 283 - 19
pandaapp/src/indexify/rollDirectory.cxx

@@ -101,6 +101,16 @@ get_name() const {
   return _name;
 }
 
+////////////////////////////////////////////////////////////////////
+//     Function: RollDirectory::get_desc
+//       Access: Public
+//  Description: Returns the one-line description for the directory.
+////////////////////////////////////////////////////////////////////
+const string &RollDirectory::
+get_desc() const {
+  return _desc;
+}
+
 ////////////////////////////////////////////////////////////////////
 //     Function: RollDirectory::scan
 //       Access: Public
@@ -111,9 +121,47 @@ scan(const string &extension) {
   bool reverse_order = false;
   bool explicit_list = false;
 
+  // Check for a .ds file, which contains a one-line description of
+  // the contents of the directory.
+  Filename ds_filename(_basename);
+  ds_filename.set_extension("ds");
+  if (cm_search.is_empty() || !ds_filename.resolve_filename(cm_search)) {
+    // If the ds file isn't found along the search path specified
+    // via -cmdir on the command line, then look for it in the
+    // appropriate source directory.
+    ds_filename = Filename(_dir, ds_filename);
+  }
+  if (ds_filename.exists()) {
+    ds_filename.set_text();
+    ifstream ds;
+    if (!ds_filename.open_read(ds)) {
+      nout << "Could not read " << ds_filename << "\n";
+    } else {
+      // Get the words out one at a time and put just one space
+      // between them.
+      string word;
+      ds >> word;
+      while (!ds.eof() && !ds.fail()) {
+	if (!_desc.empty()) {
+	  _desc += ' ';
+	}
+	_desc += word;
+	word = string();
+	ds >> word;
+      }
+      if (!word.empty()) {
+	if (!_desc.empty()) {
+	  _desc += ' ';
+	}
+	_desc += word;
+      }
+    }
+  }
+
   // Check for an .ls file in the roll directory, which may give an
   // explicit ordering, or if empty, it specifies reverse ordering.
-  Filename ls_filename(_dir, _basename + ".ls");
+  Filename ls_filename(_dir, _basename);
+  ls_filename.set_extension("ls");
   if (ls_filename.exists()) {
     add_contributing_filename(ls_filename);
     ls_filename.set_text();
@@ -213,6 +261,51 @@ collect_index_images() {
   }
 }
 
+////////////////////////////////////////////////////////////////////
+//     Function: RollDirectory::sort_date_before
+//       Access: Public
+//  Description: Returns true if the given directory name should sort
+//               before the other one, assuming the Rose naming
+//               convention of mmyyss is in place.
+////////////////////////////////////////////////////////////////////
+bool RollDirectory::
+sort_date_before(const RollDirectory &other) const {
+  if (_name == _basename && other._name == other._basename) {
+    // If Rose naming convention is not in place in either case, sort
+    // alphabetically.
+    return _basename < other._basename;
+
+  } else if (_name == _basename) {
+    // If Rose naming convention is in place on this one and not the
+    // other, it sorts first.
+    return true;
+
+  } else if (other._name == other._basename) {
+    // And vice-versa.
+    return false;
+
+  } else {
+    // Rose naming convention holds.  Sort based on year first.  Years
+    // above 90 are deemed to belong to the previous century.
+    string yy = _basename.substr(2, 2);
+    string other_yy = other._basename.substr(2, 2);
+    int year = atoi(yy.c_str());
+    int other_year = atoi(other_yy.c_str());
+    if (year < 90) {
+      year += 100;
+    }
+    if (other_year < 90) {
+      other_year += 100;
+    }
+    if (year != other_year) {
+      return year < other_year;
+    }
+
+    // After year, sort alphabetically.
+    return _basename < other._basename;
+  }
+}
+
 ////////////////////////////////////////////////////////////////////
 //     Function: RollDirectory::get_newest_contributing_filename
 //       Access: Public
@@ -310,17 +403,18 @@ generate_images(const Filename &archive_dir, PNMTextMaker *text_maker) {
 //       Access: Public
 //  Description: Generates all appropriate HTML files for this
 //               directory, and generate the appropriate HTML code
-//               into the root_html file.
+//               into the html strings (retrieved by
+//               get_comment_html() and get_index_html()).
 ////////////////////////////////////////////////////////////////////
 bool RollDirectory::
-generate_html(ostream &root_html, const Filename &archive_dir, 
-              const Filename &roll_dir_root) {
+generate_html(const Filename &archive_dir, const Filename &roll_dir_root) {
   if (is_empty()) {
     return true;
   }
   nassertr(!_index_images.empty(), false);
 
-  root_html
+  ostringstream comment_strm;
+  comment_strm
     << "<a name=\"" << _basename << "\">\n";
 
   if (!omit_roll_headers) {
@@ -336,32 +430,122 @@ generate_html(ostream &root_html, const Filename &archive_dir,
     if (cm_filename.exists()) {
       // If the comment file for the roll exists, insert its contents
       // here instead of the generic header.
-      if (!insert_html_comment(root_html, cm_filename)) {
+      if (!insert_html_comment(comment_strm, cm_filename)) {
 	return false;
       }
       
     } else {
-      root_html
+      comment_strm
 	<< "<h2>" << _name << "</h2>\n";
+      if (!_desc.empty()) {
+	comment_strm << "<p>" << escape_html(_desc) << ".</p>\n";
+      }
     }
   }
+  _comment_html = comment_strm.str();
 
-  nout << "Generating " << Filename(archive_dir, "html/")
-       << _basename << "/*\n";
+  Filename html_dir(archive_dir, "html");
+  nout << "Generating " << Filename(html_dir, _basename) << "/*\n";
 
-  root_html << "<p>\n";
+  ostringstream index_strm;
+  index_strm << "<p>\n";
   IndexImages::iterator ii;
   for (ii = _index_images.begin(); ii != _index_images.end(); ++ii) {
     IndexImage *index_image = (*ii);
-    if (!index_image->generate_html(root_html, archive_dir, roll_dir_root)) {
+    if (!index_image->generate_html(index_strm, archive_dir, roll_dir_root)) {
       return false;
     }
   }
-  root_html << "</p>\n";
+  index_strm << "</p>\n";
+  _index_html = index_strm.str();
+
+  // Also generate the index html for this directory.
+  Filename html_filename(html_dir, _basename);
+  html_filename.set_extension("htm");
+  nout << "Generating " << html_filename << "\n";
+  html_filename.set_text();
+  ofstream index_html;
+  if (!html_filename.open_write(index_html)) {
+    nout << "Unable to write to " << html_filename << "\n";
+    exit(1);
+  }
+
+  string up_href = "../index.htm#" + _basename;
+
+  Filename prev_roll_filename;
+  Filename next_roll_filename;
+
+  if (_prev != (RollDirectory *)NULL) {
+    prev_roll_filename = _prev->_basename;
+    prev_roll_filename.set_extension("htm");
+  }
+  if (_next != (RollDirectory *)NULL) {
+    next_roll_filename = _next->_basename;
+    next_roll_filename.set_extension("htm");
+  }
 
+  index_html
+    << "<html>\n"
+    << "<head>\n";
+  if (_desc.empty()) {
+    index_html
+      << "<title>" << _name << "</title>\n";
+  } else {
+    index_html
+      << "<title>" << _name << " " << escape_html(_desc) << "</title>\n";
+  }
+  index_html
+    << "</head>\n"
+    << "<body>\n"
+    << get_comment_html();
+
+  generate_nav_buttons(index_html, prev_roll_filename, next_roll_filename,
+                       up_href);
+  index_html << get_index_html();
+  generate_nav_buttons(index_html, prev_roll_filename, next_roll_filename,
+                       up_href);
+
+  index_html
+    << "<a href=\"complete.htm#" << _basename << "\">(complete archive)</a>\n"
+    << "</body>\n"
+    << "</html>\n";
+
+  
   return true;
 }
 
+////////////////////////////////////////////////////////////////////
+//     Function: RollDirectory::get_comment_html
+//       Access: Public
+//  Description: Returns the HTML text that describes this directory's
+//               index.  This is set when generate_html() returns
+//               true.
+//
+//               This text may be inserted into the middle of a HTML
+//               page to include the imagemap that references each of
+//               the images in this directory.
+////////////////////////////////////////////////////////////////////
+const string &RollDirectory::
+get_comment_html() const {
+  return _comment_html;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: RollDirectory::get_index_html
+//       Access: Public
+//  Description: Returns the HTML text that describes this directory's
+//               index.  This is set when generate_html() returns
+//               true.
+//
+//               This text may be inserted into the middle of a HTML
+//               page to include the imagemap that references each of
+//               the images in this directory.
+////////////////////////////////////////////////////////////////////
+const string &RollDirectory::
+get_index_html() const {
+  return _index_html;
+}
+
 ////////////////////////////////////////////////////////////////////
 //     Function: RollDirectory::output
 //       Access: Public
@@ -492,23 +676,103 @@ format_basename(const string &basename) {
     return basename;
   }
 
-  // The first four characters must be digits.
-  for (size_t i = 0; i < 4; i++) {
-    if (!isdigit(basename[i])) {
-      return basename;
-    }
+  // The first two characters must be alphanumeric.
+  if (!isalnum(basename[0]) || !isalnum(basename[1])) {
+    return basename;
+  }
+
+  // The next two characters must be digits.
+  if (!isdigit(basename[2]) || !isdigit(basename[3])) {
+    return basename;
   }
 
+  // If the first two were digits as well as being alphanumeric, then
+  // we have mm-yy/sequence.  Otherwise, we just have xxyy/sequence.
+  bool mm_is_month = (isdigit(basename[0]) && isdigit(basename[1]));
+
   string mm = basename.substr(0, 2);
   string yy = basename.substr(2, 2);
   string ss = basename.substr(4);
 
-  if (mm[0] == '0') {
+  if (mm_is_month && mm[0] == '0') {
     mm = mm[1];
   }
   while (ss.length() > 1 && ss[0] == '0') {
     ss = ss.substr(1);
   }
 
-  return mm + "-" + yy + "/" + ss;
+  if (mm_is_month) {
+    return mm + "-" + yy + "/" + ss;
+  } else {
+    return mm + yy + "/" + ss;
+  }
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: RollDirectory::generate_nav_buttons
+//       Access: Private, Static
+//  Description: Outputs the HTML code to generate the next, prev,
+//               up buttons when viewing each reduced image.
+////////////////////////////////////////////////////////////////////
+void RollDirectory::
+generate_nav_buttons(ostream &html, const Filename &prev_roll_filename,
+                     const Filename &next_roll_filename, 
+                     const string &up_href) {
+  html << "<p>\n";
+
+  bool first_icons = false;
+  if (!prev_icon.empty() && !next_icon.empty()) {
+    first_icons = true;
+    // Use icons to go forward and back.
+    Filename prev_icon_href = compose_href("..", prev_icon);
+    if (prev_roll_filename.empty()) {
+      html << "<img src=\"" << prev_icon_href << "\" alt=\"No previous roll\">\n";
+    } else {
+      html << "<a href=\"" << prev_roll_filename
+           << "\"><img src=\"" << prev_icon_href << "\" alt=\"previous\"></a>\n";
+    }
+
+    Filename next_icon_href = compose_href("..", next_icon);
+    if (next_roll_filename.empty()) {
+      html << "<img src=\"" << next_icon_href << "\" alt=\"No next roll\">\n";
+    } else {
+      html << "<a href=\"" << next_roll_filename
+           << "\"><img src=\"" << next_icon_href << "\" alt=\"next\"></a>\n";
+    }
+
+  } else {
+    // No prev/next icons; use text to go forward and back.
+    if (prev_roll_filename.empty()) {
+      html << "(This is the first roll.)\n";
+    } else {
+      html << "<a href=\"" << prev_roll_filename
+           << "\">Back to previous roll</a>\n";
+    }
+    
+    if (next_roll_filename.empty()) {
+      html << "<br>(This is the last roll.)\n";
+    } else {
+      html << "<br><a href=\"" << next_roll_filename
+           << "\">On to next roll</a>\n";
+    }
+  }
+
+  if (!up_href.empty()) {
+    if (!up_icon.empty()) {
+      // Use an icon to go up.
+      if (!first_icons) {
+        html << "<br>";
+      } else {
+        html << "&nbsp;&nbsp;&nbsp;";
+      }
+      Filename up_icon_href = compose_href("..", up_icon);
+      html << "<a href=\"" << up_href
+           << "\"><img src=\"" << up_icon_href << "\" alt=\"return to index\"></a>\n";
+    } else {
+      // No up icon; use text to go up.
+      html << "<br><a href=\"" << up_href
+           << "\">Return to index</a>\n";
+    }
+  }
+  html << "</p>\n";
 }

+ 15 - 2
pandaapp/src/indexify/rollDirectory.h

@@ -41,9 +41,12 @@ public:
   const Filename &get_dir() const;
   const string &get_basename() const;
   const string &get_name() const;
+  const string &get_desc() const;
   bool scan(const string &extension);
   void collect_index_images();
 
+  bool sort_date_before(const RollDirectory &other) const;
+
   const Filename &get_newest_contributing_filename() const;
 
   bool is_empty() const;
@@ -54,8 +57,10 @@ public:
   IndexImage *get_index_image(int n) const;
 
   bool generate_images(const Filename &archive_dir, PNMTextMaker *text_maker);
-  bool generate_html(ostream &root_html, const Filename &archive_dir,
-                     const Filename &roll_dir_root);
+  bool generate_html(const Filename &archive_dir,
+		     const Filename &roll_dir_root);
+  const string &get_comment_html() const;
+  const string &get_index_html() const;
 
   void output(ostream &out) const;
   void write(ostream &out, int indent_level) const;
@@ -66,6 +71,10 @@ private:
   void add_contributing_filename(const Filename &filename);
   static bool insert_html_comment_body(ostream &html, istream &cm);
   static string format_basename(const string &basename);
+  void generate_nav_buttons(ostream &html, 
+			    const Filename &prev_roll_filename,
+			    const Filename &next_roll_filename, 
+			    const string &up_href);
 
 public:
   RollDirectory *_prev;
@@ -75,6 +84,7 @@ private:
   Filename _dir;
   string _basename;
   string _name;
+  string _desc;
   typedef pvector<Photo *> Photos;
   Photos _photos;
   
@@ -82,6 +92,9 @@ private:
 
   typedef pvector<IndexImage *> IndexImages;
   IndexImages _index_images;
+
+  string _comment_html;
+  string _index_html;
 };
 
 INLINE ostream &operator << (ostream &out, const RollDirectory &d) {