File Coverage

blib/lib/EBook/Ishmael/EBook/FictionBook2.pm
Criterion Covered Total %
statement 142 164 86.5
branch 47 78 60.2
condition 5 8 62.5
subroutine 20 20 100.0
pod 0 9 0.0
total 214 279 76.7


line stmt bran cond sub pod time code
1             package EBook::Ishmael::EBook::FictionBook2;
2 17     17   280 use 5.016;
  17         49  
3             our $VERSION = '2.05';
4 17     17   74 use strict;
  17         23  
  17         311  
5 17     17   49 use warnings;
  17         21  
  17         642  
6              
7 17     17   58 use File::Spec;
  17         23  
  17         259  
8 17     17   6356 use MIME::Base64;
  17         10061  
  17         1024  
9              
10 17     17   92 use XML::LibXML;
  17         21  
  17         97  
11              
12 17     17   2247 use EBook::Ishmael::HTML qw(prepare_html);
  17         21  
  17         554  
13 17     17   57 use EBook::Ishmael::ImageID qw(mimetype_id);
  17         23  
  17         438  
14 17     17   74 use EBook::Ishmael::EBook::Metadata;
  17         23  
  17         252  
15 17     17   49 use EBook::Ishmael::Time qw(guess_time);
  17         22  
  17         24013  
16              
17             my $NS = "http://www.gribuser.ru/xml/fictionbook/2.0";
18              
19             sub heuristic {
20              
21 104     104 0 199 my $class = shift;
22 104         143 my $file = shift;
23 104         135 my $fh = shift;
24              
25 104 100       328 return 1 if $file =~ /\.fb2$/;
26 93 100       1390 return 0 unless -T $fh;
27              
28 35         84 read $fh, my ($head), 1024;
29              
30 35         456 return $head =~ /<\s*FictionBook[^<>]+xmlns\s*=\s*"\Q$NS\E"[^<>]*>/;
31              
32             }
33              
34             sub _read_metadata {
35              
36 10     10   14 my $self = shift;
37              
38 10         97 my $ns = $self->{_dom}->documentElement->namespaceURI;
39              
40 10         33 my $xpc = XML::LibXML::XPathContext->new($self->{_dom});
41 10         246 $xpc->registerNs('FictionBook', $ns);
42              
43 10 50       59 my ($desc) = $xpc->findnodes(
44             '/FictionBook:FictionBook' .
45             '/FictionBook:description'
46             ) or return 1;
47              
48 10         384 my ($title) = $xpc->findnodes('./FictionBook:title-info', $desc);
49 10         266 my ($doc) = $xpc->findnodes('./FictionBook:document-info', $desc);
50 10         210 my ($publish) = $xpc->findnodes('./FictionBook:publish-info', $desc);
51              
52 10 50       199 if (defined $title) {
53 10         28 for my $n ($title->childNodes) {
54 110         474 my $name = $n->nodeName;
55 110 100 66     320 if ($name eq 'genre') {
    100          
    100          
    100          
    50          
56 10         49 $self->{Metadata}->add_genre($n->textContent);
57             } elsif ($name eq 'author') {
58             $self->{Metadata}->add_author(
59 10         20 join(' ', grep { /\S/ } map { $_->textContent } $n->childNodes)
  20         86  
  20         114  
60             );
61             } elsif ($name eq 'book-title') {
62 10         34 $self->{Metadata}->set_title($n->textContent);
63             } elsif ($name eq 'lang' or $name eq 'src-lang') {
64 10         32 $self->{Metadata}->add_language($n->textContent);
65             } elsif ($name eq 'translator') {
66 0         0 $self->{Metadata}->add_contributor($n->textContent);
67             }
68             }
69             }
70              
71 10 50       21 if (defined $doc) {
72 10         352 for my $n ($doc->childNodes) {
73 110         379 my $name = $n->nodeName;
74 110 100       294 if ($name eq 'author') {
    100          
    100          
    100          
    100          
    50          
75             $self->{Metadata}->add_contributor(
76 10         19 join(' ', grep { /\S/ } map { $_->textContent } $n->childNodes)
  20         58  
  20         79  
77             );
78             } elsif ($name eq 'program-used') {
79 10         30 $self->{Metadata}->set_software($n->textContent);
80             } elsif ($name eq 'date') {
81 10         14 my $t = eval { guess_time($n->textContent) };
  10         35  
82 10 50       425 if (defined $t) {
83 10         28 $self->{Metadata}->set_created($t);
84             }
85             } elsif ($name eq 'id') {
86 10         35 $self->{Metadata}->set_id($n->textContent);
87             } elsif ($name eq 'version') {
88 10         39 $self->{Metadata}->set_format("FictionBook2 " . $n->textContent);
89             } elsif ($name eq 'src-ocr') {
90 0         0 $self->{Metadata}->add_author($n->textContent);
91             }
92             }
93             }
94              
95 10 50       31 if (defined $publish) {
96 10         304 for my $n ($publish->childNodes) {
97 30         105 my $name = $n->nodeName;
98 30 50 66     107 if ($name eq 'year' and not defined $self->{Metadata}->created) {
    50          
    50          
99 0         0 my $t = eval { guess_time($n->textContent) };
  0         0  
100 0 0       0 if (defined $t) {
101 0         0 $self->{Metadata}->set_created($t);
102             }
103             } elsif ($name eq 'publisher') {
104 0         0 $self->{Metadata}->add_contributor($n->textContent);
105             } elsif ($name eq 'book-name') {
106 0         0 $self->{Metadata}->set_title($n->textContent);
107             }
108             }
109             }
110              
111 10         22 for my $n ($xpc->findnodes('/FictionBook:FictionBook/FictionBook:binary')) {
112 10         382 my $mime = $n->getAttribute('content-type');
113 10 50       91 next if not defined $mime;
114 10         47 my $format = mimetype_id($mime);
115 10 50       19 next if not defined $format;
116 10         33 push @{ $self->{_images} }, [ $n, $format ];
  10         32  
117             }
118              
119 10         22 my ($covmeta) = $xpc->findnodes('./FictionBook:coverpage', $title);
120             # Put if code inside own block so we can easily last out of it.
121 10 50       245 if (defined $covmeta) {{
122 10 50       13 my ($img) = $xpc->findnodes('./FictionBook:image', $covmeta)
  10         17  
123             or last;
124 10 50       244 my $href = $img->getAttribute('l:href') or last;
125 10         102 $href =~ s/^#//;
126 10 50       28 my ($binary) = $xpc->findnodes(
127             "/FictionBook:FictionBook/FictionBook:binary[\@id=\"$href\"]"
128             ) or last;
129 10         272 my $mime = $binary->getAttribute('content-type');
130 10 50       63 last if not defined $mime;
131 10         113 my $format = mimetype_id($mime);
132 10 50       17 last if not defined $format;
133 10         33 $self->{_cover} = [ $binary, $format ];
134             }}
135              
136 10         83 return 1;
137              
138             }
139              
140             sub new {
141              
142 10     10 0 13 my $class = shift;
143 10         12 my $file = shift;
144 10         12 my $enc = shift;
145 10   50     35 my $net = shift // 1;
146              
147 10         56 my $self = {
148             Source => undef,
149             Metadata => EBook::Ishmael::EBook::Metadata->new,
150             Network => $net,
151             _dom => undef,
152             _cover => undef,
153             _images => [],
154             };
155              
156 10         15 bless $self, $class;
157              
158 10         236 $self->{Source} = File::Spec->rel2abs($file);
159              
160             $self->{_dom} = XML::LibXML->load_xml(
161             location => $file,
162             no_network => !$self->{Network},
163 10         64 );
164              
165 10         7931 $self->_read_metadata;
166              
167 10 50       296 if (not defined $self->{Metadata}->format) {
168 0         0 $self->{Metadata}->set_format('FictionBook2');
169             }
170              
171 10         31 return $self;
172              
173             }
174              
175             sub html {
176              
177 3     3 0 7 my $self = shift;
178 3         4 my $out = shift;
179              
180 3         33 my $ns = $self->{_dom}->documentElement->namespaceURI;
181              
182 3         9 my $xpc = XML::LibXML::XPathContext->new($self->{_dom});
183 3         60 $xpc->registerNs('FictionBook', $ns);
184              
185 3 50       8 my @bodies = $xpc->findnodes(
186             '/FictionBook:FictionBook' .
187             '/FictionBook:body'
188             ) or die "Invalid FictionBook2 file $self->{Source}\n";
189 3         105 prepare_html(@bodies);
190              
191             my $html = join '',
192 9         727 map { $_->toString }
193 3         6 map { $_->childNodes }
  3         10  
194             @bodies;
195              
196 3 50       17 if (defined $out) {
197 0 0       0 open my $fh, '>', $out
198             or die "Failed to open $out for writing: $!\n";
199 0         0 binmode $fh, ':utf8';
200 0         0 print { $fh } $html;
  0         0  
201 0         0 close $fh;
202 0         0 return $out;
203             } else {
204 3         42 return $html;
205             }
206              
207             }
208              
209             sub raw {
210              
211 2     2 0 3 my $self = shift;
212 2         3 my $out = shift;
213              
214 2         22 my $ns = $self->{_dom}->documentElement->namespaceURI;
215              
216 2         6 my $xpc = XML::LibXML::XPathContext->new($self->{_dom});
217 2         39 $xpc->registerNs('FictionBook', $ns);
218              
219 2 50       5 my @bodies = $xpc->findnodes(
220             '/FictionBook:FictionBook' .
221             '/FictionBook:body'
222             ) or die "Invalid FictionBook2 file $self->{Source}\n";
223 2         64 prepare_html(@bodies);
224              
225 2         5 my $raw = join '', map { $_->textContent } @bodies;
  2         111  
226              
227 2 50       8 if (defined $out) {
228 0 0       0 open my $fh, '>', $out
229             or die "Failed to open $out for writing: $!\n";
230 0         0 binmode $fh, ':utf8';
231 0         0 print { $fh } $raw;
  0         0  
232 0         0 close $fh;
233 0         0 return $out;
234             } else {
235 2         6 return $raw;
236             }
237              
238             }
239              
240             sub metadata {
241              
242 4     4 0 1734 my $self = shift;
243              
244 4         15 return $self->{Metadata};
245              
246             }
247              
248             sub has_cover {
249              
250 4     4 0 1231 my $self = shift;
251              
252 4         29 return defined $self->{_cover};
253              
254             }
255              
256             sub cover {
257              
258 2     2 0 4 my $self = shift;
259              
260 2 50       6 return (undef, undef) unless $self->has_cover;
261 2         236 my $bin = decode_base64($self->{_cover}[0]->textContent);
262 2         12 return ($bin, $self->{_cover}[1]);
263              
264             }
265              
266             sub image_num {
267              
268 4     4 0 3758 my $self = shift;
269              
270 4         7 return scalar @{ $self->{_images} };
  4         17  
271              
272             }
273              
274             sub image {
275              
276 2     2 0 4 my $self = shift;
277 2         4 my $n = shift;
278              
279 2 50       7 if ($n >= $self->image_num) {
280 0         0 return (undef, undef);
281             }
282              
283 2         291 my $img = decode_base64($self->{_images}[$n][0]->textContent);
284 2         29 return ($img, $self->{_images}[$n][1]);
285              
286             }
287              
288             1;