diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml index 2ec0448..96e9a2d 100644 --- a/.github/workflows/php-cs-fixer.yml +++ b/.github/workflows/php-cs-fixer.yml @@ -18,6 +18,6 @@ jobs: args: --config=.php-cs-fixer.dist.php --allow-risky=yes - name: Commit changes - uses: stefanzweifel/git-auto-commit-action@v4 + uses: stefanzweifel/git-auto-commit-action@v5 with: commit_message: Fix styling diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 6d1d94d..36e4dbe 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: true matrix: - php: [8.2, 8.1, 8.0, 7.4] + php: [8.3, 8.2] dependency-version: [prefer-lowest, prefer-stable] os: [ubuntu-latest] @@ -27,12 +27,9 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, mysql, mysqli, pdo_mysql, bcmath, soap, intl, gd, exif, iconv, imagick + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, mysql, mysqli, pdo_mysql, bcmath, soap, intl, gd, exif, iconv coverage: none - - name: Fix Imagick Policy - run: sudo sed -i 's/none/read|write/g' /etc/ImageMagick-6/policy.xml - - name: Install dependencies run: composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction diff --git a/.gitignore b/.gitignore index 9f09d33..af352f1 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ vendor .php_cs.cache .phpunit.result.cache *.cache +.idea +composer.phar diff --git a/CHANGELOG.md b/CHANGELOG.md index a92810c..079e3af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to `pdf-to-image` will be documented in this file +## 3.0.0 - 2024-xx-xx +- No longer use imagick for processing, use GD instead +- Colorspace and merging of layers is no longer available +- Refactor export formats to enum +- Renaming/refactoring of methods ## 2.2.0 - 2022-03-08 diff --git a/README.md b/README.md index 214eb3d..14c2d75 100644 --- a/README.md +++ b/README.md @@ -6,27 +6,17 @@ [![StyleCI](https://styleci.io/repos/38419604/shield?branch=master)](https://styleci.io/repos/38419604) [![Total Downloads](https://img.shields.io/packagist/dt/spatie/pdf-to-image.svg?style=flat-square)](https://packagist.org/packages/spatie/pdf-to-image) -This package provides an easy to work with class to convert PDF's to images. - -Spatie is a webdesign agency in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource). - -## Support us - -[](https://spatie.be/github-ad-click/pdf-to-image) - -We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). - -We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). +This package provides an easy to work with class to convert PDF's to images. It is based on the [`spatie/pdf-to-image`](https://github.com/spatie/pdf-to-image) library, which relied on Imagick for the conversion. This library relies on GD instead. ## Requirements -You should have [Imagick](http://php.net/manual/en/imagick.setresolution.php) and [Ghostscript](http://www.ghostscript.com/) installed. See [issues regarding Ghostscript](#issues-regarding-ghostscript). +You should have [Ghostscript](http://www.ghostscript.com/) installed. See [issues regarding Ghostscript](#issues-regarding-ghostscript). ## Installation The package can be installed via composer: ``` bash -composer require spatie/pdf-to-image +composer require drenso/pdf-to-image ``` ## Usage @@ -34,7 +24,7 @@ composer require spatie/pdf-to-image Converting a pdf to an image is easy. ```php -$pdf = new Spatie\PdfToImage\Pdf($pathToPdf); +$pdf = new Drenso\PdfToImage\Pdf($pathToPdf); $pdf->saveImage($pathToWhereImageShouldBeStored); ``` @@ -56,19 +46,19 @@ $pdf->setPage(2) You can override the output format: ```php -$pdf->setOutputFormat('png') +$pdf->setOutputFormat(ExportFormatEnum::PNG) ->saveImage($pathToWhereImageShouldBeStored); //the output wil be a png, no matter what ``` -You can set the quality of compression from 0 to 100: +You can set the quality of compression (this depends on the export format, see the GD documentation for more details): ```php $pdf->setCompressionQuality(100); // sets the compression quality to maximum ``` -You can specify the width of the resulting image: +You can specify the width to scale down the resulting image: ```php $pdf - ->width(400) + ->setWidth(400) ->saveImage($pathToWhereImageShouldBeStored); ``` @@ -104,21 +94,8 @@ Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recen Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. -## Security - -If you've found a bug regarding security please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker. - -## Postcardware - -You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. - -Our address is: Spatie, Kruikstraat 22, 2018 Antwerp, Belgium. - -We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards). - ## Credits -- [Freek Van der Herten](https://github.com/spatie) - [All Contributors](../../contributors) ## License diff --git a/composer.json b/composer.json index 9bc04df..8e8dea7 100644 --- a/composer.json +++ b/composer.json @@ -1,38 +1,50 @@ { - "name": "spatie/pdf-to-image", + "name": "drenso/pdf-to-image", "description": "Convert a pdf to an image", "keywords": [ - "spatie", "pdf-to-image", "pdf", "convert", "image" ], - "homepage": "https://github.com/spatie/pdf-to-image", + "homepage": "https://github.com/drenso/pdf-to-image", "license": "MIT", "authors": [ { "name": "Freek Van der Herten", "email": "freek@spatie.be", - "homepage": "https://spatie.be", - "role": "Developer" + "homepage": "https://spatie.be" + }, + { + "name": "Tobias Feijten", + "email": "tobias@drenso.nl", + "homepage": "https://drenso.nl" + }, + { + "name": "Bob van de Vijver", + "email": "bob@drenso.nl", + "homepage": "https://drenso.nl" } ], "require": { - "php" : "^7.2|^8.0", - "ext-imagick" : "*" + "php": ">=8.2", + "ext-gd": "*", + "ext-random": "*", + "symfony/filesystem": "^5.4 || ^6.4 || ^7.0", + "symfony/finder": "^5.4 || ^6.4 || ^7.0", + "symfony/process": "^5.4 || ^6.4 || ^7.0" }, "require-dev": { "pestphp/pest": "^1.21" }, "autoload": { "psr-4": { - "Spatie\\PdfToImage\\": "src" + "Drenso\\PdfToImage\\": "src" } }, "autoload-dev": { "psr-4": { - "Spatie\\PdfToImage\\Test\\": "tests" + "Drenso\\PdfToImage\\Test\\": "tests" } }, "scripts": { diff --git a/src/Enum/ExportFormatEnum.php b/src/Enum/ExportFormatEnum.php new file mode 100644 index 0000000..89ee18b --- /dev/null +++ b/src/Enum/ExportFormatEnum.php @@ -0,0 +1,38 @@ + '.jpg', + self::PNG => '.png', + self::JPEG => '.jpeg', + }; + } + + public static function fromFileName(string $fileName): ExportFormatEnum + { + return match (pathinfo($fileName, PATHINFO_EXTENSION)) { + 'png' => ExportFormatEnum::PNG, + 'jpeg' => ExportFormatEnum::JPEG, + default => ExportFormatEnum::JPG, + }; + } + + public function export(GdImage $image, string $fileName, int $quality = -1): bool + { + return match ($this) { + self::JPG, self::JPEG => imagejpeg($image, $fileName, $quality), + self::PNG => imagepng($image, $fileName, $quality), + }; + } +} diff --git a/src/Exceptions/InvalidFormat.php b/src/Exceptions/InvalidFormat.php deleted file mode 100644 index be68c71..0000000 --- a/src/Exceptions/InvalidFormat.php +++ /dev/null @@ -1,9 +0,0 @@ -filesystem = new Filesystem(); + if (! $this->filesystem->exists($pdfFile)) { throw new PdfDoesNotExist("File `{$pdfFile}` does not exist"); } - - $this->pdfFile = $pdfFile; - - $this->imagick = new Imagick(); - - $this->imagick->readImage($this->pdfFile); } - public function setResolution(int $resolution) + public function setWidth(int $width): self { - $this->resolution = $resolution; + $this->width = $width; return $this; } - public function setOutputFormat(string $outputFormat) + public function setOutputFormat(ExportFormatEnum $outputFormat): self { - if (! $this->isValidOutputFormat($outputFormat)) { - throw new InvalidFormat("Format {$outputFormat} is not supported"); - } - $this->outputFormat = $outputFormat; return $this; } - public function getOutputFormat(): string + public function getOutputFormat(): ExportFormatEnum { return $this->outputFormat; } - /** - * Sets the layer method for Imagick::mergeImageLayers() - * If int, should correspond to a predefined LAYERMETHOD constant. - * If null, Imagick::mergeImageLayers() will not be called. - * - * @param int|null - * - * @return $this - * - * @throws \Spatie\PdfToImage\Exceptions\InvalidLayerMethod - * - * @see https://secure.php.net/manual/en/imagick.constants.php - * @see Pdf::getImageData() - */ - public function setLayerMethod(?int $layerMethod) - { - $this->layerMethod = $layerMethod; - - return $this; - } - - public function isValidOutputFormat(string $outputFormat): bool - { - return in_array($outputFormat, $this->validOutputFormats); - } - - public function setPage(int $page) + public function setPage(int $page): self { if ($page > $this->getNumberOfPages() || $page < 1) { throw new PageDoesNotExist("Page {$page} does not exist"); } - $this->page = $page; return $this; @@ -107,7 +61,8 @@ public function setPage(int $page) public function getNumberOfPages(): int { if ($this->numberOfPages === null) { - $this->numberOfPages = $this->imagick->getNumberImages(); + $this->prepareImages(); + $this->numberOfPages = (new Finder())->in($this->cacheDir)->name('*.png')->count(); } return $this->numberOfPages; @@ -116,12 +71,11 @@ public function getNumberOfPages(): int public function saveImage(string $pathToImage): bool { if (is_dir($pathToImage)) { - $pathToImage = rtrim($pathToImage, '\/').DIRECTORY_SEPARATOR.$this->page.'.'.$this->outputFormat; + $pathToImage = rtrim($pathToImage, '\/').DIRECTORY_SEPARATOR.$this->page.$this->outputFormat->getExtension(); } - $imageData = $this->getImageData($pathToImage); - return file_put_contents($pathToImage, $imageData) !== false; + return $this->outputFormat->export($imageData, $pathToImage, $this->compressionQuality); } public function saveAllPagesAsImages(string $directory, string $prefix = ''): array @@ -134,102 +88,65 @@ public function saveAllPagesAsImages(string $directory, string $prefix = ''): ar return array_map(function ($pageNumber) use ($directory, $prefix) { $this->setPage($pageNumber); - - $destination = "{$directory}/{$prefix}{$pageNumber}.{$this->outputFormat}"; - + $destination = "{$directory}".DIRECTORY_SEPARATOR."{$prefix}{$pageNumber}.{$this->outputFormat}"; $this->saveImage($destination); return $destination; }, range(1, $numberOfPages)); } - public function getImageData(string $pathToImage): Imagick + public function getImageData(string $pathToImage): GdImage { - /* - * Reinitialize imagick because the target resolution must be set - * before reading the actual image. - */ - $this->imagick = new Imagick(); - - $this->imagick->setResolution($this->resolution, $this->resolution); + $this->prepareImages(); + $this->outputFormat ??= ExportFormatEnum::fromFileName($pathToImage); + $pageName = $this->cacheDir . DIRECTORY_SEPARATOR . sprintf('%03d.png', $this->page); + $originalImage = imagecreatefrompng($pageName); - if ($this->colorspace !== null) { - $this->imagick->setColorspace($this->colorspace); + if ($this->width === null) { + return $originalImage; } - if ($this->compressionQuality !== null) { - $this->imagick->setCompressionQuality($this->compressionQuality); + $imageSize = getimagesize($pageName); + // Never grow the image + if ($imageSize[0] < $this->width) { + return $originalImage; } + // Calculate scaled height + $newHeight = round((float)$imageSize[1] * ($this->width / $imageSize[0])); + // Resize in new image + $resizedImage = imagecreatetruecolor($this->width, $newHeight); + imagecopyresampled($resizedImage, $originalImage, 0, 0, 0, 0, $this->width, $newHeight, $imageSize[0], $imageSize[1]); - if (filter_var($this->pdfFile, FILTER_VALIDATE_URL)) { - return $this->getRemoteImageData($pathToImage); - } - - $this->imagick->readImage(sprintf('%s[%s]', $this->pdfFile, $this->page - 1)); - - if (is_int($this->layerMethod)) { - $this->imagick = $this->imagick->mergeImageLayers($this->layerMethod); - } - - if ($this->thumbnailWidth !== null) { - $this->imagick->thumbnailImage($this->thumbnailWidth, 0); - } - - $this->imagick->setFormat($this->determineOutputFormat($pathToImage)); - - return $this->imagick; + return $resizedImage; } - public function setColorspace(int $colorspace) - { - $this->colorspace = $colorspace; - - return $this; - } - - public function setCompressionQuality(int $compressionQuality) + public function setCompressionQuality(int $compressionQuality): self { $this->compressionQuality = $compressionQuality; return $this; } - public function width(int $thumbnailWidth) - { - $this->thumbnailWidth = $thumbnailWidth; - - return $this; - } - - protected function getRemoteImageData(string $pathToImage): Imagick + public function prepareImages(): void { - $this->imagick->readImage($this->pdfFile); - - $this->imagick->setIteratorIndex($this->page - 1); - - if (is_int($this->layerMethod)) { - $this->imagick = $this->imagick->mergeImageLayers($this->layerMethod); - } - - $this->imagick->setFormat($this->determineOutputFormat($pathToImage)); - - return $this->imagick; - } - - protected function determineOutputFormat(string $pathToImage): string - { - $outputFormat = pathinfo($pathToImage, PATHINFO_EXTENSION); - - if ($this->outputFormat != '') { - $outputFormat = $this->outputFormat; - } - - $outputFormat = strtolower($outputFormat); - - if (! $this->isValidOutputFormat($outputFormat)) { - $outputFormat = 'jpg'; + if ($this->cacheDir) { + return; } - return $outputFormat; + // Convert to PNG files, so GD can be used for the following processing + $this->cacheDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'pdftoimage' . DIRECTORY_SEPARATOR . bin2hex((new Secure())->generate()); + $this->filesystem->mkdir($this->cacheDir); + (new Process([ + 'gs', + '-dSAFER', + '-dBATCH', + '-dNOPAUSE', + '-sDEVICE=png16m', + '-dTextAlphaBits=4', + '-dGraphicsAlphaBits=4', + '-r' . $this->resolution, + '-sOutputFile=' . $this->cacheDir . DIRECTORY_SEPARATOR . '%03d.png', + $this->pdfFile]) + )->mustRun(); } } diff --git a/tests/PdfTest.php b/tests/PdfTest.php index 6d872ff..a44bae7 100644 --- a/tests/PdfTest.php +++ b/tests/PdfTest.php @@ -1,9 +1,8 @@ testFile = __DIR__.'/files/test.pdf'; @@ -13,14 +12,10 @@ it('will throw an exception when try to convert a non existing file', function () { - new Pdf('pdfdoesnotexists.pdf'); + new Pdf('pdfdoesnotexist.pdf'); })->throws(PdfDoesNotExist::class); -it('will throw an exception when trying to convert an invalid file type', function () { - (new Pdf($this->testFile))->setOutputFormat('bla'); -})->throws(InvalidFormat::class); - -it('will throw an exception when passed an invalid page number', function ($invalidPage) { +it('will throw an exception when passed an invalid page number', function () { (new Pdf($this->testFile))->setPage(100); }) ->throws(PageDoesNotExist::class) @@ -33,56 +28,27 @@ }); it('will accept a custom specified resolution', function () { - $image = (new Pdf($this->testFile)) - ->setResolution(150) - ->getImageData('test.jpg') - ->getImageResolution(); + $image = (new Pdf($this->testFile, resolution: 150)) + ->getImageData('test.jpg'); - expect($image['x'])->toEqual(150); - expect($image['y'])->toEqual(150); + $resolution = imageresolution($image); + expect($resolution[0])->toEqual(150) + ->and($resolution[1])->toEqual(150); }); it('will convert a specified page', function () { - $imagick = (new Pdf($this->multipageTestFile)) + $image = (new Pdf($this->multipageTestFile)) ->setPage(2) ->getImageData('page-2.jpg'); - expect($imagick)->toBeInstanceOf(Imagick::class); -}); - -it('will accpect a specified file type and convert to it', function () { - $imagick = (new Pdf($this->testFile)) - ->setOutputFormat('png') - ->getImageData('test.png'); - - expect($imagick->getFormat())->toEqual('png'); - expect($imagick->getFormat())->not->toEqual('jpg'); -}); - -it('can accepct a layer', function () { - $image = (new Pdf($this->testFile)) - ->setLayerMethod(Imagick::LAYERMETHOD_FLATTEN) - ->setResolution(72) - ->getImageData('test.jpg') - ->getImageResolution(); - - expect($image['x'])->toEqual(72); - expect($image['y'])->toEqual(72); -}); - -it('will set compression quality', function () { - $imagick = (new Pdf($this->testFile)) - ->setCompressionQuality(99) - ->getImageData('test.jpg'); - - expect($imagick->getCompressionQuality())->toEqual(99); + expect($image)->toBeInstanceOf(GdImage::class); }); it('will create a thumbnail at specified width', function () { - $imagick = (new Pdf($this->multipageTestFile)) - ->width(400) - ->getImageData('test.jpg') - ->getImageGeometry(); + $image = (new Pdf($this->multipageTestFile)) + ->setWidth(400) + ->getImageData('test.jpg'); - expect($imagick['width'])->toBe(400); + expect(imagesx($image))->toBe(400) + ->and(imagesy($image))->toBe(283); // round(210*400/297) });