Remove functions which are not used in the script, these are actually part of the...
[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        = qx{which rxp};
14 my $CP         = qx{which cp};
15 my $MV         = qx{which mv};
16 my $TAR        = qx{which tar};
17 my $ZIP        = qx{which zip};
18 my $BZIP2      = qx{which bzip2};
19 my $JAVA       = qx{which java};
20 # Note: In Debian/Ubuntu the Xalan processor is provided in the package libxalan2-java
21 my $XALAN_JAR  = '/usr/share/java/xalan2.jar';
22 # Old version of Xalan processor (Xalan 1)
23 #my $XALAN_JAR  = '/usr/share/ant/lib/xalan.jar';
24 my $RM         = qx{which rm};
25 my $CHMOD      = qx{which chmod};
26
27 chomp $RXP;
28 chomp $CP;
29 chomp $MV;
30 chomp $TAR;
31 chomp $ZIP;
32 chomp $BZIP2;
33 chomp $JAVA;
34 chomp $RM;
35 chomp $CHMOD;
36
37 # Check that all the binaries are were want them
38
39 my @BINARIES;
40 push @BINARIES, ($RXP, $CP, $MV, $TAR, $ZIP, $BZIP2, $JAVA, $XALAN_JAR, $RM, $CHMOD);
41
42 foreach (@BINARIES) {
43     if ( ! -e $_ ) {
44         die "$PROGRAM_NAME: Cannot find binary '".$_."'. Please install it.\n";
45     }
46 }
47
48 ###
49
50 my $EPUB_MIMETYPE  = 'application/epub+zip';
51 my $MIMETYPE_FILE  = 'mimetype';
52 my $CONTAINER_FILE = 'container.xml';
53 my $OEBPS_DIR      = 'OEBPS';
54 my $META_INF_DIR   = 'META-INF';
55 my $NCX_FILE       = 'toc.ncx';
56 my $XHTML_EXT      = 'html';
57
58 my $PROJECT_AON_URI = 'http://www.projectaon.org';
59
60
61 ###
62
63 my $bookCode     = '';
64 my $metaFile     = '';
65 my $bookXML      = '';
66 my $ncxXSL       = 'common/xsl/epub-ncx.xsl';
67 my $epubXSL      = 'common/xsl/epub-xhtml.xsl';
68 my $metadataXSL  = 'common/xsl/epub-opf-metadata.xsl';
69 my $spineXSL     = 'common/xsl/epub-opf-spine.xsl';
70 my $fontFiles    = "common/fonts";
71 my $language     = 'en';
72
73 my $verbose = 0;
74 my $noValidate = 0;
75
76 ### read command line options
77
78 while( $#ARGV > -1 ) {
79     my $cmdLineItem = shift @ARGV;
80     if( $cmdLineItem =~ /^--meta=(.+)$/ ) {
81         $metaFile = $1;
82     }
83     elsif( $cmdLineItem =~ /^--xml=(.+)$/ ) {
84         $bookXML = $1;
85     }
86     elsif( $cmdLineItem =~ /^--epub-xsl=(.+)$/ ) {
87         $epubXSL = $1;
88     }
89     elsif( $cmdLineItem =~ /^--language=(.+)$/ ) {
90         $language = $1;
91     }
92     elsif( $cmdLineItem =~ /^--verbose/ ) {
93         $verbose = 1;
94     }
95     elsif( $cmdLineItem =~ /^--no-validate/ ) {
96         $noValidate = 1;
97     }
98     elsif( $cmdLineItem =~ /^--font-files=(.+)$/ ) {
99         $fontFiles = $1;
100     }
101     else { 
102         $bookCode = $cmdLineItem;
103     }
104 }
105
106 if( $bookCode eq '' ) { 
107     die "$PROGRAM_NAME: Unspecified book code\n$USAGE";
108 }
109 if( $metaFile eq '' ) { $metaFile = "$language/.publisher/rules/epub"; }
110 if( $bookXML eq '' ) { $bookXML = "$language/xml/$bookCode.xml"; }
111 if( $epubXSL eq '' ) {
112     die "$PROGRAM_NAME: Unspecified XSL transformation file\n$USAGE";
113 }
114
115 ### validate book XML
116
117 if( (not $noValidate) && -e $RXP ) {
118     system( $RXP, '-Vs', $bookXML ) == 0
119         or die "$PROGRAM_NAME: XML validation failed\n";
120 }
121 elsif( $noValidate ) {
122     warn "$PROGRAM_NAME: XML validation skipped - validate before publication\n";
123 }
124 else {
125     warn "$PROGRAM_NAME: XML validator not installed - validate before publication\n";
126 }
127
128 ### read in metadata file
129
130 unless( -e $metaFile && -f $metaFile && -r $metaFile ) {
131     die qq{$PROGRAM_NAME: Improper metadata file "$metaFile"\n};
132 }
133
134 open( META, '<', $metaFile ) or 
135     die qq{$PROGRAM_NAME: Unable to open metadata file "$metaFile": $!\n};
136
137 my $meta = '';
138 while( my $line = <META> ) {
139     $meta .= $line if $line !~ /^[[:space:]]*#/;
140 }
141 close META;
142
143 ### interpret rules from metadata
144 my $rulesString = '';
145 if( $meta =~ /^[[:space:]]*$bookCode[[:space:]]*{([^}]*)}/sm ) {
146     $rulesString = $1;
147 }
148 else {
149     die "$PROGRAM_NAME: Book code ($bookCode) not found in metadata file or invalid file syntax\n";
150 }
151
152 my @rules = split( /[[:space:]\n]*;[[:space:]\n]*/, $rulesString );
153 my %rulesHash;
154 foreach my $rule (@rules) {
155     if( $rule =~ /[[:space:]]*([^:]+)[[:space:]]*:[[:space:]]*(.+)$/s ) {
156         $rulesHash{ $1 } = $2;
157     }
158     else {
159         die "$PROGRAM_NAME: Unrecognized rule syntax:\n$rule\n";
160     }
161 }
162
163 unless( defined $rulesHash{'book-series'} ) {
164     die "$PROGRAM_NAME: no book series set\n";
165 }
166 unless( defined $rulesHash{'csst'} ) {
167     die "$PROGRAM_NAME: metadata file leaves CSS templates unspecified\n";
168 }
169
170 my $SERIES = get_series($rulesHash{'book-series'}) ;
171 my $SERIES_NUMBER = get_series_number($bookCode);
172
173
174 ### create output directories
175
176 my %outPath;
177 $outPath{'top'} = $rulesHash{'language'} . $FILENAME_SEPARATOR .
178                      'epub' . $FILENAME_SEPARATOR .
179                      $rulesHash{'book-series'} . $FILENAME_SEPARATOR .
180                      $bookCode;
181 # clear old files
182 if( -e "$outPath{'top'}$FILENAME_SEPARATOR$MIMETYPE_FILE" ) {
183     print qx{$RM -r $outPath{'top'}$FILENAME_SEPARATOR*};
184 }
185
186 $outPath{'meta-inf'} = $outPath{'top'} . $FILENAME_SEPARATOR . $META_INF_DIR;
187 $outPath{'oebps'} = $outPath{'top'} . $FILENAME_SEPARATOR . $OEBPS_DIR;
188
189 foreach my $directory (keys(%outPath)) {
190     unless( -e $outPath{$directory} && -d $outPath{$directory} ) {
191         mkpath $outPath{$directory}
192             or die "$PROGRAM_NAME: Unknown error creating output directory " .
193                    "\"$outPath{$directory}\"\n";
194     }
195 }
196
197 ### create content files
198
199 # the location of this tempfile also controls where the xhtml will go
200 my $tempFile = "$outPath{'oebps'}${FILENAME_SEPARATOR}foo.xml";
201 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)
202 print qx{$RM $tempFile};
203
204 foreach my $imagePath (split( /:/, $rulesHash{'images'} )) {
205     unless( -e $imagePath && -d $imagePath ) {
206     die "$PROGRAM_NAME: Image path ($imagePath) does not exist or is not a directory\n";
207 }
208     print qx{$CP $imagePath${FILENAME_SEPARATOR}* $outPath{'oebps'}};
209 }
210
211 ### create the CSS stylsheet
212
213 foreach my $cssTemplate (split( /:/, $rulesHash{'csst'} )) {
214     $cssTemplate =~ m/([^${FILENAME_SEPARATOR}]+)t$/;
215     my $templateFile = $1;
216     open( TEMPLATE, '<', $cssTemplate ) 
217         or die "$PROGRAM_NAME: Unable to open CSS template file ($cssTemplate): $!\n";
218
219     my $stylesFile = "$outPath{'oebps'}$FILENAME_SEPARATOR$templateFile";
220     open( STYLESHEET, '>', $stylesFile ) 
221         or die "$PROGRAM_NAME: Unable to open stylesheet file ($stylesFile) for writing: $!\n";
222
223     while( my $templateLine = <TEMPLATE> ) {
224         while( $templateLine =~ /%%([^%[:space:]]+)%%/ ) {
225             my $name = $1;
226             $templateLine =~ s/%%${name}%%/$rulesHash{$name}/g;
227         }
228         print STYLESHEET $templateLine;
229     }
230     close STYLESHEET;
231     close TEMPLATE;
232 }
233
234 ### copy the font files
235
236 unless( -e $fontFiles && -d $fontFiles ) {
237     die "$PROGRAM_NAME: font files directory does not exist or is not a directory \"$fontFiles\": $!\n";
238 }
239 print qx{$CP $fontFiles${FILENAME_SEPARATOR}*tf $outPath{'oebps'}};
240
241 ### write NCX file
242
243 my $uniqueID = "opf-$bookCode";
244 my $bookUniqueURI = "$PROJECT_AON_URI/$language/epub/" .
245                     "$rulesHash{'book-series'}/$bookCode/";
246
247 my $ncxFile = $outPath{'oebps'} . $FILENAME_SEPARATOR . $NCX_FILE;
248 open( NCXFILE, '>', $ncxFile ) or
249     die "$PROGRAM_NAME: unable to open NCX file for writing " .
250         "\"$ncxFile\"\n";
251 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
252 close NCXFILE;
253
254 ### write mimetype file
255
256 my $mimeFile = $outPath{'top'} . $FILENAME_SEPARATOR . $MIMETYPE_FILE;
257 open( MIMETYPE, '>', $mimeFile ) or
258     die "$PROGRAM_NAME: unable to open mimetype file for writing " .
259         "\"$mimeFile\"\n";
260 print MIMETYPE $EPUB_MIMETYPE;
261 close MIMETYPE;
262
263
264 ### write OPF Root file
265 # All content files must be created prior to creating the OPF root file
266 # with its manifest of content files.
267
268 my $opfFileName = "$bookCode.opf";
269 my $opfFile = "$outPath{'oebps'}$FILENAME_SEPARATOR$opfFileName";
270 open( OPF, '>', $opfFile ) or
271     die "$PROGRAM_NAME: unable to open OPF file for writing " .
272         "\"$opfFile\"\n";
273
274 print OPF <<END_OPF_HEADER;
275 <?xml version="1.0"?>
276 <package version="2.0" 
277          xmlns="http://www.idpf.org/2007/opf" 
278          unique-identifier="$uniqueID">
279
280 END_OPF_HEADER
281
282 ## write metadata
283
284 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"}; #" comment to unconfuse VIM syntax hilighting
285 $metadata = " $metadata";
286 $metadata =~ s|(<dc:)|\n  $1|g;
287 $metadata =~ s|(</metadata>)|\n $1|g;
288
289 print OPF "$metadata\n\n";
290
291 ## write manifest data
292 # assuming a flat directory structure within the OEBPS directory
293
294 print OPF " <manifest>\n";
295
296 opendir( my $content_dir, $outPath{'oebps'} )
297     or die "$PROGRAM_NAME: unable to read OEBPS directory " .
298            "\"$outPath{'oebps'}\": $!\n";
299
300 while( my $content_file = readdir $content_dir ) {
301     next if $content_file eq '.';
302     next if $content_file eq '..';
303     next if $content_file =~ m/\.opf$/i;
304     print OPF qq{  <item id="};
305     print OPF make_id( $content_file );
306     print OPF qq{" href="$content_file" media-type="};
307     print OPF get_mime_type( $content_file );
308     print OPF qq{"/>\n};
309 }
310
311 closedir $content_dir;
312
313 print OPF " </manifest>\n\n";
314
315 ## write spine data
316
317 my $ncxID = make_id( $NCX_FILE );
318 my $spine = qx{$JAVA -classpath "$XALAN_JAR" org.apache.xalan.xslt.Process -IN "$bookXML" -XSL "$spineXSL" -PARAM toc-id "$ncxID"};
319
320 $spine =~ s/idref="([^"]*)"/idref="$1.$XHTML_EXT"/g;
321 $spine = " $spine";
322 $spine =~ s|(<itemref)|\n  $1|g;
323 $spine =~ s|(</spine>)|\n $1|g;
324
325 print OPF "$spine\n\n";
326
327 # TODO: write (optional) guide data here
328 #print OPF " <guide>\n";
329 #print OPF " </guide>\n</package>";
330
331 print OPF "</package>";
332 close OPF;
333
334
335 ### write container.xml
336
337 my $containerFile = "$outPath{'meta-inf'}$FILENAME_SEPARATOR$CONTAINER_FILE";
338 open( CONTAINER, '>', $containerFile ) or
339     die "$PROGRAM_NAME: unable to open container file for writing " .
340         "\"$containerFile\"\n";
341 print CONTAINER <<END_CONTAINER;
342 <?xml version="1.0"?>
343 <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
344   <rootfiles>
345     <rootfile full-path="$OEBPS_DIR/$opfFileName"
346      media-type="application/oebps-package+xml" />
347   </rootfiles>
348 </container>
349 END_CONTAINER
350 close CONTAINER;
351
352 ### compress epub contents
353 # complies with Open Container Format 2.0.1
354 # http://idpf.org/epub/20/spec/OCF_2.0.1_draft.doc
355
356 chdir $outPath{'top'};
357
358 system( $ZIP, '-0Xq', "$bookCode.epub", $MIMETYPE_FILE );
359 system( $ZIP, '-rq', "$bookCode.epub", $META_INF_DIR );
360 system( $ZIP, '-rq', "$bookCode.epub", $OEBPS_DIR );
361
362 exit 0;
363
364 ################################################################################
365 # Subroutines
366 ################################################################################
367
368 sub make_id {
369     my ( $name ) = ( @_ );
370     $name = "_$name" if( $name =~ m/^[-.0-9]/ );
371     $name =~ tr/\x80-\xff/_/;
372     return $name
373 }
374
375 sub get_mime_type {
376     # relies on valid file name extensions
377
378     my ( $file ) = ( @_ );
379     if( $file =~ m/\.x?html?$/i ) {
380         return 'application/xhtml+xml';
381     }
382     elsif( $file =~ m/\.css$/i ) {
383         return 'text/css';
384     }
385     elsif( $file =~ m/\.png$/i ) {
386         return 'image/png';
387     }
388     elsif( $file =~ m/\.jpe?g$/i ) {
389         return 'image/jpeg';
390     }
391     elsif( $file =~ m/\.svg$/i ) {
392         return 'image/svg+xml';
393     }
394     elsif( $file =~ m/\.gif$/i ) {
395         return 'image/gif';
396     }
397     elsif( $file =~ m/\.ncx$/i ) {
398         return 'application/x-dtbncx+xml';
399     }
400     elsif( $file =~ m/\.otf$/i ) {
401         return 'application/x-font-opentype';
402     }
403     else {
404         return 'application/x-unrecognized-mime';
405     }
406 }
407
408 sub check_file {
409 # Check if a file is empty, if it is, abort as this is an indication
410 # that the previous processing step went wrong
411     my ($file) = @_;
412
413     if ( -z $file ) {
414         print  STDERR "There was an error generating $file (empty file produced)";
415         exit 1;
416     }
417
418     return 0;
419 }
420
421 #unless( $bookXML =~ m{^([-\w\@./]+)$} ) {
422 #    die "$PROGRAM_NAME: bad book XML filename \"$bookXML\"\n";
423 #}
424 #$bookXML = $1;
425 #
426 #unless( -e $bookXML && -f $bookXML && -r $bookXML ) {
427 #    die "$PROGRAM_NAME: XML does not exist or is not readable \"$bookXML\"\n";
428 #}
429 #
430 #if( -e $RXP ) {
431 #    system( $RXP, '-Vs', $bookXML ) == 0
432 #        or die "$PROGRAM_NAME: XML validation failed\n";
433 #}
434 #else {
435 #    warn "$PROGRAM_NAME: XML Validator not installed - validate before publication\n";
436 #}
437 #
438 #unless( defined $rulesHash{'language'} ) { die "$PROGRAM_NAME: Metadata file leaves language unspecified\n"; }
439 #unless( defined $rulesHash{'book-series'} ) { die "$PROGRAM_NAME: Metadata file leaves book series unspecified\n"; }
440 #unless( defined $rulesHash{'images'} ) { die "$PROGRAM_NAME: Metadata file leaves image directories unspecified\n"; }
441 #unless( defined $rulesHash{'csst'} ) { die "$PROGRAM_NAME: Metadata file leaves CSS templates unspecified\n"; }
442 #
443 #
444 #my $bookPath = "$outPath${FILENAME_SEPARATOR}";
445 #print qx{$RM ${bookPath}*} if -e $bookPath."/toc.htm";
446 #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'}"};
447 #print qx{$RM ${bookPath}foo.xml};
448 #
449 #
450 #
451 #print qx{$ZIP -8 -q ${bookCode}.zip ${bookPath}*};
452 #print qx{$MV ${bookCode}* $bookPath};
453 #
454 #print "Success\n" if $verbose;
455
456 # Determine series long name by the series acronym
457 sub get_series {
458     my ($series) = @_;
459     my $series_name = "";
460     if ($series eq "lw" ) {
461         $series_name = "Lone Wolf";
462     } elsif ($series eq "ls" ) {
463         $series_name = "Lobo Solitario";
464     } elsif ($series eq "gs" ) {
465         $series_name = "Grey Star the Wizard";
466     } elsif ($series eq "fw" ) {
467         $series_name = "Freeway Warrior";
468     } else {
469         print STDERR "WARN: Undefined series. Short name given: '$series'\n";
470         $series_name = "[undefined]";
471     }
472     return $series_name;
473 }
474
475 # Determine the series number based on book code
476 sub get_series_number {
477     my ($bookCode) = @_;
478     my $series_number = "";
479     if ( $bookCode =~ /^(\d\d)/ ) {
480         $series_number = $1;
481     } else {
482         print STDERR "WARN: Undefined series number. Book code is '$bookCode'.\n";
483         $series_number = "xx";
484     }
485     return $series_number;
486 }
487
488 # Convert character entities to their correspondent values
489 sub convert_entities {
490     my ($text) = @_;
491
492     $text =~ s/\<ch.apos\/\>/'/g; 
493     $text =~ s/\<ch.nbsp\/\>/ /g;
494     $text =~ s/\<ch.plusmn\/\>/+-/g;
495     $text =~ s/\<ch.aacute\/\>/á/g;
496     $text =~ s/\<ch.eacute\/\>/é/g;
497     $text =~ s/\<ch.iacute\/\>/í/g;
498     $text =~ s/\<ch.oacute\/\>/ó/g;
499     $text =~ s/\<ch.uacute\/\>/ú/g;
500     $text =~ s/\<ch.ntilde\/\>/ñ/g;
501     $text =~ s/\<ch.Aacute\/\>/Á/g;
502     $text =~ s/\<ch.Eacute\/\>/É/g;
503     $text =~ s/\<ch.Iacute\/\>/Í/g;
504     $text =~ s/\<ch.Oacute\/\>/Ó/g;
505     $text =~ s/\<ch.Uacute\/\>/Ú/g;
506     $text =~ s/\<ch.auml\/\>/ä/g;
507     $text =~ s/\<ch.euml\/\>/ë/g;
508     $text =~ s/\<ch.iuml\/\>/ï/g;
509     $text =~ s/\<ch.ouml\/\>/ö/g;
510     $text =~ s/\<ch.uuml\/\>/ü/g;
511     $text =~ s/\<ch.Ntilde\/\>/Ñ/g;
512     $text =~ s/\<ch.acute\/\>/´/g;
513     $text =~ s/\<ch.iexcl\/\>/¡/g;
514     $text =~ s/\<ch.iquest\/\>/¿/g;
515     $text =~ s/\<ch.laquo\/\>/«/g;
516     $text =~ s/\<ch.raquo\/\>/»/g;
517     $text =~ s/\<ch.ampersand\/\>/&/g;
518
519     return $text;
520 }
521
522 # Quote metacaracters for shell use
523 sub quote_shell {
524     my ($text) = @_;
525     $text =~ s/'/\\'/g; 
526     return $text;
527 }