diff --git a/bin/wkhtmltoimage b/bin/wkhtmltoimage new file mode 100755 index 00000000..8acfa45a Binary files /dev/null and b/bin/wkhtmltoimage differ diff --git a/composer.json b/composer.json index 88c74604..a1e8061b 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,8 @@ "firebase/php-jwt": "^4.0", "monolog/monolog": "^1.21", "theorchard/monolog-cascade": "^0.4", - "endroid/qrcode": "^1.7" + "endroid/qrcode": "^1.7", + "mikehaertl/phpwkhtmltopdf": "^2.2" }, "require-dev": { "phpunit/phpunit": "^5.0", diff --git a/config/autoload/phpwkhtmltopdf.global.php b/config/autoload/phpwkhtmltopdf.global.php new file mode 100644 index 00000000..6029b973 --- /dev/null +++ b/config/autoload/phpwkhtmltopdf.global.php @@ -0,0 +1,11 @@ + [ + 'images' => [ + 'binary' => 'bin/wkhtmltoimage', + 'files_location' => 'data/cache/previews', + ], + ], + +]; diff --git a/module/Common/config/dependencies.config.php b/module/Common/config/dependencies.config.php index 4c5c6ace..6188d4ad 100644 --- a/module/Common/config/dependencies.config.php +++ b/module/Common/config/dependencies.config.php @@ -2,14 +2,16 @@ use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory; use Doctrine\Common\Cache\Cache; use Doctrine\ORM\EntityManager; +use mikehaertl\wkhtmlto\Image; use Monolog\Logger; use Psr\Log\LoggerInterface; use Shlinkio\Shlink\Common\Factory\CacheFactory; use Shlinkio\Shlink\Common\Factory\EntityManagerFactory; use Shlinkio\Shlink\Common\Factory\LoggerFactory; use Shlinkio\Shlink\Common\Factory\TranslatorFactory; +use Shlinkio\Shlink\Common\Image\ImageFactory; use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware; -use Shlinkio\Shlink\Common\Service\IpLocationResolver; +use Shlinkio\Shlink\Common\Service; use Shlinkio\Shlink\Common\Twig\Extension\TranslatorExtension; use Zend\I18n\Translator\Translator; use Zend\ServiceManager\Factory\InvokableFactory; @@ -22,12 +24,14 @@ return [ GuzzleHttp\Client::class => InvokableFactory::class, Cache::class => CacheFactory::class, 'Logger_Shlink' => LoggerFactory::class, + Image::class => ImageFactory::class, Translator::class => TranslatorFactory::class, TranslatorExtension::class => AnnotatedFactory::class, LocaleMiddleware::class => AnnotatedFactory::class, - IpLocationResolver::class => AnnotatedFactory::class, + Service\IpLocationResolver::class => AnnotatedFactory::class, + Service\PreviewGenerator::class => AnnotatedFactory::class, ], 'aliases' => [ 'em' => EntityManager::class, diff --git a/module/Common/src/Image/ImageFactory.php b/module/Common/src/Image/ImageFactory.php new file mode 100644 index 00000000..af2ef768 --- /dev/null +++ b/module/Common/src/Image/ImageFactory.php @@ -0,0 +1,30 @@ +get('config')['phpwkhtmltopdf']; + return new Image(isset($config['images']) ? $config['images'] : null); + } +} diff --git a/module/Common/src/Service/PreviewGenerator.php b/module/Common/src/Service/PreviewGenerator.php new file mode 100644 index 00000000..0a902967 --- /dev/null +++ b/module/Common/src/Service/PreviewGenerator.php @@ -0,0 +1,58 @@ +image = $image; + $this->cache = $cache; + $this->location = $location; + } + + /** + * Generates and stores preview for provided website and returns the path to the image file + * + * @param string $url + * @return string + */ + public function generatePreview($url) + { + $cacheId = sprintf('preview_%s.png', urlencode($url)); + if ($this->cache->contains($cacheId)) { + return $this->cache->fetch($cacheId); + } + + $path = $this->location . '/' . $cacheId; + $this->image->setPage($url); + $this->image->saveAs($path); + $this->cache->save($cacheId, $path); + + return $path; + } +} diff --git a/module/Common/src/Service/PreviewGeneratorInterface.php b/module/Common/src/Service/PreviewGeneratorInterface.php new file mode 100644 index 00000000..3029eb97 --- /dev/null +++ b/module/Common/src/Service/PreviewGeneratorInterface.php @@ -0,0 +1,13 @@ +image = $this->prophesize(Image::class); + $this->cache = new ArrayCache(); + $this->generator = new PreviewGenerator($this->image->reveal(), $this->cache, 'dir'); + } + + /** + * @test + */ + public function alreadyCachedElementsAreNotProcessed() + { + $url = 'http://foo.com'; + $this->cache->save(sprintf('preview_%s.png', urlencode($url)), 'dir/foo.png'); + $this->image->saveAs(Argument::cetera())->shouldBeCalledTimes(0); + $this->assertEquals('dir/foo.png', $this->generator->generatePreview($url)); + } + + /** + * @test + */ + public function nonCachedElementsAreProcessedAndThenCached() + { + $url = 'http://foo.com'; + $cacheId = sprintf('preview_%s.png', urlencode($url)); + $expectedPath = 'dir/' . $cacheId; + + $this->image->setPage($url)->shouldBeCalledTimes(1); + $this->image->saveAs($expectedPath)->shouldBeCalledTimes(1); + + $this->assertFalse($this->cache->contains($cacheId)); + $this->assertEquals($expectedPath, $this->generator->generatePreview($url)); + $this->assertTrue($this->cache->contains($cacheId)); + } +}