File Coverage

blib/lib/EBook/Ishmael/EBook/Mobi.pm
Criterion Covered Total %
statement 386 472 81.7
branch 70 138 50.7
condition 16 38 42.1
subroutine 38 44 86.3
pod 0 10 0.0
total 510 702 72.6


line stmt bran cond sub pod time code
1             package EBook::Ishmael::EBook::Mobi;
2 17     17   11263 use 5.016;
  17         75  
3             our $VERSION = '2.01';
4 17     17   115 use strict;
  17         68  
  17         606  
5 17     17   115 use warnings;
  17         35  
  17         1125  
6              
7 17     17   106 use Encode qw(from_to);
  17         36  
  17         1393  
8              
9 17     17   124 use XML::LibXML;
  17         29  
  17         149  
10              
11 17     17   12917 use EBook::Ishmael::Decode qw(palmdoc_decode);
  17         62  
  17         1450  
12 17     17   144 use EBook::Ishmael::ImageID;
  17         40  
  17         1166  
13 17     17   10379 use EBook::Ishmael::PDB;
  17         64  
  17         853  
14 17     17   9402 use EBook::Ishmael::MobiHuff;
  17         57  
  17         903  
15 17     17   139 use EBook::Ishmael::Time qw(guess_time);
  17         43  
  17         117731  
16              
17             # Many thanks to Tommy Persson, the original author of mobi2html, a script
18             # which much of this code is based off of.
19              
20             # TODO: Implement AZW4 support
21             # TODO: Add support for UTF16 MOBIs (65002)
22              
23             my $TYPE = 'BOOK';
24             my $CREATOR = 'MOBI';
25              
26             my $RECSIZE = 4096;
27              
28             my $NULL_INDEX = 0xffffffff;
29              
30             sub heuristic {
31              
32 70     70 0 155 my $class = shift;
33 70         187 my $file = shift;
34 70         133 my $fh = shift;
35              
36 70 50       1057 return 0 unless -s $file >= 68;
37              
38 70         420 seek $fh, 32, 0;
39 70         630 read $fh, my ($null), 1;
40              
41 70 100       295 unless ($null eq "\0") {
42 35         299 return 0;
43             }
44              
45 35         238 seek $fh, 60, 0;
46 35         252 read $fh, my ($type), 4;
47 35         86 read $fh, my ($creator), 4;
48              
49 35 100 66     258 return 0 unless $type eq $TYPE && $creator eq $CREATOR;
50              
51 11         81 seek $fh, 78, 0;
52 11         74 read $fh, my ($off), 4;
53 11         38 $off = unpack "N", $off;
54 11         83 seek $fh, $off + 36, 0;
55 11         77 read $fh, my ($ver), 4;
56 11         39 $ver = unpack "N", $ver;
57              
58 11         69 return $ver != 8;
59              
60             }
61              
62             sub _clean_html {
63              
64 5     5   12 my $html = shift;
65              
66 5         235 $$html =~ s/
67 5         304 $$html =~ s/
68 5         368 $$html =~ s/<\/mbp:pagebreak>//g;
69 5         96 $$html =~ s/.*?<\/guide>//g;
70 5         102 $$html =~ s/<\/?mbp:nu>//g;
71 5         261 $$html =~ s/<\/?mbp:section//g;
72 5         72 $$html =~ s/<\/?mbp:frameset>//g;
73 5         64 $$html =~ s/<\/?mbp:slave-frame>//g;
74              
75 5         15 return 1;
76              
77             }
78              
79             sub _trailing_entry_size {
80              
81 0     0   0 my $data = shift;
82              
83 0         0 my $res = 0;
84              
85 0         0 my $trail = substr $data, -4;
86              
87 0         0 for my $c (unpack "C4", $trail) {
88 0 0       0 if ($c & 0x80) {
89 0         0 $res = 0;
90             }
91 0         0 $res = ($res << 7) | ($c & 0x7f);
92             }
93              
94 0         0 return $res;
95              
96             }
97              
98             sub _trailing_entries_size {
99              
100 138     138   225 my $self = shift;
101 138         242 my $data = shift;
102              
103 138         266 my $res = 0;
104              
105 138         417 for my $i (0 .. $self->{_trailers} - 1) {
106 0         0 my $n = _trailing_entry_size($data);
107 0         0 $res += $n;
108 0         0 substr $data, -$n, $n, '';
109             }
110              
111 138 50       383 if ($self->{_extra_data} & 1) {
112 138         358 $res += (ord(substr $data, -1) & 3) + 1;
113             }
114              
115 138         287 return $res;
116              
117             }
118              
119             # Index processing code was adapted from KindleUnpack
120              
121             sub _get_index_data {
122              
123 20     20   36 my $self = shift;
124 20         36 my $idx = shift;
125              
126 20 50       51 return {} if $idx == $NULL_INDEX;
127              
128 20         39 my $outtbl = [];
129 20         37 my $ctoc = {};
130              
131 20         56 my $data;
132 20         100 $$data = $self->{_pdb}->record($idx)->data;
133              
134 20         79 my ($idxhdr, $hordt1, $hordt2) = $self->_parse_indx_header($data);
135 20         44 my $icount = $idxhdr->{count};
136 20         37 my $roff = 0;
137 20         39 my $off = $idx + $icount + 1;
138              
139 20         68 for my $i (0 .. $idxhdr->{nctoc} - 1) {
140 10         50 my $cdata = $self->{_pdb}->record($off + $i)->data;
141 10         53 my $ctocdict = $self->_read_ctoc(\$cdata);
142 10         66 for my $j (sort keys %$ctocdict) {
143 0         0 $ctoc->{ $j + $roff } = $ctocdict->{ $j };
144             }
145 10         32 $roff += 0x10000;
146             }
147              
148 20         48 my $tagstart = $idxhdr->{len};
149 20         62 my ($ctrlcount, $tagtbl) = _read_tag_section($tagstart, $data);
150              
151 20         76 for my $i ($idx + 1 .. $idx + 1 + $icount - 1) {
152 20         111 my $d = $self->{_pdb}->record($i)->data;
153 20         71 my ($hdrinfo, $ordt1, $ordt2) = $self->_parse_indx_header(\$d);
154 20         45 my $idxtpos = $hdrinfo->{start};
155 20         40 my $ecount = $hdrinfo->{count};
156 20         34 my $idxposits = [];
157 20         73 for my $j (0 .. $ecount - 1) {
158 100         220 my $pos = unpack "n", substr $d, $idxtpos + 4 + (2 * $j), 2;
159 100         209 push @$idxposits, $pos;
160             }
161 20         53 for my $j (0 .. $ecount - 1) {
162 100         180 my $spos = $idxposits->[$j];
163 100         215 my $epos = $idxposits->[$j + 1];
164 100         214 my $txtlen = ord(substr $d, $spos, 1);
165 100         241 my $txt = substr $d, $spos + 1, $txtlen;
166 100 50       336 if (@$hordt2) {
167             $txt = join '',
168 0         0 map { chr $hordt2->[ ord $_ ] }
  0         0  
169             split //, $txt;
170             }
171 100         336 my $tagmap = _get_tagmap(
172             $ctrlcount,
173             $tagtbl,
174             \$d,
175             $spos + 1 + $txtlen,
176             $epos
177             );
178 100         517 push @$outtbl, [ $txt, $tagmap ];
179             }
180             }
181              
182 20         206 return ( $outtbl, $ctoc );
183              
184             }
185              
186             sub _parse_indx_header {
187              
188 40     40   69 my $self = shift;
189 40         63 my $data = shift;
190              
191 40 50       138 unless (substr($$data, 0, 4) eq 'INDX') {
192 0         0 die "Index section is not INDX\n";
193             }
194              
195 40         218 my @words = qw(
196             len nul1 type gen start count code lng total ordt ligt nligt nctoc
197             );
198 40         91 my $num = scalar @words;
199 40         198 my @values = unpack "N$num", substr $$data, 4, 4 * $num;
200 40         77 my $header = {};
201              
202 40         119 for my $i (0 .. $#words) {
203 520         1359 $header->{ $words[$i] } = $values[$i];
204             }
205              
206 40         85 my $ordt1 = [];
207 40         62 my $ordt2 = [];
208              
209             my (
210 40         193 $ocnt,
211             $oentries,
212             $op1,
213             $op2,
214             $otagx
215             ) = unpack "N N N N N", substr $$data, 0xa4, 4 * 5;
216              
217 40 50 33     296 if ($header->{code} == 0xfdea or $ocnt != 0 or $oentries > 0) {
      33        
218              
219 0 0       0 unless ($ocnt == 1) {
220 0         0 die "Corrupted INDX record\n";
221             }
222 0 0       0 unless (substr($$data, $op1, 4) eq 'ORDT') {
223 0         0 die "Corrupted INDX record\n";
224             }
225 0 0       0 unless (substr($$data, $op2, 4) eq 'ORDT') {
226 0         0 die "Corrupted INDX record\n";
227             }
228              
229             $ordt1 = [
230 0         0 unpack("C$oentries", substr $$data, $op1 + 4, $oentries)
231             ];
232 0         0 $ordt2 = [
233             unpack("n$oentries", substr $$data, $op2 + 4, $oentries * 2)
234             ];
235              
236             }
237              
238 40         205 return ( $header, $ordt1, $ordt2 );
239              
240             }
241              
242             sub _read_ctoc {
243              
244 10     10   21 my $self = shift;
245 10         19 my $data = shift;
246              
247 10         20 my $ctoc = {};
248              
249 10         20 my $off = 0;
250 10         27 my $len = length $$data;
251              
252 10         31 while ($off < $len) {
253 40 100       130 if (substr($$data, $off, 1) eq "\0") {
254 10         26 last;
255             }
256              
257 30         50 my $idxoff = $off;
258              
259 30         68 my ($pos, $ilen) = _vwv($data, $off);
260 30         51 $off += $pos;
261              
262 30         66 my $name = substr $$data, $off, $ilen;
263 30         47 $off += $ilen;
264              
265 30         156 my $ctoc->{ $idxoff } = $name;
266              
267             }
268              
269 10         24 return $ctoc;
270              
271             }
272              
273             sub _vwv {
274              
275 560     560   883 my $data = shift;
276 560         810 my $off = shift;
277              
278 560         829 my $value = 0;
279 560         863 my $consume = 0;
280 560         784 my $fin = 0;
281              
282 560         1214 while (!$fin) {
283 810         1557 my $v = substr $$data, $off + $consume, 1;
284 810         1147 $consume++;
285 810 100       1752 if (ord($v) & 0x80) {
286 560         796 $fin = 1;
287             }
288 810         1970 $value = ($value << 7) | (ord($v) & 0x7f);
289             }
290              
291 560         1296 return ( $consume, $value );
292              
293             }
294              
295             sub _read_tag_section {
296              
297 20     20   62 my $start = shift;
298 20         36 my $data = shift;
299              
300 20         53 my $ctrlcount = 0;
301              
302 20         33 my $tags = [];
303              
304 20 50       69 if (substr($$data, $start, 4) eq 'TAGX') {
305 20         65 my $foff = unpack "N", substr $$data, $start + 4, 4;
306 20         49 $ctrlcount = unpack "N", substr $$data, $start + 8, 4;
307 20         56 for (my $i = 12; $i < $foff; $i += 4) {
308 80         127 my $pos = $start + $i;
309 80         336 push @$tags, [ unpack "C4", substr $$data, $pos, 4 ];
310             }
311             }
312              
313 20         69 return ( $ctrlcount, $tags );
314              
315             }
316              
317             sub _count_setbits {
318              
319 280     280   430 my $val = shift;
320 280   50     852 my $bits = shift // 8;
321              
322 280         417 my $count = 0;
323 280         585 for my $i (0 .. $bits - 1) {
324 2240 100       4295 if (($val & 0x01) == 0x01) {
325 280         424 $count++;
326             }
327 2240         3461 $val >>= 1;
328             }
329              
330 280         718 return $count;
331              
332             }
333              
334             sub _get_tagmap {
335              
336 100     100   170 my $ctrlcount = shift;
337 100         150 my $tagtbl = shift;
338 100         158 my $entry = shift;
339 100         139 my $spos = shift;
340 100         186 my $epos = shift;
341              
342 100         170 my $tags = [];
343 100         174 my $tagmap = {};
344 100         155 my $ctrli = 0;
345 100         176 my $start = $spos + $ctrlcount;
346              
347 100         232 for my $t (@$tagtbl) {
348 440         979 my ($tag, $values, $mask, $endflag) = @$t;
349 440 100       962 if ($endflag == 1) {
350 100         168 $ctrli++;
351 100         234 next;
352             }
353 340         729 my $cbyte = ord(substr $$entry, $spos + $ctrli, 1);
354 340         609 my $val = $cbyte & $mask;
355 340 50       735 if ($val != 0) {
356 340 100       679 if ($val == $mask) {
357 280 50       525 if (_count_setbits($mask) > 1) {
358 0         0 my ($consume, $val) = _vwv($entry, $start);
359 0         0 $start += $consume;
360 0         0 push @$tags, [ $tag, undef, $val, $values ];
361             } else {
362 280         902 push @$tags, [ $tag, 1, undef, $values ];
363             }
364             } else {
365 60         156 while (($mask & 0x01) == 0) {
366 60         88 $mask >>= 1;
367 60         163 $val >>= 1;
368             }
369 60         313 push @$tags, [ $tag, $val, undef, $values ];
370             }
371             }
372             }
373              
374 100         184 for my $t (@$tags) {
375 340         796 my ($tag, $count, $bytes, $per_entry) = @$t;
376 340         565 my $values = [];
377 340 50       691 if (defined $count) {
378 340         1149 for my $i (1 .. $count) {
379 400         806 for my $j (1 .. $per_entry) {
380 530         1185 my ($consume, $data) = _vwv($entry, $start);
381 530         1107 $start += $consume;
382 530         1340 push @$values, $data;
383             }
384             }
385             } else {
386 0         0 my $constotal = 0;
387 0         0 while ($constotal < $bytes) {
388 0         0 my ($consume, $data) = _vwv($entry, $start);
389 0         0 $start += $consume;
390 0         0 push @$values, $data;
391             }
392             # Should we warn if $constotal does not match $bytes?
393             }
394 340         1008 $tagmap->{ $tag } = $values;
395             }
396              
397 100         460 return $tagmap;
398              
399             }
400              
401             sub _kf8_init {
402              
403 10     10   21 my $self = shift;
404              
405 10 50       45 if ($self->{_fdst} != $NULL_INDEX) {
406 10         46 my $hdr = $self->{_pdb}->record($self->{_fdst})->data;
407 10 50       47 unless (substr($hdr, 0, 4) eq 'FDST') {
408 0         0 die "KF8 Mobi missing FDST info\n";
409             }
410 10         33 my $secnum = unpack "N", substr $hdr, 0x08, 4;
411 10         30 my $sc2 = $secnum * 2;
412 10         53 my @secs = unpack "N$sc2", substr $hdr, 12, 4 * $sc2;
413             $self->{_fdsttbl} = [
414 10         49 map({ $secs[$_] } grep { $_ % 2 == 0 } 0 .. $#secs)
  30         83  
  60         128  
415             ];
416 10         29 push @{ $self->{_fdsttbl} }, $self->{_textlen};
  10         53  
417             }
418              
419 10 50       39 if ($self->{_skelidx} != $NULL_INDEX) {
420 10         58 my ($outtbl, $ctoc) = $self->_get_index_data($self->{_skelidx});
421 10         24 my $fptr = 0;
422 10         68 for my $o (@$outtbl) {
423 30         65 my ($txt, $tagmap) = @$o;
424 30         112 push @{ $self->{_skeltbl} }, [
425 30         49 $fptr, $txt, $tagmap->{1}[0], $tagmap->{6}[0], $tagmap->{6}[1]
426             ];
427 30         106 $fptr++;
428             }
429             }
430              
431             # TODO: The $cdat is usually undef. Not too important as we don't use it
432             # for anything at the moment.
433 10 50       55 if ($self->{_fragidx} != $NULL_INDEX) {
434 10         45 my ($outtbl, $ctoc) = $self->_get_index_data($self->{_fragidx});
435 10         33 for my $o (@$outtbl) {
436 70         139 my ($txt, $tagmap) = @$o;
437 70         138 my $coff = $tagmap->{2}[0];
438 70         109 my $cdat = $ctoc->{ $coff };
439 70         365 push @{ $self->{_fragtbl} }, [
440             int($txt), $cdat, $tagmap->{3}[0], $tagmap->{4}[0],
441 70         104 $tagmap->{6}[0], $tagmap->{6}[1]
442             ];
443             }
444             }
445              
446 10 50       46 if ($self->{_guideidx} != $NULL_INDEX) {
447 0         0 my ($outtbl, $ctoc) = $self->_get_index_data($self->{_guideidx});
448 0         0 for my $o (@$outtbl) {
449 0         0 my ($txt, $tagmap) = @$o;
450 0         0 my $coff = $tagmap->{1}[0];
451 0         0 my $rtitle = $ctoc->{ $coff };
452 0         0 my $rtype = $txt;
453 0         0 my $fno;
454 0 0       0 if (exists $tagmap->{3}) {
455 0         0 $fno = $tagmap->{3}[0];
456             }
457 0 0       0 if (exists $tagmap->{6}) {
458 0         0 $fno = $tagmap->{6}[0];
459             }
460 0         0 push @{ $self->{_guidetbl} }, [ $rtype, $rtitle, $fno ];
  0         0  
461             }
462             }
463              
464 10         27 return 1;
465              
466             }
467              
468             sub _kf8_xhtml {
469              
470 5     5   14 my $self = shift;
471              
472 5         11 my @parts;
473              
474 5         26 my $rawml = $self->rawml;
475              
476             # xhtml is the first flow piece
477             my $source = substr(
478             $rawml,
479             $self->{_fdsttbl}[0],
480 5         199 $self->{_fdsttbl}[1] - $self->{_fdsttbl}[0]
481             );
482              
483 5         15 my $fragptr = 0;
484 5         12 my $baseptr = 0;
485              
486 5         10 for my $s (@{ $self->{_skeltbl} }) {
  5         24  
487             my (
488 15         48 $skelnum,
489             $skelnam,
490             $fragcnt,
491             $skelpos,
492             $skellen
493             ) = @$s;
494 15         26 my $baseptr = $skelpos + $skellen;
495 15         40 my $skeleton = substr $source, $skelpos, $skellen;
496 15         39 for my $i (0 .. $fragcnt - 1) {
497             my (
498             $inpos,
499             $idtxt,
500             $fnum,
501             $seqnum,
502             $spos,
503             $len
504 35         55 ) = @{ $self->{_fragtbl}[$fragptr] };
  35         259  
505 35         149 my $slice = substr $source, $baseptr, $len;
506 35         52 $inpos -= $skelpos;
507 35         259 my $head = substr $skeleton, 0, $inpos;
508 35         65 my $tail = substr $skeleton, $inpos;
509 35         358 $skeleton = $head . $slice . $tail;
510 35         79 $baseptr += $len;
511 35         67 $fragptr++;
512             }
513 15         74 push @parts, $skeleton;
514             }
515              
516 5         26 return @parts;
517              
518             }
519              
520             sub _decode_record {
521              
522 138     138   59482 my $self = shift;
523 138         230 my $rec = shift;
524              
525 138         249 $rec++;
526              
527 138         423 my $encode = $self->{_pdb}->record($rec)->data;
528 138         350 my $trail = $self->_trailing_entries_size($encode);
529 138         474 substr $encode, -$trail, $trail, '';
530              
531 138 50       411 if ($self->{_compression} == 1) {
    50          
    0          
532 0         0 return $encode;
533             } elsif ($self->{_compression} == 2) {
534 138         417 return palmdoc_decode($encode);
535             } elsif ($self->{_compression} == 17480) {
536 0         0 return $self->{_huff}->decode($encode);
537             }
538              
539             }
540              
541             # TODO: Could probably optimize this.
542             sub _read_exth {
543              
544 20     20   42 my $self = shift;
545 20         252 my $exth = shift;
546              
547             my %exth_records = (
548 20     20   105 100 => sub { $self->{Metadata}->add_author(shift) },
549 0     0   0 101 => sub { $self->{Metadata}->add_contributor(shift) },
550 0     0   0 103 => sub { $self->{Metadata}->set_description(shift) },
551 0     0   0 104 => sub { $self->{Metadata}->set_id(shift) },
552 0     0   0 105 => sub { $self->{Metadata}->add_genre(shift) },
553 20     20   56 106 => sub { $self->{Metadata}->set_created(eval { guess_time(shift) }) },
  20         167  
554 20     20   112 108 => sub { $self->{Metadata}->add_contributor(shift) },
555 0     0   0 114 => sub { $self->{Metadata}->set_format('MOBI ' . shift) },
556             201 => sub {
557 20 50   20   77 if (defined $self->{_imgrec}) {
558 20         76 $self->{_coverrec} = $self->{_imgrec} + unpack "N", shift;
559             }
560             },
561 20     20   156 524 => sub { $self->{Metadata}->add_language(shift) },
562 20         633 );
563              
564 20         128 my ($doctype, $len, $items) = unpack "a4 N N", $exth;
565              
566 20         52 my $pos = 12;
567              
568 20         71 for my $i (1 .. $items) {
569              
570 390         1215 my (undef, $size) = unpack "N N", substr $exth, $pos;
571 390         686 my $contlen = $size - 8;
572 390         1417 my ($id, undef, $content) = unpack "N N a$contlen", substr $exth, $pos;
573              
574 390 100       1003 if (exists $exth_records{ $id }) {
575 100         261 $exth_records{ $id }->($content);
576             }
577              
578 390         850 $pos += $size;
579              
580             }
581              
582 20         501 return 1;
583              
584             }
585              
586             sub new {
587              
588 20     20 0 49 my $class = shift;
589 20         44 my $file = shift;
590 20         59 my $enc = shift;
591 20   50     76 my $net = shift // 1;
592              
593 20         275 my $self = {
594             Source => undef,
595             Metadata => EBook::Ishmael::EBook::Metadata->new,
596             Network => $net,
597             _pdb => undef,
598             _compression => undef,
599             _textlen => undef,
600             _recnum => undef,
601             _recsize => undef,
602             _encryption => undef,
603             _doctype => undef,
604             _length => undef,
605             _type => undef,
606             _codepage => undef,
607             _uid => undef,
608             _version => undef,
609             _exth_flag => undef,
610             _extra_data => undef,
611             _trailers => 0,
612             _huff => undef,
613             _imgrec => undef,
614             _coverrec => undef,
615             _lastcont => undef,
616             _images => [],
617             # kf8 stuff
618             _skelidx => undef,
619             _skeltbl => [],
620             _fragidx => undef,
621             _fragtbl => [],
622             _guideidx => undef,
623             _guidetbl => [],
624             _fdst => undef,
625             _fdsttbl => [ 0, $NULL_INDEX ],
626             };
627              
628 20         68 bless $self, $class;
629              
630 20         978 $self->{Source} = File::Spec->rel2abs($file);
631              
632 20         225 $self->{_pdb} = EBook::Ishmael::PDB->new($file);
633              
634 20         116 my $hdr = $self->{_pdb}->record(0)->data;
635              
636             (
637             $self->{_compression},
638             undef,
639             $self->{_textlen},
640             $self->{_recnum},
641             $self->{_recsize},
642             $self->{_encryption},
643             undef,
644 20         181 ) = unpack "n n N n n n n", $hdr;
645              
646 20 50 33     193 unless (
      33        
647             $self->{_compression} == 1 or
648             $self->{_compression} == 2 or
649             $self->{_compression} == 17480
650             ) {
651 0         0 die "Mobi $self->{Source} uses an unsupported compression level\n";
652             }
653              
654 20 50       80 if ($self->{_recsize} != 4096) {
655 0         0 die "$self->{Source} is not a Mobi file\n";
656             }
657              
658 20 50       71 unless ($self->{_encryption} == 0) {
659 0         0 die "Cannot read encrypted Mobi $self->{Source}\n";
660             }
661              
662             (
663             $self->{_doctype},
664             $self->{_length},
665             $self->{_type},
666             $self->{_codepage},
667             $self->{_uid},
668             $self->{_version},
669 20         167 ) = unpack "a4 N N N N N", substr $hdr, 16, 4 * 6;
670              
671 20 50 33     138 unless ($self->{_codepage} == 1252 or $self->{_codepage} == 65001) {
672 0         0 die "Mobi $self->{Source} uses an unsupported text encoding\n";
673             }
674              
675             # Read some parts of the Mobi header that we care about.
676 20         79 my ($toff, $tlen) = unpack "N N", substr $hdr, 0x54, 8;
677 20         85 $self->{_imgrec} = unpack "N", substr $hdr, 0x6c, 4;
678 20         69 my ($hoff, $hcount) = unpack "N N", substr $hdr, 0x70, 8;
679 20         70 $self->{_exth_flag} = unpack "N", substr $hdr, 0x80, 4;
680 20         72 $self->{_lastcont} = unpack "n", substr $hdr, 0xc2, 2;
681 20         67 $self->{_extra_data} = unpack "n", substr $hdr, 0xf2, 2;
682              
683 20 50       83 if ($self->{_compression} == 17480) {
684              
685 0 0       0 unless ($EBook::Ishmael::MobiHuff::UNPACK_Q) {
686 0         0 die "Cannot read AZW $self->{Source}; perl does not support " .
687             "unpacking 64-bit integars\n";
688             }
689              
690 0         0 my @huffs = map { $self->{_pdb}->record($_)->data } ($hoff .. $hoff + $hcount - 1);
  0         0  
691 0         0 $self->{_huff} = EBook::Ishmael::MobiHuff->new(@huffs);
692             }
693              
694 20 50 33     153 if ($self->{_length} >= 0xe3 and $self->{_version} >= 5) {
695 20         51 my $flags = $self->{_extra_data};
696 20         118 while ($flags > 1) {
697 0 0       0 $self->{_trailers}++ if $flags & 2;
698 0         0 $flags >>= 1;
699             }
700             }
701              
702 20 100       104 if ($self->{_version} == 8) {
703 10         39 $self->{_fdst} = unpack "N", substr $hdr, 0xc0, 4;
704 10         31 $self->{_fragidx} = unpack "N", substr $hdr, 0xf8, 4;
705 10         29 $self->{_skelidx} = unpack "N", substr $hdr, 0xfc, 4;
706 10         32 $self->{_guideidx} = unpack "N", substr $hdr, 0x104, 4;
707 10         64 $self->_kf8_init;
708             }
709              
710 20 50       145 if ($self->{_lastcont} > $self->{_pdb}->recnum - 1) {
711 0         0 $self->{_lastcont} = $self->{_pdb}->recnum - 1;
712             }
713              
714 20 50       85 if ($self->{_imgrec} >= $self->{_lastcont}) {
715 0         0 undef $self->{_imgrec};
716             }
717              
718 20 50       72 if (defined $self->{_imgrec}) {
719 20         88 for my $i ($self->{_imgrec} .. $self->{_lastcont}) {
720 50         189 my $img = $self->{_pdb}->record($i)->data;
721 50         247 my $id = image_id(\$img);
722 50 100       143 push @{ $self->{_images} }, $i if defined $id;
  40         154  
723             }
724             }
725              
726 20 50       76 if ($self->{_exth_flag}) {
727 20         183 $self->_read_exth(substr $hdr, $self->{_length} + 16);
728             }
729              
730 20 50 33     155 if (
731             defined $self->{_coverrec} and
732 40         168 not grep { $self->{_coverrec} == $_ } @{ $self->{_images} }
  20         81  
733             ) {
734 0         0 undef $self->{_coverrec};
735             }
736              
737 20         168 $self->{Metadata}->set_title(substr $hdr, $toff, $tlen);
738              
739 20 50 33     89 if (
740             not defined $self->{Metadata}->created or
741             # If the PDB's created date is greater than the MOBI's EXTH one,
742             # probably means a corrupted EXTH date.
743             $self->{_pdb}->cdate > $self->{Metadata}->created
744             ) {
745 20         129 $self->{Metadata}->set_created($self->{_pdb}->cdate);
746             }
747              
748 20 50       73 if ($self->{_pdb}->mdate) {
749 20         74 $self->{Metadata}->set_modified($self->{_pdb}->mdate);
750             }
751              
752 20 100       100 if ($self->{_version} == 8) {
    50          
753 10         47 $self->{Metadata}->set_format('KF8');
754             } elsif (not defined $self->{Metadata}->format) {
755 10         45 $self->{Metadata}->set_format('MOBI');
756             }
757              
758 20         127 return $self;
759              
760             }
761              
762             sub rawml {
763              
764 10     10 0 28 my $self = shift;
765 10         44 my %param = @_;
766              
767 10   50     66 my $decode = $param{decode} // 0;
768 10   100     44 my $clean = $param{clean} // 0;
769              
770             my $cont =
771             join '',
772 115         305 map { $self->_decode_record($_) }
773 10         51 0 .. $self->{_recnum} - 1;
774              
775 10 100       92 _clean_html(\$cont) if $clean;
776              
777 10 50 33     61 if ($decode and $self->{_codepage} == 1252) {
778 0 0       0 from_to($cont, "cp1252", "utf-8")
779             or die "Failed to encode Mobi $self->{Source} text as utf-8\n";
780             }
781              
782 10         251 return $cont;
783              
784             }
785              
786             sub html {
787              
788 6     6 0 18 my $self = shift;
789 6         14 my $out = shift;
790              
791 6         13 my $html;
792              
793 6 100       26 if ($self->{_version} == 8) {
794              
795 3         23 for my $part ($self->_kf8_xhtml) {
796              
797             my $dom = XML::LibXML->load_html(
798             string => $part,
799             no_network => !$self->{Network},
800 9         7630 recover => 2,
801             );
802              
803 9 50       8142 my ($body) = $dom->findnodes('/html/body') or next;
804              
805 9         383 $html .= join '', map { $_->toString } $body->childNodes;
  1821         11448  
806              
807             }
808              
809             } else {
810              
811 3         11 my $rawml = $self->rawml(clean => 1);
812 3 50       82 my $enc = $self->{_codepage} == 1252 ? "cp1252" : "utf-8";
813             my $dom = XML::LibXML->load_html(
814             string => $rawml,
815             no_network => !$self->{Network},
816 3         37 encoding => $enc,
817             recover => 2
818             );
819 3         24004 $html = $dom->documentElement->toString;
820             }
821              
822 6 50       1165 if (defined $out) {
823 0 0       0 open my $fh, '>', $out
824             or die "Failed to open $out for writing: $!\n";
825 0         0 binmode $fh, ':utf8';
826 0         0 print { $fh } $html;
  0         0  
827 0         0 close $fh;
828 0         0 return $out;
829             } else {
830 6         642 return $html;
831             }
832              
833             }
834              
835             sub raw {
836              
837 4     4 0 14 my $self = shift;
838 4         12 my $out = shift;
839              
840 4         11 my $raw;
841              
842 4 100       22 if ($self->{_version} == 8) {
843              
844 2         11 for my $part ($self->_kf8_xhtml) {
845             my $dom = XML::LibXML->load_html(
846             string => $part,
847             no_network => !$self->{Network},
848 6         484 recover => 2,
849             );
850 6 50       5549 my ($body) = $dom->findnodes('/html/body') or next;
851 6         691 $raw .= $body->textContent;
852             }
853              
854             } else {
855              
856 2         11 my $rawml = $self->rawml(clean => 1);
857 2 50       15 my $enc = $self->{_codepage} == 1252 ? "cp1252" : "utf-8";
858             my $dom = XML::LibXML->load_html(
859             string => $rawml,
860             no_network => !$self->{Network},
861 2         38 encoding => $enc,
862             recover => 2,
863             );
864              
865 2         19772 $raw = $dom->documentElement->textContent;
866              
867             }
868              
869 4 50       157 if (defined $out) {
870 0 0       0 open my $fh, '>', $out
871             or die "Failed to open $out for writing: $!\n";
872 0         0 binmode $fh, ':utf8';
873 0         0 print { $fh } $raw;
  0         0  
874 0         0 close $fh;
875 0         0 return $out;
876             } else {
877 4         446 return $raw;
878             }
879              
880             }
881              
882             sub metadata {
883              
884 8     8 0 22 my $self = shift;
885              
886 8         63 return $self->{Metadata};
887              
888             }
889              
890             sub has_cover {
891              
892 10     10 0 26 my $self = shift;
893              
894 10         70 return defined $self->{_coverrec};
895              
896             }
897              
898             sub cover {
899              
900 6     6 0 16 my $self = shift;
901 6         14 my $out = shift;
902              
903 6 50       21 return undef unless $self->has_cover;
904              
905 6         40 my $bin = $self->{_pdb}->record($self->{_coverrec})->data;
906              
907 6 50       25 if (defined $out) {
908 0 0       0 open my $fh, '>', $out
909             or die "Failed to open $out for writing: $!\n";
910 0         0 binmode $fh;
911 0         0 print { $fh } $out;
  0         0  
912 0         0 close $fh;
913 0         0 return $out;
914             } else {
915 6         205 return $bin;
916             }
917              
918             }
919              
920             sub image_num {
921              
922 12     12 0 27 my $self = shift;
923              
924 12         24 return scalar @{ $self->{_images} };
  12         68  
925              
926             }
927              
928             sub image {
929              
930 8     8 0 25 my $self = shift;
931 8         19 my $n = shift;
932              
933 8 50       31 if ($n >= $self->image_num) {
934 0         0 return undef;
935             }
936              
937 8         51 my $img = $self->{_pdb}->record($self->{_images}->[$n])->data;
938              
939 8         42 return \$img;
940              
941             }
942              
943             1;