File Coverage

blib/lib/EBook/EPUB.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             # Copyright (c) 2009, 2010 Oleksandr Tymoshenko <gonzo@bluezbox.com>
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;
26              
27 1     1   41687 use version;
  1         2876  
  1         6  
28             our $VERSION = 0.6;
29              
30 1     1   414 use Moose;
  0            
  0            
31              
32             use EBook::EPUB::Metadata;
33             use EBook::EPUB::Manifest;
34             use EBook::EPUB::Guide;
35             use EBook::EPUB::Spine;
36             use EBook::EPUB::NCX;
37              
38             use EBook::EPUB::Container::Zip;
39              
40             use Data::UUID;
41             use File::Temp qw/tempdir/;
42             use File::Basename qw/dirname/;
43             use File::Copy;
44             use File::Path;
45             use Carp;
46              
47             has metadata => (
48             isa => 'Object',
49             is => 'ro',
50             default => sub { EBook::EPUB::Metadata->new() },
51             handles => [ qw/add_contributor
52             add_creator
53             add_coverage
54             add_date
55             add_meta_dcitem
56             add_description
57             add_format
58             add_meta_item
59             add_language
60             add_publisher
61             add_relation
62             add_rights
63             add_source
64             add_subject
65             add_translator
66             add_type
67             /],
68              
69             );
70              
71             has manifest => (
72             isa => 'Object',
73             is => 'ro',
74             default => sub { EBook::EPUB::Manifest->new() },
75             );
76              
77             has spine => (
78             isa => 'Object',
79             is => 'ro',
80             default => sub { EBook::EPUB::Spine->new() },
81             );
82              
83             has guide => (
84             isa => 'Object',
85             is => 'ro',
86             default => sub { EBook::EPUB::Guide->new() },
87             );
88              
89             has ncx => (
90             isa => 'Object',
91             is => 'ro',
92             default => sub { EBook::EPUB::NCX->new() },
93             handles => [ qw/add_navpoint/ ],
94             );
95              
96             has _uuid => (
97             isa => 'Str',
98             is => 'rw',
99             );
100              
101             has _encryption_key => (
102             isa => 'Str',
103             is => 'rw',
104             );
105              
106             # Array of filenames that should be encrypted
107             has _encrypted_filerefs => (
108             traits => ['Array'],
109             is => 'ro',
110             isa => 'ArrayRef[Str]',
111             default => sub { [] },
112             handles => {
113             add_encrypted_fileref => 'push',
114             encrypted_filerefs => 'elements',
115             },
116             );
117              
118             has id_counters => ( isa => 'HashRef', is => 'ro', default => sub { {} });
119             has tmpdir => ( isa => 'Str', is => 'rw', default => sub { tempdir( CLEANUP => 1 ); });
120              
121             sub BUILD
122             {
123             my ($self) = @_;
124             $self->manifest->add_item(
125             id => 'ncx',
126             href => 'toc.ncx',
127             media_type => 'application/x-dtbncx+xml'
128             );
129              
130             $self->spine->toc('ncx');
131             mkdir ($self->tmpdir . "/OPS") or die "Can't make OPS dir in " . $self->tmpdir;
132             # Implicitly generate UUID for book
133             my $ug = new Data::UUID;
134             my $uuid = $ug->create_str();
135             $self->_set_uuid($uuid);
136             }
137              
138             sub to_xml
139             {
140             my ($self) = @_;
141             my $xml;
142              
143             my $writer = XML::Writer->new(
144             OUTPUT => \$xml,
145             DATA_MODE => 1,
146             DATA_INDENT => 2,
147             );
148              
149             $writer->xmlDecl("utf-8");
150             $writer->startTag('package',
151             xmlns => 'http://www.idpf.org/2007/opf',
152             version => '2.0',
153             'unique-identifier' => 'BookId',
154             );
155             $self->metadata->encode($writer);
156             $self->manifest->encode($writer);
157             $self->spine->encode($writer);
158             $self->guide->encode($writer);
159             $writer->endTag('package');
160             $writer->end();
161              
162             return $xml;
163             }
164              
165             sub add_author
166             {
167             my ($self, $author, $formal) = @_;
168             $self->metadata->add_author($author, $formal);
169             $self->ncx->add_author($author);
170             }
171              
172             sub add_title
173             {
174             my ($self, $title) = @_;
175             $self->metadata->add_title($title);
176             my $ncx_title = $self->ncx->title;
177             # Collect all titles in a row for NCX
178             $title = "$ncx_title $title" if (defined($ncx_title));
179             $self->ncx->title($title);
180             }
181              
182             sub _set_uuid
183             {
184             my ($self, $uuid) = @_;
185              
186             # Just some naive check for key to be UUID
187             if ($uuid !~ /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i) {
188             carp "$uuid - is not valid UUID";
189             return;
190             }
191             my $key = $uuid;
192              
193             $key =~ s/-//g;
194             $key =~ s/([a-f0-9]{2})/chr(hex($1))/egi;
195             $self->_encryption_key($key);
196             if (defined($self->_uuid)) {
197             warn "Overriding existing uuid " . $self->_uuid;
198             $self->_uuid($uuid);
199             }
200              
201             $self->ncx->uid("urn:uuid:$uuid");
202             $self->metadata->set_book_id("urn:uuid:$uuid");
203             $self->_uuid($uuid);
204             }
205              
206             sub add_identifier
207             {
208             my ($self, $ident, $scheme) = @_;
209             if ($ident =~ /^urn:uuid:(.*)/i) {
210             my $uuid = $1;
211             $self->_set_uuid($uuid);
212             }
213             else {
214             $self->metadata->add_identifier($ident, $scheme);
215             }
216             }
217              
218             sub add_xhtml_entry
219             {
220             my ($self, $filename, %opts) = @_;
221             my $linear = 1;
222              
223             $linear = 0 if (defined ($opts{'linear'}) &&
224             $opts{'linear'} eq 'no');
225              
226              
227             my $id = $self->nextid('ch');
228             $self->manifest->add_item(
229             id => $id,
230             href => $filename,
231             media_type => 'application/xhtml+xml',
232             );
233              
234             $self->spine->add_itemref(
235             idref => $id,
236             linear => $linear,
237             );
238              
239             return $id;
240             }
241              
242             sub add_stylesheet_entry
243             {
244             my ($self, $filename) = @_;
245             my $id = $self->nextid('css');
246             $self->manifest->add_item(
247             id => $id,
248             href => $filename,
249             media_type => 'text/css',
250             );
251              
252             return $id;
253             }
254              
255             sub add_image_entry
256             {
257             my ($self, $filename, $type) = @_;
258             # trying to guess
259             if (!defined($type)) {
260             if (($filename =~ /\.jpg$/i) || ($filename =~ /\.jpeg$/i)) {
261             $type = 'image/jpeg';
262             }
263             elsif ($filename =~ /\.gif$/i) {
264             $type = 'image/gif';
265             }
266             elsif ($filename =~ /\.png$/i) {
267             $type = 'image/png';
268             }
269             elsif ($filename =~ /\.svg$/i) {
270             $type = 'image/svg+xml';
271             }
272             else {
273             croak ("Unknown image type for file $filename");
274             return;
275             }
276             }
277              
278             my $id = $self->nextid('img');
279             $self->manifest->add_item(
280             id => $id,
281             href => $filename,
282             media_type => $type,
283             );
284              
285             return $id;
286             }
287              
288             sub add_entry
289             {
290             my ($self, $filename, $type) = @_;
291             my $id = $self->nextid('item');
292             $self->manifest->add_item(
293             id => $id,
294             href => $filename,
295             media_type => $type,
296             );
297              
298             return $id;
299             }
300              
301             sub add_xhtml
302             {
303             my ($self, $filename, $data, %opts) = @_;
304             my $tmpdir = $self->tmpdir;
305             open F, ">:utf8", "$tmpdir/OPS/$filename";
306             print F $data;
307             close F;
308              
309             return $self->add_xhtml_entry($filename, %opts);
310             }
311              
312             sub add_stylesheet
313             {
314             my ($self, $filename, $data) = @_;
315             my $tmpdir = $self->tmpdir;
316             open F, ">:utf8", "$tmpdir/OPS/$filename";
317             print F $data;
318             close F;
319              
320             return $self->add_stylesheet_entry($filename);
321             }
322              
323             sub add_image
324             {
325             my ($self, $filename, $data, $type) = @_;
326             my $tmpdir = $self->tmpdir;
327             open F, "> $tmpdir/OPS/$filename";
328             binmode F;
329             print F $data;
330             close F;
331              
332             return $self->add_image_entry($filename, $type);
333             }
334              
335             sub add_data
336             {
337             my ($self, $filename, $data, $type) = @_;
338             my $tmpdir = $self->tmpdir;
339             open F, "> $tmpdir/OPS/$filename";
340             binmode F;
341             print F $data;
342             close F;
343              
344             return $self->add_entry($filename, $type);
345             }
346              
347             sub copy_xhtml
348             {
349             my ($self, $src_filename, $filename, %opts) = @_;
350             my $tmpdir = $self->tmpdir;
351             if (mkdir_and_copy($src_filename, "$tmpdir/OPS/$filename")) {
352             return $self->add_xhtml_entry($filename, %opts);
353             }
354             else {
355             carp ("Failed to copy $src_filename to $tmpdir/OPS/$filename");
356             }
357              
358             return;
359             }
360              
361             sub copy_stylesheet
362             {
363             my ($self, $src_filename, $filename) = @_;
364             my $tmpdir = $self->tmpdir;
365             if (mkdir_and_copy($src_filename, "$tmpdir/OPS/$filename")) {
366             return $self->add_stylesheet_entry("$filename");
367             }
368             else {
369             carp ("Failed to copy $src_filename to $tmpdir/OPS/$filename");
370             }
371              
372             return;
373             }
374              
375             sub copy_image
376             {
377             my ($self, $src_filename, $filename, $type) = @_;
378             my $tmpdir = $self->tmpdir;
379             if (mkdir_and_copy($src_filename, "$tmpdir/OPS/$filename")) {
380             return $self->add_image_entry("$filename");
381             }
382             else {
383             carp ("Failed to copy $src_filename to $tmpdir/OPS/$filename");
384             }
385              
386             return;
387             }
388              
389             sub copy_file
390             {
391             my ($self, $src_filename, $filename, $type) = @_;
392             my $tmpdir = $self->tmpdir;
393             if (mkdir_and_copy($src_filename, "$tmpdir/OPS/$filename")) {
394             my $id = $self->nextid('id');
395             $self->manifest->add_item(
396             id => $id,
397             href => "$filename",
398             media_type => $type,
399             );
400             return $id;
401             }
402             else {
403             carp ("Failed to copy $src_filename to $tmpdir/OPS/$filename");
404             }
405              
406             return;
407             }
408              
409             sub encrypt_file
410             {
411             my ($self, $src_filename, $filename, $type) = @_;
412             my $tmpdir = $self->tmpdir;
413             if (!defined($self->_encryption_key)) {
414             croak "Can't encrypt without a key: no urn:uuid: indetifier has been provided";
415             }
416              
417             my $key = $self->_encryption_key;
418             if (adobe_encrypt($src_filename, "$tmpdir/OPS/$filename", $key)) {
419             my $id = $self->nextid('id');
420             $self->manifest->add_item(
421             id => $id,
422             href => "$filename",
423             media_type => $type,
424             );
425             $self->add_encrypted_fileref("OPS/$filename");
426             return $id;
427             }
428             else {
429             carp ("Failed to copy $src_filename to $tmpdir/OPS/$filename");
430             }
431              
432             return;
433             }
434              
435              
436             sub nextid
437             {
438             my ($self, $prefix) = @_;
439             my $id;
440              
441             $prefix = 'id' unless(defined($prefix));
442             if (defined(${$self->id_counters}{$prefix})) {
443             $id = "$prefix" . ${$self->id_counters}{$prefix};
444             ${$self->id_counters}{$prefix}++;
445             }
446             else
447             {
448             # First usage of prefix
449             $id = "${prefix}1";
450             ${$self->id_counters}{$prefix} = 2;
451             }
452              
453             return $id;
454             }
455              
456             sub pack_zip
457             {
458             my ($self, $filename) = @_;
459             my $tmpdir = $self->tmpdir;
460             $self->write_ncx("$tmpdir/OPS/toc.ncx");
461             $self->write_opf("$tmpdir/OPS/content.opf");
462             my $container = EBook::EPUB::Container::Zip->new($filename);
463             $container->add_path($tmpdir . "/OPS", "OPS/");
464             $container->add_root_file("OPS/content.opf", "application/oebps-package+xml");
465             foreach my $fref ($self->encrypted_filerefs) {
466             $container->add_encrypted_path($fref);
467             }
468             return $container->write();
469             }
470              
471             sub write_opf
472             {
473             my ($self, $filename) = @_;
474             open F, ">:utf8", $filename or die "Failed to create OPF file: $filename";
475             my $xml = $self->to_xml();
476             print F $xml;
477             close F;
478             }
479              
480             sub write_ncx
481             {
482             my ($self, $filename) = @_;
483             open F, ">:utf8", $filename or die "Failed to create NCX file: $filename";
484             my $xml = $self->ncx->to_xml();
485             print F $xml;
486             close F;
487             }
488              
489              
490             # helper function that performs Adobe content protection "encryption"
491             sub adobe_encrypt
492             {
493             my ($src, $dst, $key) = @_;
494             my @key_bytes = unpack "C*", $key;
495              
496             # open source/destination files for read/write
497             open (IN, "< $src") or return;
498             if (!open (OUT, "> $dst")) {
499             close IN;
500             return;
501             }
502              
503             binmode IN;
504             binmode OUT;
505              
506             # XOR first 1024 bytes of file by provided key
507             my $data;
508             read(IN, $data, 1024);
509             my @bytes = unpack ("C*", $data);
510             my $key_ptr = 0;
511             foreach my $d (@bytes) {
512             $d = $d ^ $key_bytes[$key_ptr];
513             $key_ptr += 1;
514             $key_ptr = $key_ptr % @key_bytes;
515             }
516              
517             my $crypted_data = pack "C*", @bytes;
518             print OUT $crypted_data;
519              
520             # Copy th erest of the file, 1M buffer seems to be reasonable default
521             while (read(IN, $data, 1024*1024)) {
522             print OUT $data;
523             }
524              
525             close IN;
526             close OUT;
527             }
528              
529             sub mkdir_and_copy {
530             my ($from, $to) = @_;
531             mkpath(dirname($to));
532             return copy($from, $to);
533             }
534              
535             no Moose;
536             __PACKAGE__->meta->make_immutable;
537              
538             1;
539              
540             __END__
541             =head1 NAME
542              
543             EBook::EPUB - module for generating EPUB documents
544              
545             =head1 VERSION
546              
547             Version 0.6
548              
549              
550             =head1 SYNOPSIS
551              
552             use EBook::EPUB;
553              
554             # Create EPUB object
555             my $epub = EBook::EPUB->new;
556              
557             # Set metadata: title/author/language/id
558             $epub->add_title('Three Men in a Boat');
559             $epub->add_author('Jerome K. Jerome');
560             $epub->add_language('en');
561             $epub->add_identifier('1440465908', 'ISBN');
562              
563             # Add package content: stylesheet, font, xhtml and cover
564             $epub->copy_stylesheet('/path/to/style.css', 'style.css');
565             $epub->copy_file('/path/to/figure1.png',
566             'figure1.png', 'image/png');
567             $epub->encrypt_file('/path/to/CharisSILB.ttf',
568             'CharisSILB.ttf', 'application/x-font-ttf');
569             my $chapter_id = $epub->copy_xhtml('/path/to/page1.xhtml',
570             'page1.xhtml');
571             $epub->copy_xhtml('/path/to/notes.xhtml', 'notes.xhtml',
572             linear => 'no'
573             );
574              
575             # Add top-level nav-point
576             my $navpoint = $epub->add_navpoint(
577             label => "Chapter 1",
578             id => $chapter_id,
579             content => 'page1.xhtml',
580             play_order => 1 # should always start with 1
581             );
582              
583             # Add cover image
584             # Not actual epub standart but does the trick for iBooks
585             my $cover_id = $epub->copy_image('/path/to/cover.jpg', 'cover.jpg');
586             $epub->add_meta_item('cover', $cover_id);
587              
588             # Generate resulting ebook
589             $epub->pack_zip('/path/to/three_men_in_a_boat.epub');
590              
591             =head1 SUBROUTINES/METHODS
592              
593             =over 4
594              
595             =item new([$params])
596              
597             Create an EBook::EPUB object
598              
599             =item add_title($title)
600              
601             Set the title of the book
602              
603             =item add_identifier($id, [$scheme])
604              
605             Set a unique identifier for the book, such as its ISBN or a URL
606              
607             =item add_author($name, [$formal_name])
608              
609             Add author of the document. For details see add_contributor.
610              
611             =item add_creator($name, [fileas =E<gt> $formal_name, role =E<gt> $role])
612              
613             Add primary creator or author of the publication of the publication. See
614             add_contributor for details
615              
616              
617             =item add_contributor($name, [fileas =E<gt> $formal_name, role =E<gt>])
618              
619             Add person/organization that contributed to publication. $name is the name in
620             human-readable form, e.g. "Arthur Conan Doyle", $formal_name is in form,
621             suitable for machine processing, e.g. "Doyle, Arthur Conan". $role reflects
622             kind of contribution to document. See Section 2.2.6 of OPF specification for
623             list of possible values L<http://www.idpf.org/2007/opf/OPF_2.0_final_spec.html#Section2.2.6>
624              
625             =item add_coverage($coverage)
626              
627             The extent or scope of the content of the resource.
628              
629             =item add_date($date, [$event])
630              
631             Date of publication, in the format defined by "Date and Time Formats" at
632             http://www.w3.org/TR/NOTE-datetime and by ISO 8601 on which it is based. In
633             particular, dates without times are represented in the form YYYY[-MM[-DD]]: a
634             required 4-digit year, an optional 2-digit month, and if the month is given, an
635             optional 2-digit day of month. $event is an optional description of event that
636             date refers to. Possible values may include: creation, publication, and
637             modification.
638              
639             =item add_description($description)
640              
641             Add description of the publication content
642              
643             =item add_format($format)
644              
645             The media type or dimensions of the resource. Best practice is to use a value from a controlled vocabulary (e.g. MIME media types).
646              
647             =item add_language($lang)
648              
649             Add language of the content of the publication. $lang must comply with RFC 3066
650             (see http://www.ietf.org/rfc/rfc3066.txt)
651              
652             =item add_publisher($publisher)
653              
654             An entity responsible for making the resource available
655              
656             =item add_relation($relation)
657              
658             An identifier of an auxiliary resource and its relationship to the publication.
659              
660             =item add_rights($rights)
661              
662             A statement about rights, or a reference to one. In this specification, the copyright notice and any further rights description should appear directly.
663              
664             =item add_source($source)
665              
666             Information regarding a prior resource from which the publication was derived
667              
668             =item add_subject($subject)
669              
670             Add subject of the publication
671              
672             =item add_translator($name, [$formal_name])
673              
674             Add translator of the document. $name is in human-readable form, e.g. "Arthur
675             Conan Doyle", $formal_name is in form, suitable for machine processing, e.g.
676             "Doyle, Arthur Conan"
677              
678             =item add_type($type)
679              
680             type includes terms describing general categories, functions, genres, or
681             aggregation levels for content. The advised best practice is to select a value
682             from a controlled vocabulary.
683              
684             =item add_navpoint(%opts)
685              
686             Add refrence to an OPS Content Document that is a part of publication. %opts is
687             an anonymous hash, for possible key values see L<EBook::EPUB::NCX::NavPoint>.
688             Method returns created EBook::EPUB::NCX::NavPoint object that could be used
689             later for adding subsections.
690              
691             =item add_meta_item($name, $value)
692              
693             Add non-standard item to metadata e.g. metadata from source documetn that is not described in Doublin Core spec.
694              
695             =item add_xhtml($filename, $data, %opts)
696              
697             Add xhtml data $data to $filename in package. Returns id of newly added entry.
698              
699             %opts is an anonymous hash array of parameters:
700              
701             =over 8
702              
703             =item linear
704              
705             'yes' or 'no'
706              
707             =back
708              
709             =item add_stylesheet($filename, $data)
710              
711             Add stylesheet data $data as $filename in package. Returns id of newly added entry.
712              
713             =item add_image($filename, $data, $type)
714              
715             Add image data $data as $filename in package with content type $type (e.g. image/jpeg). Returns id of newly added entry.
716              
717             =item copy_xhtml($source_file, $filename, %opts)
718              
719             Add existing xhtml file $source_file as $filename in package. Returns id of newly added entry.
720              
721             %opts is an anonymous hash array of parameters:
722              
723             =over 8
724              
725             =item linear
726              
727             'yes' or 'no'
728              
729             =back
730              
731             =item copy_stylesheet($source_file, $filename)
732              
733             Add existing css file $source_file as $filename in package. Returns id of newly added entry.
734              
735             =item copy_image($source_file, $filename, $type)
736              
737             Add existing image file $source_file as $filename in package and set its content type to $type (e.g. image/jpeg). Returns id of newly added entry.
738              
739             =item copy_file($source_file, $filename, $type)
740              
741             Add existing file $source_file as $filename in package and set its content type to $type (e.g. text/plain). Returns id of newly created entry. Returns id of newly added entry.
742              
743             =item encrypt_file($source_file, $filename, $type)
744              
745             Add existing file $source_file as $filename in package and set its content type to $type (e.g. text/plain) Apply Adobe copy protection scheme to this file using book UUID as a key. Function croaks if key has not been set previously using. Returns id of newly added entry.
746              
747             =item pack_zip($filename)
748              
749             Generate OCF Zip container with contents of current package
750              
751             =back
752              
753             =head1 AUTHOR
754              
755             Oleksandr Tymoshenko, E<lt>gonzo@bluezbox.comE<gt>
756              
757             =head1 BUGS
758              
759             Please report any bugs or feature requests to E<lt>gonzo@bluezbox.comE<gt>
760              
761             =head1 LICENSE AND COPYRIGHT
762              
763             Copyright 2009, 2010 Oleksandr Tymoshenko.
764              
765             L<http://bluezbox.com>
766              
767             This module is free software; you can redistribute it and/or
768             modify it under the terms of the BSD license. See the F<LICENSE> file
769             included with this distribution.