Check that all binaries needed are installed before running
[project-aon.git] / common / scripts / gbtoepub.pl
1 #!/usr/bin/perl
2
3 use strict;
4 use warnings;
5
6 use File::Path qw(mkpath);
7
8 my $PROGRAM_NAME    = 'gbtoepub';
9 my $USAGE           = "$PROGRAM_NAME [options] book-code\n\t--meta=[metadata file]\n\t--xml=[book XML]\n\t--epub-xsl=[XSL transformation]\n\t--language=[language area of input data (output determined by meta file)]\n\t--font-files=[font-files]\n\t--no-validate\n\t--verbose\n";
10
11 my $FILENAME_SEPARATOR = '/';
12
13 my $RXP        = '/usr/bin/rxp';
14 my $CP         = '/bin/cp';
15 my $MV         = '/bin/mv';
16 my $TAR        = '/bin/tar';
17 my $ZIP        = '/usr/bin/zip';
18 my $BZIP2      = '/bin/bzip2';
19 my $JAVA       = '/usr/bin/java';
20 my $XALAN_JAR  = '/usr/share/java/xalan2.jar';
21 my $RM         = '/bin/rm';
22 my $CHMOD      = '/bin/chmod';
23
24 # Check that all the binaries are were want them
25
26 my @BINARIES;
27 push @BINARIES, ($RXP, $CP, $MV, $TAR, $ZIP, $BZIP2, $JAVA, $XALAN_JAR, $RM, $CHMOD);
28
29 foreach (@BINARIES) {
30     if ( ! -e $_ ) {
31         die "$PROGRAM_NAME: Cannot find binary '".$_."'. Please install it.\n";
32     }
33 }
34
35 ###
36
37 my $EPUB_MIMETYPE  = 'application/epub+zip';
38 my $MIMETYPE_FILE  = 'mimetype';
39 my $CONTAINER_FILE = 'container.xml';
40 my $OEBPS_DIR      = 'OEBPS';
41 my $META_INF_DIR   = 'META-INF';
42 my $NCX_FILE       = 'toc.ncx';
43 my $XHTML_EXT      = 'html';
44
45 my $PROJECT_AON_URI = 'http://www.projectaon.org';
46
47
48 ###
49
50 my $bookCode     = '';
51 my $metaFile     = '';
52 my $bookXML      = '';
53 my $ncxXSL       = 'common/xsl/epub-ncx.xsl';
54 my $epubXSL      = 'common/xsl/epub-xhtml.xsl';
55 my $metadataXSL  = 'common/xsl/epub-opf-metadata.xsl';
56 my $spineXSL     = 'common/xsl/epub-opf-spine.xsl';
57 my $coverXSL     = 'common/xsl/epub-coverpage.xsl';
58 my $fontFiles    = "$ENV{'HOME'}${FILENAME_SEPARATOR}souvenir";
59 my $language     = 'en';
60
61 my $verbose = 0;
62 my $noValidate = 0;
63
64 ### read command line options
65
66 while( $#ARGV > -1 ) {
67     my $cmdLineItem = shift @ARGV;
68     if( $cmdLineItem =~ /^--meta=(.+)$/ ) {
69         $metaFile = $1;
70     }
71     elsif( $cmdLineItem =~ /^--xml=(.+)$/ ) {
72         $bookXML = $1;
73     }
74     elsif( $cmdLineItem =~ /^--epub-xsl=(.+)$/ ) {
75         $epubXSL = $1;
76     }
77     elsif( $cmdLineItem =~ /^--language=(.+)$/ ) {
78         $language = $1;
79     }
80     elsif( $cmdLineItem =~ /^--verbose/ ) {
81         $verbose = 1;
82     }
83     elsif( $cmdLineItem =~ /^--no-validate/ ) {
84         $noValidate = 1;
85     }
86     elsif( $cmdLineItem =~ /^--font-files=(.+)$/ ) {
87         $fontFiles = $1;
88     }
89     else { 
90         $bookCode = $cmdLineItem;
91     }
92 }
93
94 if( $bookCode eq '' ) { 
95     die "$PROGRAM_NAME: Unspecified book code\n$USAGE";
96 }
97 if( $metaFile eq '' ) { $metaFile = "$language/.publisher/rules/epub"; }
98 if( $bookXML eq '' ) { $bookXML = "$language/xml/$bookCode.xml"; }
99 if( $epubXSL eq '' ) {
100     die "$PROGRAM_NAME: Unspecified XSL transformation file\n$USAGE";
101 }
102
103 ### validate book XML
104
105 if( (not $noValidate) && -e $RXP ) {
106     system( $RXP, '-Vs', $bookXML ) == 0
107         or die "$PROGRAM_NAME: XML validation failed\n";
108 }
109 elsif( $noValidate ) {
110     warn "$PROGRAM_NAME: XML validation skipped - validate before publication\n";
111 }
112 else {
113     warn "$PROGRAM_NAME: XML validator not installed - validate before publication\n";
114 }
115
116 ### read in metadata file
117
118 unless( -e $metaFile && -f $metaFile && -r $metaFile ) {
119     die qq{$PROGRAM_NAME: Improper metadata file "$metaFile"\n};
120 }
121
122 open( META, '<', $metaFile ) or 
123     die qq{$PROGRAM_NAME: Unable to open metadata file "$metaFile": $!\n};
124
125 my $meta = '';
126 while( my $line = <META> ) {
127     $meta .= $line if $line !~ /^[[:space:]]*#/;
128 }
129 close META;
130
131 ### interpret rules from metadata
132 my $rulesString = '';
133 if( $meta =~ /^[[:space:]]*$bookCode[[:space:]]*{([^}]*)}/sm ) {
134     $rulesString = $1;
135 }
136 else {
137     die "$PROGRAM_NAME: Book code ($bookCode) not found in metadata file or invalid file syntax\n";
138 }
139
140 my @rules = split( /[[:space:]\n]*;[[:space:]\n]*/, $rulesString );
141 my %rulesHash;
142 foreach my $rule (@rules) {
143     if( $rule =~ /[[:space:]]*([^:]+)[[:space:]]*:[[:space:]]*(.+)$/s ) {
144         $rulesHash{ $1 } = $2;
145     }
146     else {
147         die "$PROGRAM_NAME: Unrecognized rule syntax:\n$rule\n";
148     }
149 }
150
151 unless( defined $rulesHash{'book-series'} ) {
152     die "$PROGRAM_NAME: no book series set\n";
153 }
154 unless( defined $rulesHash{'csst'} ) {
155     die "$PROGRAM_NAME: metadata file leaves CSS templates unspecified\n";
156 }
157
158 my $SERIES = get_series($rulesHash{'book-series'}) ;
159 my $SERIES_NUMBER = get_series_number($bookCode);
160
161
162 ### create output directories
163
164 my %outPath;
165 $outPath{'top'} = $rulesHash{'language'} . $FILENAME_SEPARATOR .
166                      'epub' . $FILENAME_SEPARATOR .
167                      $rulesHash{'book-series'} . $FILENAME_SEPARATOR .
168                      $bookCode;
169 # clear old files
170 if( -e "$outPath{'top'}$FILENAME_SEPARATOR$MIMETYPE_FILE" ) {
171     print qx{$RM -r $outPath{'top'}$FILENAME_SEPARATOR*};
172 }
173
174 $outPath{'meta-inf'} = $outPath{'top'} . $FILENAME_SEPARATOR . $META_INF_DIR;
175 $outPath{'oebps'} = $outPath{'top'} . $FILENAME_SEPARATOR . $OEBPS_DIR;
176
177 foreach my $directory (keys(%outPath)) {
178     unless( -e $outPath{$directory} && -d $outPath{$directory} ) {
179         mkpath $outPath{$directory}
180             or die "$PROGRAM_NAME: Unknown error creating output directory " .
181                    "\"$outPath{$directory}\"\n";
182     }
183 }
184
185 ### create content files
186
187 # the location of this tempfile also controls where the xhtml will go
188 my $tempFile = "$outPath{'oebps'}${FILENAME_SEPARATOR}foo.xml";
189 print qx{$JAVA -classpath "$XALAN_JAR" org.apache.xalan.xslt.Process -IN "$bookXML" -XSL "$epubXSL" -OUT "$tempFile" -PARAM xhtml-ext ".$XHTML_EXT" -PARAM use-illustrators "$rulesHash{'use-illustrators'}" -PARAM language "$rulesHash{'language'}"}; #" <- comment to unconfuse VIM syntax hilighting (ugh)
190 print qx{$RM $tempFile};
191
192 foreach my $imagePath (split( /:/, $rulesHash{'images'} )) {
193     unless( -e $imagePath && -d $imagePath ) {
194     die "$PROGRAM_NAME: Image path ($imagePath) does not exist or is not a directory\n";
195 }
196     print qx{$CP $imagePath${FILENAME_SEPARATOR}* $outPath{'oebps'}};
197 }
198
199 ### create the CSS stylsheet
200
201 foreach my $cssTemplate (split( /:/, $rulesHash{'csst'} )) {
202     $cssTemplate =~ m/([^${FILENAME_SEPARATOR}]+)t$/;
203     my $templateFile = $1;
204     open( TEMPLATE, '<', $cssTemplate ) 
205         or die "$PROGRAM_NAME: Unable to open CSS template file ($cssTemplate): $!\n";
206
207     my $stylesFile = "$outPath{'oebps'}$FILENAME_SEPARATOR$templateFile";
208     open( STYLESHEET, '>', $stylesFile ) 
209         or die "$PROGRAM_NAME: Unable to open stylesheet file ($stylesFile) for writing: $!\n";
210
211     while( my $templateLine = <TEMPLATE> ) {
212         while( $templateLine =~ /%%([^%[:space:]]+)%%/ ) {
213             my $name = $1;
214             $templateLine =~ s/%%${name}%%/$rulesHash{$name}/g;
215         }
216         print STYLESHEET $templateLine;
217     }
218     close STYLESHEET;
219     close TEMPLATE;
220 }
221
222 ### copy the font files
223
224 unless( -e $fontFiles && -d $fontFiles ) {
225     die "$PROGRAM_NAME: font files directory does not exist or is not a directory \"$fontFiles\": $!\n";
226 }
227 print qx{$CP $fontFiles${FILENAME_SEPARATOR}*.otf $outPath{'oebps'}};
228
229 ### write NCX file
230
231 my $uniqueID = "opf-$bookCode";
232 my $bookUniqueURI = "$PROJECT_AON_URI/$language/epub/" .
233                     "$rulesHash{'book-series'}/$bookCode/";
234
235 my $ncxFile = $outPath{'oebps'} . $FILENAME_SEPARATOR . $NCX_FILE;
236 open( NCXFILE, '>', $ncxFile ) or
237     die "$PROGRAM_NAME: unable to open NCX file for writing " .
238         "\"$ncxFile\"\n";
239 print NCXFILE qx{$JAVA -classpath "$XALAN_JAR" org.apache.xalan.xslt.Process -IN "$bookXML" -XSL "$ncxXSL" -PARAM xhtml-ext ".$XHTML_EXT" -PARAM unique-identifier "$bookUniqueURI" -PARAM language "$rulesHash{'language'}"}; #" comment to unconfuse VIM syntax highlighting
240 close NCXFILE;
241
242 ### write mimetype file
243
244 my $mimeFile = $outPath{'top'} . $FILENAME_SEPARATOR . $MIMETYPE_FILE;
245 open( MIMETYPE, '>', $mimeFile ) or
246     die "$PROGRAM_NAME: unable to open mimetype file for writing " .
247         "\"$mimeFile\"\n";
248 print MIMETYPE $EPUB_MIMETYPE;
249 close MIMETYPE;
250
251
252 ## write coverpage 
253
254 # Generate the cover image. This can be done in two ways:
255 # 1.- A file is available under the directory of JPEG files for the book
256 # 2.- A file is generated using imagemagick
257
258 # Cover filename
259 my $coverImage = $outPath{'oebps'} . $FILENAME_SEPARATOR . "cover.jpg"; 
260 # Cover filename generated by Project Aon
261 my $pa_coverImage = $rulesHash{'language'} . $FILENAME_SEPARATOR . "jpeg" . $FILENAME_SEPARATOR .$rulesHash{'book-series'}. $FILENAME_SEPARATOR .$bookCode . $FILENAME_SEPARATOR . "cover.jpg";
262
263 if ( -e  "$pa_coverImage") {
264     # Copy the file here
265     print STDERR "DEBUG: Using cover from $pa_coverImage\n" if $verbose;
266     system "cp $pa_coverImage $coverImage";
267 } else {
268
269 # Use Imagemagick if available
270     if ( -x "/usr/bin/convert" ) {
271
272         print STDERR "DEBUG: Will generate cover with ImageMagick\n" if $verbose;
273         my $TITLE = quote_shell(find_title($bookXML));
274         my $AUTHOR = quote_shell(find_author($bookXML));
275         my $ILLUSTRATOR = quote_shell(find_illustrator($bookXML));
276         my $convert_cmd = "";
277
278         if ( -e "$fontFiles/SouvenirStd-Demi.otf" && -e "$fontFiles/SouvenirStd-Light.otf" ) { 
279             $convert_cmd="convert -size 600x800 -background white  -font $fontFiles/SouvenirStd-Demi.otf -pointsize 32 -fill '#006633' -gravity north caption:\"\" -annotate +0+218 \"$TITLE\"  -font $fontFiles/SouvenirStd-Light.otf -pointsize 22 -fill black -annotate +0+304 '$AUTHOR' -annotate +0+333 '$ILLUSTRATOR' $coverImage"
280         } else {
281             print STDERR "WARN: Fontfiles not found, using standard font\n";
282             $convert_cmd="convert -size 600x800 -background white -pointsize 32 -fill '#006633' -gravity north caption:\"\" -annotate +0+218 \"$TITLE\"  -pointsize 22 -fill black -annotate +0+304 '$AUTHOR' -annotate +0+333 '$ILLUSTRATOR' $coverImage";
283         }
284
285         print STDERR "DEBUG: Will run '$convert_cmd'\n" if $verbose;
286         system $convert_cmd;
287     }
288 }
289
290 # If there is no coverImage then we will change the XSL output
291 my $addCover = "yes"; 
292 $addCover = "no" if ! -e $coverImage;
293
294
295 #
296 #  TODO: The coverpage seems to be stripped by Calibre since it 
297 #  expects an image file for the coverpage. Alternatively, create
298 #  a jpeg file based on the HTML (using html2ps for example) 
299 #  and use that for the OPF description of the book
300
301 my $coverFileName = "coverpage.html";
302 my $coverFile = "$outPath{'oebps'}$FILENAME_SEPARATOR$coverFileName";
303 open( COVER, '>', $coverFile ) or
304     die "$PROGRAM_NAME: unable to open OPF file for writing " .
305         "\"$coverFile\"\n";
306
307 my $cover  = qx{$JAVA -classpath "$XALAN_JAR" org.apache.xalan.xslt.Process -IN "$bookXML" -XSL "$coverXSL" -PARAM opf-id "$uniqueID" -PARAM unique-identifier "$bookUniqueURI" -PARAM language "$rulesHash{'language'}" -PARAM book_series "$SERIES" -PARAM book_series_index "$SERIES_NUMBER" -PARAM addcover "$addCover"}; #" comment to unconfuse VIM syntax hilighting
308
309
310 print COVER "$cover";
311
312 close COVER;
313
314 check_file($coverFileName);
315
316 ### write OPF Root file
317 # All content files must be created prior to creating the OPF root file
318 # with its manifest of content files.
319
320 my $opfFileName = "$bookCode.opf";
321 my $opfFile = "$outPath{'oebps'}$FILENAME_SEPARATOR$opfFileName";
322 open( OPF, '>', $opfFile ) or
323     die "$PROGRAM_NAME: unable to open OPF file for writing " .
324         "\"$opfFile\"\n";
325
326 print OPF <<END_OPF_HEADER;
327 <?xml version="1.0"?>
328 <package version="2.0" 
329          xmlns="http://www.idpf.org/2007/opf" 
330          unique-identifier="$uniqueID">
331
332 END_OPF_HEADER
333
334 ## write metadata
335
336 my $metadata = qx{$JAVA -classpath "$XALAN_JAR" org.apache.xalan.xslt.Process -IN "$bookXML" -XSL "$metadataXSL" -PARAM opf-id "$uniqueID" -PARAM unique-identifier "$bookUniqueURI" -PARAM language "$rulesHash{'language'}" -PARAM book_series "$SERIES" -PARAM book_series_index "$SERIES_NUMBER" -PARAM addcover "$addCover"}; #" comment to unconfuse VIM syntax hilighting
337 $metadata = " $metadata";
338 $metadata =~ s|(<dc:)|\n  $1|g;
339 $metadata =~ s|(</metadata>)|\n $1|g;
340
341 print OPF "$metadata\n\n";
342
343 ## write manifest data
344 # assuming a flat directory structure within the OEBPS directory
345
346 print OPF " <manifest>\n";
347
348 opendir( my $content_dir, $outPath{'oebps'} )
349     or die "$PROGRAM_NAME: unable to read OEBPS directory " .
350            "\"$outPath{'oebps'}\": $!\n";
351
352 while( my $content_file = readdir $content_dir ) {
353     next if $content_file eq '.';
354     next if $content_file eq '..';
355     next if $content_file =~ m/\.opf$/i;
356     print OPF qq{  <item id="};
357     print OPF make_id( $content_file );
358     print OPF qq{" href="$content_file" media-type="};
359     print OPF get_mime_type( $content_file );
360     print OPF qq{"/>\n};
361 }
362
363 closedir $content_dir;
364
365 print OPF " </manifest>\n\n";
366
367 ## write spine data
368
369 my $ncxID = make_id( $NCX_FILE );
370 #print OPF qx{$JAVA -classpath "$XALAN_JAR" org.apache.xalan.xslt.Process -IN "$bookXML" -XSL "$spineXSL" -PARAM toc-id "$ncxID"};
371 my $spine = qx{$JAVA -classpath "$XALAN_JAR" org.apache.xalan.xslt.Process -IN "$bookXML" -XSL "$spineXSL" -PARAM toc-id "$ncxID" -PARAM addcover "$addCover"};
372
373 $spine =~ s/idref="([^"]*)"/idref="$1.$XHTML_EXT"/g;
374 $spine = " $spine";
375 $spine =~ s|(<itemref)|\n  $1|g;
376 $spine =~ s|(</spine>)|\n $1|g;
377
378 print OPF "$spine\n\n";
379
380 # TODO: write (optional) guide data here
381 #print OPF " <guide>\n";
382 #print OPF " </guide>\n</package>";
383
384 print OPF "</package>";
385 close OPF;
386
387
388 ### write container.xml
389
390 my $containerFile = "$outPath{'meta-inf'}$FILENAME_SEPARATOR$CONTAINER_FILE";
391 open( CONTAINER, '>', $containerFile ) or
392     die "$PROGRAM_NAME: unable to open container file for writing " .
393         "\"$containerFile\"\n";
394 print CONTAINER <<END_CONTAINER;
395 <?xml version="1.0"?>
396 <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
397   <rootfiles>
398     <rootfile full-path="$OEBPS_DIR/$opfFileName"
399      media-type="application/oebps-package+xml" />
400   </rootfiles>
401 </container>
402 END_CONTAINER
403 close CONTAINER;
404
405 ### compress epub contents
406 # complies with Open Container Format 2.0.1
407 # http://idpf.org/epub/20/spec/OCF_2.0.1_draft.doc
408
409 chdir $outPath{'top'};
410
411 system( $ZIP, '-0Xq', "$bookCode.epub", $MIMETYPE_FILE );
412 system( $ZIP, '-rq', "$bookCode.epub", $META_INF_DIR );
413 system( $ZIP, '-rq', "$bookCode.epub", $OEBPS_DIR );
414
415 exit 0;
416
417 ################################################################################
418 # Subroutines
419 ################################################################################
420
421 sub make_id {
422     my ( $name ) = ( @_ );
423     $name = "_$name" if( $name =~ m/^[-.0-9]/ );
424     $name =~ tr/\x80-\xff/_/;
425     return $name
426 }
427
428 sub get_mime_type {
429     # relies on valid file name extensions
430
431     my ( $file ) = ( @_ );
432     if( $file =~ m/\.x?html?$/i ) {
433         return 'application/xhtml+xml';
434     }
435     elsif( $file =~ m/\.css$/i ) {
436         return 'text/css';
437     }
438     elsif( $file =~ m/\.png$/i ) {
439         return 'image/png';
440     }
441     elsif( $file =~ m/\.jpe?g$/i ) {
442         return 'image/jpeg';
443     }
444     elsif( $file =~ m/\.svg$/i ) {
445         return 'image/svg+xml';
446     }
447     elsif( $file =~ m/\.gif$/i ) {
448         return 'image/gif';
449     }
450     elsif( $file =~ m/\.ncx$/i ) {
451         return 'application/x-dtbncx+xml';
452     }
453     elsif( $file =~ m/\.otf$/i ) {
454         return 'application/x-font-opentype';
455     }
456     else {
457         return 'application/x-unrecognized-mime';
458     }
459 }
460
461 sub check_file {
462 # Check if a file is empty, if it is, abort as this is an indication
463 # that the previous processing step went wrong
464     my ($file) = @_;
465
466     if ( -z $file ) {
467         print  STDERR "There was an error generating $file (empty file produced)";
468         exit 1;
469     }
470
471     return 0;
472 }
473
474 #unless( $bookXML =~ m{^([-\w\@./]+)$} ) {
475 #    die "$PROGRAM_NAME: bad book XML filename \"$bookXML\"\n";
476 #}
477 #$bookXML = $1;
478 #
479 #unless( -e $bookXML && -f $bookXML && -r $bookXML ) {
480 #    die "$PROGRAM_NAME: XML does not exist or is not readable \"$bookXML\"\n";
481 #}
482 #
483 #if( -e $RXP ) {
484 #    system( $RXP, '-Vs', $bookXML ) == 0
485 #        or die "$PROGRAM_NAME: XML validation failed\n";
486 #}
487 #else {
488 #    warn "$PROGRAM_NAME: XML Validator not installed - validate before publication\n";
489 #}
490 #
491 #unless( defined $rulesHash{'language'} ) { die "$PROGRAM_NAME: Metadata file leaves language unspecified\n"; }
492 #unless( defined $rulesHash{'book-series'} ) { die "$PROGRAM_NAME: Metadata file leaves book series unspecified\n"; }
493 #unless( defined $rulesHash{'images'} ) { die "$PROGRAM_NAME: Metadata file leaves image directories unspecified\n"; }
494 #unless( defined $rulesHash{'csst'} ) { die "$PROGRAM_NAME: Metadata file leaves CSS templates unspecified\n"; }
495 #
496 #
497 #my $bookPath = "$outPath${FILENAME_SEPARATOR}";
498 #print qx{$RM ${bookPath}*} if -e $bookPath."/toc.htm";
499 #print qx{$JAVA -classpath "$XALAN_JAR" org.apache.xalan.xslt.Process -IN "$bookXML" -XSL "$xhtmlXSL" -OUT "${bookPath}foo.xml" -PARAM background-color "$rulesHash{'background-color'}" -PARAM text-color "$rulesHash{'text-color'}" -PARAM link-color "$rulesHash{'link-color'}" -PARAM use-illustrators "$rulesHash{'use-illustrators'}" -PARAM language "$rulesHash{'language'}"};
500 #print qx{$RM ${bookPath}foo.xml};
501 #
502 #
503 #
504 #print qx{$ZIP -8 -q ${bookCode}.zip ${bookPath}*};
505 #print qx{$MV ${bookCode}* $bookPath};
506 #
507 #print "Success\n" if $verbose;
508
509 # Determine series long name by the series acronym
510 sub get_series {
511     my ($series) = @_;
512     my $series_name = "";
513     if ($series eq "lw" ) {
514         $series_name = "Lone Wolf";
515     } elsif ($series eq "ls" ) {
516         $series_name = "Lobo Solitario";
517     } elsif ($series eq "gs" ) {
518         $series_name = "Grey Star the Wizard";
519     } elsif ($series eq "fw" ) {
520         $series_name = "Freeway Warrior";
521     } else {
522         print STDERR "WARN: Undefined series. Short name given: '$series'\n";
523         $series_name = "[undefined]";
524     }
525     return $series_name;
526 }
527
528 # Determine the series number based on book code
529 sub get_series_number {
530     my ($bookCode) = @_;
531     my $series_number = "";
532     if ( $bookCode =~ /^(\d\d)/ ) {
533         $series_number = $1;
534     } else {
535         print STDERR "WARN: Undefined series number. Book code is '$bookCode'.\n";
536         $series_number = "xx";
537     }
538     return $series_number;
539 }
540
541 # Determine the book title by reading the book meta information
542 sub find_title {
543     my ($book) = @_;
544     my $title = ""; my $line = "";
545     open (BOOK, "head -100 $book | ") || die ("Could not read $book: $!");
546     while ($title eq "" && ( $line = <BOOK> ) ) {
547         chomp $line;
548         if ( $line =~ /<title>(.*?)<\/title>/ ) {
549             $title = $1;
550         }
551     }
552     close BOOK;
553
554     if ( $title eq "" ) {
555         print STDERR "WARN: Cannot find title for book '$book'\n";
556         $title = "[Undefined]";
557     }
558
559     return convert_entities($title);
560 }
561
562 # Determine the book author by reading the book meta information
563 sub find_author {
564     my ($book) = @_;
565     my $author = ""; 
566     my $line = "";
567     open (BOOK, "head -100 $book |") || die ("Could not read $book: $!");
568
569     my $find_line = 0;
570     while ($author eq "" && ( $line = <BOOK> ) ) {
571         chomp $line;
572         if ( $find_line == 1 && $line =~ /<line>(.*?)<\/line>/ ) {
573             $author = $1;
574         }
575         $find_line = 1 if ( $line =~ /<creator class="medium">/ );
576         $find_line = 0 if ( $line =~ /<\/creator>/ );
577         if ( $line =~ /<creator class="author">(.*?)<\/title>/ ) {
578             $author = $1;
579         }
580     }
581     close BOOK;
582
583     if ( $author eq "" ) {
584         print STDERR "WARN: Cannot find author for book '$book'\n";
585         $author = "[Undefined]";
586     }
587
588
589     return $author;
590 }
591
592 # Determine the book illustrator by reading the book meta information
593 sub find_illustrator {
594     my ($book) = @_;
595     my $illustrator = "";
596     my $line = "";
597     open (BOOK, "head -100 $book | ") || die ("Could not read $book: $!");
598
599     my $find_line = 0;
600     while ($illustrator eq "" && ( $line = <BOOK> ) ) {
601         chomp $line;
602         if ( $find_line == 1 && $line =~ /<line>Illustrated by (.*?)<\/line>/ ) {
603             $illustrator = $1;
604         }
605         $find_line = 1 if ( $line =~ /<creator class="medium">/ );
606         $find_line = 0 if ( $line =~ /<\/creator>/ );
607         if ( $line =~ /<creator class="illustrator">(.*?)<\/title>/ ) {
608             $illustrator = $1;
609         }
610     }
611     close BOOK;
612
613     if ( $illustrator eq "" ) {
614         print STDERR "WARN: Cannot find illustrator for book '$book'\n";
615         $illustrator = "[Undefined]";
616     }
617     if ( $language eq "en" ) {
618         $illustrator = "Illustrated by ".$illustrator;
619     } elsif ( $language eq "es" ) {
620         $illustrator = "Illustrado por ".$illustrator;
621     }
622
623     return $illustrator;
624 }
625
626 sub convert_entities {
627 # Convert character entities to their correspondent values
628     my ($text) = @_;
629
630     $text =~ s/\<ch.apos\/\>/'/g; 
631     $text =~ s/\<ch.nbsp\/\>/ /g;
632     $text =~ s/\<ch.plusmn\/\>/+-/g;
633     $text =~ s/\<ch.aacute\/\>/á/g;
634     $text =~ s/\<ch.eacute\/\>/é/g;
635     $text =~ s/\<ch.iacute\/\>/í/g;
636     $text =~ s/\<ch.oacute\/\>/ó/g;
637     $text =~ s/\<ch.uacute\/\>/ú/g;
638     $text =~ s/\<ch.ntilde\/\>/ñ/g;
639     $text =~ s/\<ch.Aacute\/\>/Á/g;
640     $text =~ s/\<ch.Eacute\/\>/É/g;
641     $text =~ s/\<ch.Iacute\/\>/Í/g;
642     $text =~ s/\<ch.Oacute\/\>/Ó/g;
643     $text =~ s/\<ch.Uacute\/\>/Ú/g;
644     $text =~ s/\<ch.auml\/\>/ä/g;
645     $text =~ s/\<ch.euml\/\>/ë/g;
646     $text =~ s/\<ch.iuml\/\>/ï/g;
647     $text =~ s/\<ch.ouml\/\>/ö/g;
648     $text =~ s/\<ch.uuml\/\>/ü/g;
649     $text =~ s/\<ch.Ntilde\/\>/Ñ/g;
650     $text =~ s/\<ch.acute\/\>/´/g;
651     $text =~ s/\<ch.iexcl\/\>/¡/g;
652     $text =~ s/\<ch.iquest\/\>/¿/g;
653     $text =~ s/\<ch.laquo\/\>/«/g;
654     $text =~ s/\<ch.raquo\/\>/»/g;
655     $text =~ s/\<ch.ampersand\/\>/&/g;
656
657     return $text;
658 }
659
660 # Quote metacaracters for shell use
661 sub quote_shell {
662     my ($text) = @_;
663     $text =~ s/'/\\'/g; 
664     return $text;
665 }