File Coverage

blib/lib/EBook/Ishmael/EBook/Mobi.pm
Criterion Covered Total %
statement 392 552 71.0
branch 70 152 46.0
condition 16 41 39.0
subroutine 38 48 79.1
pod 0 10 0.0
total 516 803 64.2


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