diff --git a/README.md b/README.md index 1a617ee..725b173 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ There is a lot of different formats for eBooks and comics, if you want to know m | :--------------: | :-------------------------------------------------------------: | :-------: | :-----------: | | EPUB (IDPF) | `.epub` | ✅ | | | Kindle (Amazon) | `.azw`, `.azw3`, `.kf8`, `.kfx` | ❌ | _proprietary_ | -| Mobipocket (KF8) | `.mobi`, `.prc` | ❌ | _deprecated_ | +| Mobipocket | `.mobi`, `.prc` | ❌ | _deprecated_ | | PDF | `.pdf` | ✅ | | | iBook (Apple) | `.ibooks` | ❌ | _proprietary_ | | DjVu | `.djvu`, `.djv` | ❌ | | diff --git a/php-mobi b/php-mobi new file mode 160000 index 0000000..e725dd2 --- /dev/null +++ b/php-mobi @@ -0,0 +1 @@ +Subproject commit e725dd27e2d15830a54da27bab08ccef816890aa diff --git a/src/Formats/Mobi/Parser/MobiParser.php b/src/Formats/Mobi/Parser/MobiParser.php index bee7126..417a3d1 100644 --- a/src/Formats/Mobi/Parser/MobiParser.php +++ b/src/Formats/Mobi/Parser/MobiParser.php @@ -10,12 +10,13 @@ class MobiParser { /** + * @param string[] $errors * @param PalmRecord[] $palmRecords * @param ExthRecord[] $exthRecords */ protected function __construct( protected Stream $stream, - protected ?string $error = null, + protected ?array $errors = [], protected array $palmRecords = [], protected array $exthRecords = [], protected ?PalmDOCHeader $palmDOCHeader = null, @@ -32,6 +33,10 @@ public static function make(string $path): ?self $self->parse(); $self->images = MobiImages::make($path); + if (empty($self->errors)) { + $self->errors = null; + } + return $self; } @@ -56,10 +61,7 @@ private function parse(): self $content = $this->stream->read(8); if ($content !== 'BOOKMOBI') { - $this->error = 'Invalid file format'; - $this->stream->close(); - - return $this; + $this->errors[] = "File format invalid: {$content} (expected BOOKMOBI)"; } $this->stream->seek(0); @@ -92,9 +94,7 @@ private function parse(): self $mobiStart = $this->stream->tell(); $header = $this->stream->read(4); if ($header !== 'MOBI') { - $this->error = 'No MOBI header'; - - return $this; + $this->errors[] = "Header invalid: {$header} (expected MOBI)"; } $this->mobiHeader = new MobiHeader( @@ -108,9 +108,7 @@ private function parse(): self $this->stream->seek($mobiStart + $this->mobiHeader->length); $exthHeader = $this->stream->read(4); if ($exthHeader !== 'EXTH') { - $this->error = 'No EXTH header'; - - return $this; + $this->errors[] = "EXTH header invalid: {$exthHeader} (expected EXTH)"; } $this->exthHeader = new ExthHeader( @@ -129,7 +127,9 @@ private function parse(): self } $this->exthRecords = $this->exthHeader->records; - $this->isValid = true; + if (empty($this->errors)) { + $this->isValid = true; + } $this->stream->close(); @@ -172,9 +172,12 @@ public function getExthRecords(): array return $this->exthRecords; } - public function getError(): ?string + /** + * @return string[]|null + */ + public function getErrors(): ?array { - return $this->error; + return $this->errors; } public function getImages(): ?MobiImages diff --git a/src/Formats/Mobi/Parser/MobiReader.php b/src/Formats/Mobi/Parser/MobiReader.php index 041537b..d616406 100644 --- a/src/Formats/Mobi/Parser/MobiReader.php +++ b/src/Formats/Mobi/Parser/MobiReader.php @@ -2,6 +2,9 @@ namespace Kiwilan\Ebook\Formats\Mobi\Parser; +/** + * @docs https://wiki.mobileread.com/wiki/Mobi + */ class MobiReader { const DRM_SERVER_ID = 1; diff --git a/src/Utils/Stream.php b/src/Utils/Stream.php index 4aea687..50fab32 100644 --- a/src/Utils/Stream.php +++ b/src/Utils/Stream.php @@ -10,6 +10,11 @@ protected function __construct( ) { } + /** + * Creates a new instance of Stream. + * + * @throws \Exception + */ public static function make(string $path): self { $resource = fopen($path, 'rb'); @@ -21,42 +26,65 @@ public static function make(string $path): self return new self($path, $resource); } + /** + * Returns the file size in bytes. + */ public function filesize(): int { return filesize($this->path); } + /** + * Sets the file position indicator for the file pointer. + */ public function seek(int $offset): int { return fseek($this->resource, $offset, SEEK_SET); } + /** + * Reads a line from the file pointer. + */ public function read(int $bytes): string|false { return fread($this->resource, $bytes); } + /** + * Reads a 32-bit unsigned integer from the current position of the file read pointer. + */ public function binaryToDecimal($bytes): int|float { return hexdec(bin2hex($bytes)); } + /** + * Returns the current position of the file read/write pointer. + */ public function tell(): int { return ftell($this->resource); } + /** + * Closes an open file pointer. + */ public function close(): bool { return fclose($this->resource); } + /** + * Get file path. + */ public function getPath(): string { return $this->path; } /** + * Get file resource. + * * @return resource */ public function getResource(): mixed diff --git a/tests/FormatTest.php b/tests/FormatTest.php index 3644cce..ab98cf9 100644 --- a/tests/FormatTest.php +++ b/tests/FormatTest.php @@ -48,7 +48,7 @@ // FORMAT_PDB, // FORMAT_PDF, // FORMAT_PMLZ, - // FORMAT_PRC, + FORMAT_PRC, // FORMAT_RB, // FORMAT_RTF, // FORMAT_SNB, @@ -76,20 +76,3 @@ expect($ebook->getCover())->toBeInstanceOf(EbookCover::class); expect('tests/output/mobi/cover.jpg')->toBeReadableFile(); }); - -it('can parse specific format', function () { - recurseRmdir('tests/output/mobi'); - $parser = MobiParser::make(FORMAT_PDB); - ray($parser); - - // ray($parser->getImages()); - // foreach ($parser->getImages()->getItems() as $key => $value) { - // $path = "tests/output/mobi/{$key}.jpg"; - // file_put_contents($path, $value); - // expect($path)->toBeReadableFile(); - // } - - // $ebook = Ebook::read(FORMAT_MOBI); - // ray($ebook); - // file_put_contents('tests/output/mobi/cover.jpg', $ebook->getCover()?->getContent()); -}); diff --git a/tests/MobiTest.php b/tests/MobiTest.php index 82f7868..abe5610 100644 --- a/tests/MobiTest.php +++ b/tests/MobiTest.php @@ -33,7 +33,7 @@ expect($parser->getExthHeader())->toBeInstanceOf(ExthHeader::class); expect($parser->getExthRecords())->toBeArray(); expect($parser->getPalmRecords())->toBeArray(); - expect($parser->getError())->toBeNull(); + expect($parser->getErrors())->toBeNull(); expect($parser->getPalmDOCHeader()->compression)->toBe(2); expect($parser->getPalmDOCHeader()->textLength)->toBe(231532);