File Coverage

blib/lib/App/MakeEPUB.pm
Criterion Covered Total %
statement 39 232 16.8
branch 2 66 3.0
condition 4 10 40.0
subroutine 11 27 40.7
pod 3 3 100.0
total 59 338 17.4


line stmt bran cond sub pod time code
1             # vim: set ts=4 sw=4 tw=78 et si ft=perl:
2             package App::MakeEPUB;
3              
4 2     2   174794 use warnings;
  2         15  
  2         68  
5 2     2   11 use strict;
  2         4  
  2         48  
6 2     2   1574 use Archive::Zip qw( :ERROR_CODES :CONSTANTS );
  2         201077  
  2         314  
7 2     2   20 use Carp;
  2         4  
  2         115  
8 2     2   12 use File::Basename;
  2         6  
  2         98  
9 2     2   12 use File::Find;
  2         4  
  2         143  
10 2     2   13 use File::Path qw(make_path);
  2         4  
  2         83  
11 2     2   1700 use HTML::TreeBuilder;
  2         66951  
  2         18  
12              
13 2     2   1104 use version; our $VERSION = qv('v0.3.2');
  2         4130  
  2         28  
14              
15             my %guidetitle = (
16             cover => 'Cover',
17             );
18              
19             my %embed = (
20             'container.xml' => q(
21            
22            
23            
24            
25            
26             ),
27             'content.opf' => q(
28            
29              
30            
31             %%METADATA%%
32            
33              
34            
35             %%MANIFEST%%
36            
37              
38            
39             %%SPINE%%
40            
41              
42             %%GUIDE%%
43             ),
44             'toc.ncx' => q(
45            
46            
47              
48            
49             %%TOCNCXHEAD%%
50            
51              
52            
53             %%TOCNCXDOCTITLE%%
54            
55              
56            
57             %%TOCNCXNAVMAP%%
58            
59              
60             ),
61             );
62              
63             sub new {
64 0     0 1 0 my ($self, $args) = @_;
65 0   0     0 my $type = ref($self) || $self;
66              
67 0         0 $self = bless {}, $type;
68 0         0 $self->{path} = {};
69             #
70             $self->{nav_l2} = {
71 0         0 '_tag' => 'span',
72             'class' => 'h2',
73             };
74              
75 0 0       0 $self->_init($args) if (defined $args);
76              
77 0         0 return $self;
78             } # new()
79              
80             sub add_metadata {
81 0     0 1 0 my ($self, $opt) = @_;
82 0         0 my %data = ();
83 0         0 $data{metadata}->{identifier} = $opt->{identifier};
84 0         0 $data{metadata}->{language} = $opt->{language};
85 0         0 $data{metadata}->{title} = $opt->{title};
86 0 0       0 $data{metadata}->{creator} = $opt->{creator} if $opt->{creator};
87 0 0       0 $data{metadata}->{publisher} = $opt->{publisher} if $opt->{publisher};
88 0 0       0 $data{metadata}->{rights} = $opt->{rights} if $opt->{rights};
89 0 0       0 $data{guide}->{cover} = $opt->{cover} if $opt->{cover};
90 0         0 $data{tocncx}->{uid} = $opt->{identifier};
91 0         0 $data{tocncx}->{depth} = $opt->{tocdepth};
92 0         0 $data{tocncx}->{totalPageCount} = 0;
93 0         0 $data{tocncx}->{maxPageNumber} = 0;
94 0         0 $self->{data} = \%data;
95 0         0 return %data;
96             } # add_metadata()
97              
98             sub write_epub {
99 0     0 1 0 my ($self,$outname) = @_;
100 0         0 my $paths = $self->{path_ids};
101 0         0 my $epub = Archive::Zip->new();
102 0         0 my $m;
103              
104 0         0 $m = $epub->addString('application/epub+zip', 'mimetype');
105              
106 0         0 $m = $epub->addString($embed{'container.xml'}, 'META-INF/container.xml');
107              
108 0         0 $m = $epub->addString($self->_substitute_template($embed{'content.opf'}),
109             'content.opf');
110              
111 0         0 $m = $epub->addString($self->_substitute_template($embed{'toc.ncx'}),
112             'toc.ncx');
113              
114 0         0 foreach my $path (keys %$paths) {
115 0 0       0 next if 'toc.ncx' eq $path;
116 0         0 $m = $epub->addFile($self->{epubdir} . '/' . $path, $path);
117             }
118              
119 0 0       0 unless ($outname) {
120 0         0 $outname = $self->{epubdir} . '.epub';
121             }
122 0 0       0 unless (AZ_OK == $epub->writeToFileNamed($outname)) {
123 0         0 die "could not write to $self->{epubdir}.epub: $!";
124             }
125             } # write_epub()
126              
127             sub _generate_guide {
128 0     0   0 my ($self) = @_;
129 0         0 my $data = $self->{data};
130 0         0 my @guide = ();
131 0 0       0 if (my $g = $data->{guide}) {
132 0         0 push @guide, q( );
133 0         0 foreach my $type (keys %$g) {
134 0         0 push @guide,
135             qq(
136             . qq( href="$g->{$type}" />);
137             }
138 0         0 push @guide ,q( );
139             }
140 0         0 return join "\n", @guide;
141             } # _generate_guide()
142              
143             sub _generate_manifest {
144 0     0   0 my ($self) = @_;
145 0         0 my $c_opf = $self->{path}->{'content.opf'};
146 0         0 my $paths = $self->{path_ids};
147 0         0 my @manifest = ();
148 0         0 my $type;
149 0         0 foreach my $path (keys %$paths) {
150 0         0 my $id = $paths->{$path};
151 0 0       0 next if $path eq 'mimetype';
152 0 0       0 next if $path eq 'META-INF/container.xml';
153 0 0       0 next if $path eq $c_opf;
154 0 0       0 if ($path =~ /\.html$/i) {
    0          
    0          
    0          
    0          
155 0         0 $type = 'application/xhtml+xml';
156             }
157             elsif ($path =~ /\.png$/i) {
158 0         0 $type = 'image/png';
159             }
160             elsif ($path =~ /\.jpe?g$/i) {
161 0         0 $type = 'image/jpeg';
162             }
163             elsif ($path =~ /toc\.ncx$/i) {
164 0         0 $type = 'application/x-dtbncx+xml';
165 0         0 $id = 'ncx';
166             }
167             elsif ($path =~ /\.css$/i) {
168 0         0 $type = 'text/css';
169             }
170             else {
171 0         0 die "Don't know type media-type for '$path'!";
172             }
173 0         0 push @manifest, qq( );
174             }
175 0         0 return join "\n", @manifest;
176             } # _generate_manifest()
177              
178             sub _generate_metadata {
179 0     0   0 my ($self) = @_;
180 0         0 my @metadata = ();
181 0         0 my $md = $self->{data}->{metadata};
182 0         0 push(@metadata
183             ,qq( $md->{identifier})
184             , " $md->{language}"
185             , " $md->{title}");
186             push(@metadata
187 0 0       0 , " $md->{creator}") if ($md->{creator});
188             push(@metadata
189             , " $md->{publisher}"
190 0 0       0 ) if ($md->{publisher});
191             push(@metadata
192             , " $md->{rights}"
193 0 0       0 ) if ($md->{rights});
194 0         0 return join "\n", @metadata;
195             } # _generate_metadata()
196              
197             sub _generate_spine {
198 0     0   0 my ($self) = @_;
199 0         0 my $sp = $self->{spine_order};
200 0         0 my $paths = $self->{path_ids};
201 0         0 my @spine = ();
202 0         0 foreach my $path (@$sp) {
203 0         0 my $id = $paths->{$path};
204 0         0 push @spine, qq( );
205             }
206 0         0 return join "\n", @spine;
207             } # _generate_spine()
208              
209             sub _generate_tocncx_head {
210 0     0   0 my ($self) = @_;
211 0         0 my $data = $self->{data}->{tocncx};
212 0         0 my @head = ();
213 0         0 foreach my $key (keys %$data) {
214 0         0 push @head, qq( );
215             }
216 0         0 return join "\n", @head;
217             } # _generate_tocncx_head()
218              
219             sub _generate_tocncx_navMap {
220 0     0   0 my ($self) = @_;
221 0         0 my $epubdir = $self->{epubdir};
222 0         0 my $spine_paths = $self->{spine_order};
223 0         0 my $tocdepth = $self->{data}->{tocncx}->{depth};
224 0         0 my @navMap = ();
225 0         0 my $id = 1;
226 0         0 foreach my $sp (@$spine_paths) {
227 0         0 my $navPoint;
228 0         0 ($id, $navPoint) = $self->_generate_tocncx_navPoint($sp,
229             $tocdepth,
230             $id);
231 0         0 push @navMap, $navPoint;
232             }
233 0         0 return join "\n", @navMap;
234             } # _generate_tocncx_navMap()
235              
236             sub _generate_tocncx_navPoint {
237 0     0   0 my ($self, $path, $depth, $id) = @_;
238 0         0 my $epubdir = $self->{epubdir};
239              
240 0 0       0 die "Can't do tocdepth > 1 at the moment." if (2 < $depth);
241 0         0 my $tree = HTML::TreeBuilder->new();
242              
243 0         0 $tree->parse_file("$epubdir/$path");
244              
245 0         0 my $tt = $tree->look_down('_tag' => 'title');
246 0         0 my $title = $tt->as_text();
247 0         0 my $extra = '';
248 0         0 my $l1id = $id;
249 0         0 my $args = { };
250 0         0 my $cnt;
251            
252 0 0       0 if (2 == $depth) {
253 0         0 my @l2s = $tree->look_down(%{$self->{nav_l2}});
  0         0  
254 0         0 my $nps = [];
255              
256 0         0 foreach my $l2 (@l2s) {
257 0         0 my $text = $l2->as_text();
258 0 0       0 if (my $a = $l2->look_down('_tag' => 'a', 'id' => qr//)) {
259 0         0 my $id = $a->attr('id');
260 0         0 push @$nps, [ $path, $id, $text ];
261             }
262             }
263 0         0 $args->{counter} = $id + 1;
264 0         0 $args->{array} = $nps;
265 0         0 $args->{indent} = ' ';
266 0         0 $extra = _tocncf_navPoints_from_array($args);
267 0         0 $cnt = $args->{counter};
268             }
269             else {
270 0         0 $cnt = $id + 1;
271             }
272              
273 0         0 $args->{counter} = $id;
274 0         0 $args->{array} = [ [ $path, '', $title, $extra ], ];
275 0         0 $args->{indent} = ' ';
276              
277 0         0 my $navPoints = _tocncf_navPoints_from_array($args);
278              
279 0         0 return ($cnt, $navPoints);
280             } # _generate_tocncx_navPoint()
281              
282             sub _init {
283 0     0   0 my ($self, $args) = @_;
284              
285 0 0       0 die "need argument 'epubdir'" unless (defined $args->{epubdir});
286 0         0 $self->{epubdir} = $args->{epubdir};
287 0         0 $self->{epubdir} =~ s|/$||;
288 0         0 $self->{spine_order} = $args->{spine_order};
289 0 0       0 if ($args->{level2}) {
290             #
291             # $args->{level2} comes as 'attr1:val1,attr2:val2,...' and goes into
292             # $self->{nav_l2} as { attr1 => val1, attr2 => val2, ... }
293             #
294 0         0 my @attrs = split(/,/, $args->{level2});
295 0         0 my %nav_l2 = map { my @p = split(/:/, $_, 2); $p[0] => $p[1] } @attrs;
  0         0  
  0         0  
296 0         0 $self->{nav_l2} = \%nav_l2;
297             }
298              
299 0         0 $self->_scan_directory();
300 0         0 $self->_spine_order();
301 0         0 $self->{path}->{'content.opf'} = 'content.opf';
302             } # _init()
303              
304             sub _scan_directory {
305 0     0   0 my ($self) = @_;
306 0         0 my $startdir = $self->{epubdir};
307 0         0 my $id = 1;
308 0         0 my $dirs = {};
309 0         0 my $have_toc_ncx = 0;
310             my $adddir = sub {
311 0 0 0 0   0 if (m|^$startdir/(.+)$| && -f $_) {
312 0         0 my $path = $1;
313 0         0 $dirs->{$path} = "id$id";
314 0         0 $id++;
315 0 0       0 if ($path =~ m|(.*/)?toc\.ncx$|) {
316 0         0 $have_toc_ncx = 1;
317             }
318             }
319 0         0 };
320 0         0 find( {wanted => $adddir, no_chdir => 1 },$startdir);
321 0 0       0 unless ($have_toc_ncx) {
322 0         0 $dirs->{'toc.ncx'} = 'ncx';
323             }
324 0         0 $self->{path_ids} = $dirs;
325 0         0 return %$dirs;
326             } # _scan_directory()
327              
328             sub _spine_order {
329 0     0   0 my ($self) = @_;
330 0         0 my $paths = $self->{path_ids};
331 0         0 my $order = $self->{spine_order};
332 0         0 my %o2p = ();
333 0         0 my @spo = ();
334 0         0 foreach my $path (keys %$paths) {
335 0 0       0 next unless $path =~ /^(.+)\.html/i;
336 0         0 my $si = $1;
337 0         0 $si =~ s|^.+/([^/]+)$|$1|;
338 0         0 $o2p{$si} = $path;
339             }
340 0 0       0 if ($order) {
341 0         0 @spo = map { $o2p{$_} } split /,/, $order;
  0         0  
342             }
343             else {
344 0         0 @spo = map { $o2p{$_} } sort keys %o2p;
  0         0  
345             }
346 0         0 $self->{spine_order} = \@spo;
347 0         0 return @spo;
348             } # _spine_order()
349              
350             sub _substitute_template {
351 0     0   0 my ($self,$tmpl,$data) = @_;
352 0         0 my $out = "";
353              
354 0 0       0 unless (exists $self->{substitutes}) {
355 0         0 my $s = {};
356             $s->{'%%GUIDE%%'} = $self->_generate_guide(),
357             $s->{'%%MANIFEST%%'} = $self->_generate_manifest(),
358             $s->{'%%METADATA%%'} = $self->_generate_metadata(),
359             $s->{'%%SPINE%%'} = $self->_generate_spine(),
360             $s->{'%%TOCNCXDOCTITLE%%'} = ''
361             . $self->{data}->{metadata}->{title}
362             . '',
363             $s->{'%%TOCNCXHEAD%%'} = $self->_generate_tocncx_head(),
364             $s->{'%%TOCNCXNAVMAP%%'} = $self->_generate_tocncx_navMap(),
365 0         0 $self->{substitutes} = $s;
366             }
367 0         0 my $substitutes = $self->{substitutes};
368              
369             my $replace = sub {
370 0     0   0 my ($pattern) = @_;
371 0 0       0 return $substitutes->{$pattern} ? $substitutes->{$pattern} : '';
372 0         0 };
373              
374 0         0 my @lines = split /\n/, $tmpl;
375 0         0 foreach (@lines) {
376 0         0 s/(%%[^%]+%%)/$replace->($1)/e;
  0         0  
377 0         0 $out .= $_ . "\n";
378             }
379 0         0 return $out;
380             } # _substitute_template()
381              
382             # Functions not bound to an object.
383             # ---------------------------------
384              
385             # _tocncf_navPoints_from_array( {
386             # counter => $cnt,
387             # array => $array,
388             # indent => " ",
389             # } )
390             #
391             # Returns a string containing entries for the in toc.ncf
392             # from the given array. The first id is named "navPoint-$cnt" and the first
393             # playOrder "$cnt". $cnt is updated to the next number after the last
394             # playOrder.
395             #
396             # The array should be of the form [ [ $fname, $anchor, $text, $extra ], ... ],
397             # where $fname is the name of the file, $anchor the id of an html anchor
398             # () and $text the text belonging to the anchor. The
399             # fourth field ($extra) is optional and can be used for next level navPoints.
400             #
401             sub _tocncf_navPoints_from_array {
402 2     2   1485 my ($args) = @_;
403 2         4 my @anchors = @{$args->{array}};
  2         6  
404 2   100     12 my $indent = $args->{indent} || "";
405              
406             my $np = sub {
407 4     4   6 my $count = $args->{counter}++;
408 4 100       15 my $href = ($_[0]->[1]) ? $_[0]->[0] . "#" . $_[0]->[1] : $_[0]->[0];
409 4         6 my $label = $_[0]->[2];
410 4   100     12 my $extra = $_[0]->[3] || '';
411 4         41 return << "EONAVPOINT";
412             $indent
413             $indent $label
414             $indent
415             $extra$indent
416             EONAVPOINT
417 2         13 };
418              
419 2         18 my $navPoints = join("", map { $np->($_) } @anchors);
  4         12  
420             } # _tocncf_navPoints_from_array()
421              
422             1; # Magic true value required at end of module
423             __END__