File Coverage

blib/lib/EBook/EPUB/Lite.pm
Criterion Covered Total %
statement 198 246 80.4
branch 15 46 32.6
condition 1 6 16.6
subroutine 43 49 87.7
pod 12 27 44.4
total 269 374 71.9


line stmt bran cond sub pod time code
1             # Copyright (c) 2009, 2010 Oleksandr Tymoshenko
2             # All rights reserved.
3              
4             # Redistribution and use in source and binary forms, with or without
5             # modification, are permitted provided that the following conditions
6             # are met:
7             # 1. Redistributions of source code must retain the above copyright
8             # notice, this list of conditions and the following disclaimer.
9             # 2. Redistributions in binary form must reproduce the above copyright
10             # notice, this list of conditions and the following disclaimer in the
11             # documentation and/or other materials provided with the distribution.
12              
13             # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
14             # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15             # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
16             # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
17             # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18             # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
19             # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
20             # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
21             # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
22             # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
23             # SUCH DAMAGE.
24              
25             package EBook::EPUB::Lite;
26              
27 4     4   135680 use strict;
  4         8  
  4         108  
28 4     4   19 use warnings;
  4         10  
  4         121  
29 4     4   2917 use version;
  4         8904  
  4         24  
30             our $VERSION = 0.71;
31              
32 4     4   5902 use Moo;
  4         95077  
  4         28  
33 4     4   34239 use Types::Standard qw/ArrayRef HashRef Object Str/;
  4         315899  
  4         48  
34 4     4   7129 use EBook::EPUB::Lite::Metadata; # done
  4         16  
  4         139  
35 4     4   2531 use EBook::EPUB::Lite::Manifest; # done
  4         10  
  4         109  
36 4     4   2137 use EBook::EPUB::Lite::Guide; # done
  4         11  
  4         120  
37 4     4   2101 use EBook::EPUB::Lite::Spine; # done
  4         9  
  4         127  
38 4     4   2418 use EBook::EPUB::Lite::NCX; # done
  4         13  
  4         125  
39              
40 4     4   2475 use EBook::EPUB::Lite::Container::Zip; # not moose
  4         16  
  4         135  
41              
42 4     4   3679 use UUID::Tiny qw();
  4         266447  
  4         143  
43 4     4   39 use File::Temp;
  4         9  
  4         396  
44 4     4   24 use File::Basename qw/dirname/;
  4         8  
  4         265  
45 4     4   25 use File::Copy;
  4         11  
  4         262  
46 4     4   24 use File::Path;
  4         8  
  4         210  
47 4     4   23 use File::Spec;
  4         105  
  4         84  
48 4     4   20 use Carp;
  4         7  
  4         13258  
49              
50             has metadata => (
51             isa => Object,
52             is => 'ro',
53             default => sub { EBook::EPUB::Lite::Metadata->new() },
54             handles => [ qw/add_contributor
55             add_creator
56             add_coverage
57             add_date
58             add_meta_dcitem
59             add_description
60             add_format
61             add_meta_item
62             add_language
63             add_publisher
64             add_relation
65             add_rights
66             add_source
67             add_subject
68             add_translator
69             add_type
70             /],
71              
72             );
73              
74             has manifest => (
75             isa => Object,
76             is => 'ro',
77             default => sub { EBook::EPUB::Lite::Manifest->new() },
78             );
79              
80             has spine => (
81             isa => Object,
82             is => 'ro',
83             default => sub { EBook::EPUB::Lite::Spine->new() },
84             );
85              
86             has guide => (
87             isa => Object,
88             is => 'ro',
89             default => sub { EBook::EPUB::Lite::Guide->new() },
90             );
91              
92             has ncx => (
93             isa => Object,
94             is => 'ro',
95             default => sub { EBook::EPUB::Lite::NCX->new() },
96             handles => [ qw/add_navpoint/ ],
97             );
98              
99             has _uuid => (
100             isa => Str,
101             is => 'rw',
102             );
103              
104             has _encryption_key => (
105             isa => Str,
106             is => 'rw',
107             );
108              
109             # Array of filenames that should be encrypted
110             has _encrypted_filerefs => (
111             is => 'ro',
112             isa => ArrayRef[Object],
113             default => sub { [] },
114             );
115              
116             sub add_encrypted_fileref {
117 1     1 0 4 my ($self, @args) = @_;
118 1         3 push @{ shift->_encrypted_filerefs }, @args;
  1         7  
119             }
120              
121             sub encrypted_filerefs {
122 2     2 0 4 return @{ shift->_encrypted_filerefs };
  2         27  
123             }
124              
125             has id_counters => ( isa => HashRef, is => 'ro', default => sub { {} });
126              
127             has _temporary_dir_handle => (isa => Object,
128             is => 'ro',
129             default => sub {
130             # defaults to CLEANUP => 1 as per doc
131             return File::Temp->newdir;
132             });
133              
134             sub tmpdir {
135             # return the path, not the object.
136 18     18 0 190 return shift->_temporary_dir_handle->dirname;
137             }
138              
139             sub BUILD
140             {
141 2     2 0 137 my ($self) = @_;
142 2         27 $self->manifest->add_item(
143             id => 'ncx',
144             href => 'toc.ncx',
145             media_type => 'application/x-dtbncx+xml'
146             );
147              
148 2         18 $self->spine->toc('ncx');
149 2 50       1496 mkdir ($self->tmpdir . "/OPS") or die "Can't make OPS dir in " . $self->tmpdir;
150             # Implicitly generate UUID for book
151 2         433 $self->_set_uuid(UUID::Tiny::uuid_to_string(UUID::Tiny::create_uuid()));
152             }
153              
154             sub to_xml
155             {
156 2     2 0 5 my ($self) = @_;
157 2         4 my $xml;
158              
159 2         16 my $writer = XML::Writer->new(
160             OUTPUT => \$xml,
161             DATA_MODE => 1,
162             DATA_INDENT => 2,
163             );
164              
165 2         296 $writer->xmlDecl("utf-8");
166 2         58 $writer->startTag('package',
167             xmlns => 'http://www.idpf.org/2007/opf',
168             version => '2.0',
169             'unique-identifier' => 'BookId',
170             );
171 2         237 $self->metadata->encode($writer);
172 2         82 $self->manifest->encode($writer);
173 2         139 $self->spine->encode($writer);
174 2         104 $self->guide->encode($writer);
175 2         11 $writer->endTag('package');
176 2         86 $writer->end();
177              
178 2         470 return $xml;
179             }
180              
181             sub add_author
182             {
183 1     1 1 13 my ($self, $author, $formal) = @_;
184 1         14 $self->metadata->add_author($author, $formal);
185 1         11 $self->ncx->add_author($author);
186             }
187              
188             sub add_title
189             {
190 1     1 1 12 my ($self, $title) = @_;
191 1         8 $self->metadata->add_title($title);
192 1         10 my $ncx_title = $self->ncx->title;
193             # Collect all titles in a row for NCX
194 1 50       1013 $title = "$ncx_title $title" if (defined($ncx_title));
195 1         31 $self->ncx->title($title);
196             }
197              
198             sub _set_uuid
199             {
200 2     2   796 my ($self, $uuid) = @_;
201              
202             # Just some naive check for key to be UUID
203 2 50       18 if ($uuid !~ /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i) {
204 0         0 carp "$uuid - is not valid UUID";
205 0         0 return;
206             }
207 2         3 my $key = $uuid;
208              
209 2         27 $key =~ s/-//g;
210 2         12 $key =~ s/([a-f0-9]{2})/chr(hex($1))/egi;
  32         77  
211 2         12 $self->_encryption_key($key);
212 2 50       1239 if (defined($self->_uuid)) {
213 0         0 warn "Overriding existing uuid " . $self->_uuid;
214 0         0 $self->_uuid($uuid);
215             }
216              
217 2         1096 $self->ncx->uid("urn:uuid:$uuid");
218 2         1135 $self->metadata->set_book_id("urn:uuid:$uuid");
219 2         1158 $self->_uuid($uuid);
220             }
221              
222             sub add_identifier
223             {
224 0     0 1 0 my ($self, $ident, $scheme) = @_;
225 0 0       0 if ($ident =~ /^urn:uuid:(.*)/i) {
226 0         0 my $uuid = $1;
227 0         0 $self->_set_uuid($uuid);
228             }
229             else {
230 0         0 $self->metadata->add_identifier($ident, $scheme);
231             }
232             }
233              
234             sub add_xhtml_entry
235             {
236 4     4 0 14 my ($self, $filename, %opts) = @_;
237 4         9 my $linear = 1;
238              
239             $linear = 0 if (defined ($opts{'linear'}) &&
240 4 50 33     23 $opts{'linear'} eq 'no');
241              
242              
243 4         18 my $id = $self->nextid('ch');
244 4         31 $self->manifest->add_item(
245             id => $id,
246             href => $filename,
247             media_type => 'application/xhtml+xml',
248             );
249              
250 4         35 $self->spine->add_itemref(
251             idref => $id,
252             linear => $linear,
253             );
254              
255 4         20 return $id;
256             }
257              
258             sub add_stylesheet_entry
259             {
260 2     2 0 7 my ($self, $filename) = @_;
261 2         13 my $id = $self->nextid('css');
262 2         26 $self->manifest->add_item(
263             id => $id,
264             href => $filename,
265             media_type => 'text/css',
266             );
267              
268 2         9 return $id;
269             }
270              
271             sub add_image_entry
272             {
273 1     1 0 3 my ($self, $filename, $type) = @_;
274             # trying to guess
275 1 50       5 if (!defined($type)) {
276 0 0 0     0 if (($filename =~ /\.jpg$/i) || ($filename =~ /\.jpeg$/i)) {
    0          
    0          
    0          
277 0         0 $type = 'image/jpeg';
278             }
279             elsif ($filename =~ /\.gif$/i) {
280 0         0 $type = 'image/gif';
281             }
282             elsif ($filename =~ /\.png$/i) {
283 0         0 $type = 'image/png';
284             }
285             elsif ($filename =~ /\.svg$/i) {
286 0         0 $type = 'image/svg+xml';
287             }
288             else {
289 0         0 croak ("Unknown image type for file $filename");
290 0         0 return;
291             }
292             }
293              
294 1         5 my $id = $self->nextid('img');
295 1         8 $self->manifest->add_item(
296             id => $id,
297             href => $filename,
298             media_type => $type,
299             );
300              
301 1         5 return $id;
302             }
303              
304             sub add_entry
305             {
306 1     1 0 3 my ($self, $filename, $type) = @_;
307 1         5 my $id = $self->nextid('item');
308 1         9 $self->manifest->add_item(
309             id => $id,
310             href => $filename,
311             media_type => $type,
312             );
313              
314 1         4 return $id;
315             }
316              
317             sub add_xhtml {
318 4     4 1 78 my ($self, $filename, $data, %opts) = @_;
319 4         27 $self->_write_text([OPS => $filename], $data);
320 4         23 return $self->add_xhtml_entry($filename, %opts);
321             }
322              
323             sub add_stylesheet {
324 2     2 1 479 my ($self, $filename, $data) = @_;
325 2         15 $self->_write_text([OPS => $filename], $data);
326 2         18 return $self->add_stylesheet_entry($filename);
327             }
328              
329             sub add_image
330             {
331 1     1 1 17 my ($self, $filename, $data, $type) = @_;
332 1         9 $self->_write_data([OPS => $filename], $data);
333 1         11 return $self->add_image_entry($filename, $type);
334             }
335              
336             sub add_data
337             {
338 1     1 0 7 my ($self, $filename, $data, $type) = @_;
339 1         5 $self->_write_data([OPS => $filename], $data);
340 1         8 return $self->add_entry($filename, $type);
341             }
342              
343             sub copy_xhtml
344             {
345 0     0 1 0 my ($self, $src_filename, $filename, %opts) = @_;
346 0         0 my $tmpdir = $self->tmpdir;
347 0 0       0 if (mkdir_and_copy($src_filename, "$tmpdir/OPS/$filename")) {
348 0         0 return $self->add_xhtml_entry($filename, %opts);
349             }
350             else {
351 0         0 carp ("Failed to copy $src_filename to $tmpdir/OPS/$filename");
352             }
353              
354 0         0 return;
355             }
356              
357             sub copy_stylesheet
358             {
359 0     0 1 0 my ($self, $src_filename, $filename) = @_;
360 0         0 my $tmpdir = $self->tmpdir;
361 0 0       0 if (mkdir_and_copy($src_filename, "$tmpdir/OPS/$filename")) {
362 0         0 return $self->add_stylesheet_entry("$filename");
363             }
364             else {
365 0         0 carp ("Failed to copy $src_filename to $tmpdir/OPS/$filename");
366             }
367              
368 0         0 return;
369             }
370              
371             sub copy_image
372             {
373 0     0 1 0 my ($self, $src_filename, $filename, $type) = @_;
374 0         0 my $tmpdir = $self->tmpdir;
375 0 0       0 if (mkdir_and_copy($src_filename, "$tmpdir/OPS/$filename")) {
376 0         0 return $self->add_image_entry("$filename");
377             }
378             else {
379 0         0 carp ("Failed to copy $src_filename to $tmpdir/OPS/$filename");
380             }
381              
382 0         0 return;
383             }
384              
385             sub copy_file
386             {
387 0     0 1 0 my ($self, $src_filename, $filename, $type) = @_;
388 0         0 my $tmpdir = $self->tmpdir;
389 0 0       0 if (mkdir_and_copy($src_filename, "$tmpdir/OPS/$filename")) {
390 0         0 my $id = $self->nextid('id');
391 0         0 $self->manifest->add_item(
392             id => $id,
393             href => "$filename",
394             media_type => $type,
395             );
396 0         0 return $id;
397             }
398             else {
399 0         0 carp ("Failed to copy $src_filename to $tmpdir/OPS/$filename");
400             }
401              
402 0         0 return;
403             }
404              
405             sub encrypt_file
406             {
407 1     1 1 13 my ($self, $src_filename, $filename, $type) = @_;
408 1         4 my $tmpdir = $self->tmpdir;
409 1 50       34 if (!defined($self->_encryption_key)) {
410 0         0 croak "Can't encrypt without a key: no urn:uuid: indetifier has been provided";
411             }
412              
413 1         33 my $key = $self->_encryption_key;
414 1 50       12 if (adobe_encrypt($src_filename, "$tmpdir/OPS/$filename", $key)) {
415 1         6 my $id = $self->nextid('id');
416 1         13 $self->manifest->add_item(
417             id => $id,
418             href => "$filename",
419             media_type => $type,
420             );
421 1         8 $self->add_encrypted_fileref("OPS/$filename");
422 1         6 return $id;
423             }
424             else {
425 0         0 carp ("Failed to copy $src_filename to $tmpdir/OPS/$filename");
426             }
427              
428 0         0 return;
429             }
430              
431              
432             sub nextid
433             {
434 9     9 0 22 my ($self, $prefix) = @_;
435 9         11 my $id;
436              
437 9 50       35 $prefix = 'id' unless(defined($prefix));
438 9 100       14 if (defined(${$self->id_counters}{$prefix})) {
  9         57  
439 2         5 $id = "$prefix" . ${$self->id_counters}{$prefix};
  2         11  
440 2         4 ${$self->id_counters}{$prefix}++;
  2         10  
441             }
442             else
443             {
444             # First usage of prefix
445 7         15 $id = "${prefix}1";
446 7         13 ${$self->id_counters}{$prefix} = 2;
  7         24  
447             }
448              
449 9         29 return $id;
450             }
451              
452             sub pack_zip
453             {
454 2     2 1 24 my ($self, $filename) = @_;
455 2         9 my $tmpdir = $self->tmpdir;
456 2         20 $self->write_ncx;
457 2         13 $self->write_opf;
458 2         48 my $container = EBook::EPUB::Lite::Container::Zip->new($filename);
459 2         27 $container->add_path($tmpdir . "/OPS", "OPS/");
460 2         53 $container->add_root_file("OPS/content.opf", "application/oebps-package+xml");
461 2         18 foreach my $fref ($self->encrypted_filerefs) {
462 1         11 $container->add_encrypted_path($fref);
463             }
464 2         15 return $container->write();
465             }
466              
467             sub write_opf {
468 2     2 0 6 my ($self) = @_;
469 2         15 $self->_write_text([OPS => 'content.opf' ], $self->to_xml);
470             }
471              
472             sub write_ncx {
473 2     2 0 5 my ($self) = @_;
474 2         22 $self->_write_text([OPS => 'toc.ncx'], $self->ncx->to_xml)
475             }
476              
477             # helper function that performs Adobe content protection "encryption"
478             sub adobe_encrypt
479             {
480 1     1 0 3 my ($src, $dst, $key) = @_;
481 1         10 my @key_bytes = unpack "C*", $key;
482              
483             # open source/destination files for read/write
484 1 50       42 open (my $in, '<', $src) or return;
485 1 50       70 open (my $out, '>', $dst) or return;
486 1         3 binmode $in;
487 1         3 binmode $out;
488              
489             # XOR first 1024 bytes of file by provided key
490 1         4 my $data;
491 1         26 read($in, $data, 1024);
492 1         160 my @bytes = unpack ("C*", $data);
493 1         23 my $key_ptr = 0;
494 1         4 foreach my $d (@bytes) {
495 1024         1396 $d = $d ^ $key_bytes[$key_ptr];
496 1024         1071 $key_ptr += 1;
497 1024         1618 $key_ptr = $key_ptr % @key_bytes;
498             }
499              
500 1         32 my $crypted_data = pack "C*", @bytes;
501 1         19 print $out $crypted_data;
502              
503             # Copy th erest of the file, 1M buffer seems to be reasonable default
504 1         35 while (read($in, $data, 1024*1024)) {
505 1         6 print $out $data;
506             }
507              
508 1         12 close $in;
509 1         89 close $out;
510             }
511              
512             sub _write_text {
513 10     10   25 my ($self, $path, $data) = @_;
514 10         40 my $filename = File::Spec->catfile($self->tmpdir, @$path);
515             # print "Writing $filename\n";
516 10 50   2   1509 open (my $fh, '>:encoding(UTF-8)', $filename)
  2         19  
  2         3  
  2         19  
517             or die "Failed to open $filename $!";
518 10         44184 print $fh $data;
519 10         806 close $fh;
520             }
521              
522             sub _write_data {
523 2     2   5 my ($self, $path, $data) = @_;
524 2         7 my $filename = File::Spec->catfile($self->tmpdir, @$path);
525 2 50       185 open (my $fh, '>', $filename)
526             or die "Failed to open $filename $!";
527 2         7 binmode $fh;
528 2         10 print $fh $data;
529 2         72 close $fh;
530             }
531              
532             sub mkdir_and_copy {
533 0     0 0 0 my ($from, $to) = @_;
534 0         0 mkpath(dirname($to));
535 0         0 return copy($from, $to);
536             }
537              
538             1;
539              
540             __END__