File Coverage

blib/lib/EBook/Ishmael/EBook/FictionBook2.pm
Criterion Covered Total %
statement 128 156 82.0
branch 44 74 59.4
condition 6 10 60.0
subroutine 18 18 100.0
pod 0 9 0.0
total 196 267 73.4


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