├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── examples └── github-shots.php └── src └── Codegyre └── PhantomShot ├── ComposerHook.php ├── ThumbnailCollector.php ├── WebPage.php └── js ├── package.json ├── phantomshot-service.js └── phantomshot.js /.gitignore: -------------------------------------------------------------------------------- 1 | src/Codegyre/PhantomShot/js/node_modules/ 2 | 3 | .idea 4 | *.swp 5 | composer.phar -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Codegyre developers team, http://codegyre.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PhantomShot 2 | 3 | PHP tool to make web-shots using PhantomJS 4 | 5 | ## Installation 6 | 7 | ### add composer requirements 8 | 9 | "require": { 10 | "codegyre/phantomshot": "dev-master" 11 | } 12 | 13 | 14 | ### add post-install and post-update hooks to your composer.json 15 | 16 | "scripts": { 17 | "post-install-cmd": [ 18 | "Codegyre\\PhantomShot\\ComposerHook::hook" 19 | ], 20 | "post-update-cmd": [ 21 | "Codegyre\\PhantomShot\\ComposerHook::hook" 22 | ] 23 | } 24 | 25 | We need this to install required dependencies through npm. 26 | 27 | ## Making webshot 28 | 29 | $webPage = new \Codegyre\PhantomShot\WebPage('http://github.com'); 30 | $webPage->getShot('/tmp/github.png'); 31 | 32 | ## Thumbnailing 33 | 34 | $tc = new \Codegyre\PhantomShot\ThumbnailCollector('http://github.com'); 35 | $tc->createThumbnail('/destination/dir/1.png', 200, 400, ThumbnailCollector::TRANSFORM_FILL_HEIGHT_MIDDLE); 36 | $tc->createThumbnail('/destination/dir/1.png', 350, 160, ThumbnailCollector::TRANSFORM_FILL_WIDTH_MIDDLE); 37 | 38 | Make sure you have npm installed on your system 39 | 40 | ## Using as service 41 | 42 | You have a possibility to use phantomshot as a web service to get website screenshots 43 | 44 | cd src/js && ./phantomshot-service.js --port=3000 45 | 46 | It will start http server on localhost:3000. Usage: 47 | 48 | http://localhost:3000/?width=1024&height=768&url=http://example.com 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codegyre/phantomshot", 3 | "type": "library", 4 | "description": "PHP tool to make web-shots using PhantomJS", 5 | "keywords": ["webshot","pageshot","screenshot"], 6 | "homepage": "http://codegyre.com", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Nick Palamarchuk", 11 | "email": "lividgreen@codegyre.com" 12 | }, 13 | { 14 | "name": "Codegyre developers team", 15 | "homepage": "http://codegyre.com" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=5.3.3" 20 | }, 21 | "autoload": { 22 | "psr-0": { "Codegyre\\PhantomShot\\": "src/" } 23 | } 24 | } -------------------------------------------------------------------------------- /examples/github-shots.php: -------------------------------------------------------------------------------- 1 | createThumbnail($destDir.'/wt.png', 200, 400, ThumbnailCollector::TRANSFORM_FILL_WIDTH_TOP); 17 | $tc->createThumbnail($destDir.'/wm.png', 200, 400, ThumbnailCollector::TRANSFORM_FILL_WIDTH_MIDDLE); 18 | $tc->createThumbnail($destDir.'/wb.png', 200, 400, ThumbnailCollector::TRANSFORM_FILL_WIDTH_BOTTOM); 19 | 20 | $tc->createThumbnail($destDir.'/hl.png', 200, 400, ThumbnailCollector::TRANSFORM_FILL_HEIGHT_LEFT); 21 | $tc->createThumbnail($destDir.'/hm.png', 200, 400, ThumbnailCollector::TRANSFORM_FILL_HEIGHT_MIDDLE); 22 | $tc->createThumbnail($destDir.'/hr.png', 200, 400, ThumbnailCollector::TRANSFORM_FILL_HEIGHT_RIGHT); 23 | 24 | echo "Done!\n"; 25 | } 26 | catch (Exception $e) { 27 | echo "Error happened: {$e->getMessage()}\n"; 28 | } 29 | -------------------------------------------------------------------------------- /src/Codegyre/PhantomShot/ComposerHook.php: -------------------------------------------------------------------------------- 1 | getName()) { 12 | case ScriptEvents::POST_INSTALL_CMD: 13 | $command = 'install'; 14 | break; 15 | case ScriptEvents::POST_UPDATE_CMD: 16 | $command = 'update'; 17 | break; 18 | default: 19 | return; 20 | } 21 | 22 | $executableFinder = new ExecutableFinder; 23 | $npmPath = $executableFinder->find('npm'); 24 | if ($npmPath === null) { 25 | throw new \RuntimeException('Unable to locate npm executable.'); 26 | } 27 | 28 | exec('cd '.escapeshellarg(__DIR__.'/js').' && "' . $npmPath . '" '.escapeshellarg($command), $out, $code); 29 | 30 | $io = $event->getIO(); 31 | if ($code != 0) { 32 | throw new \RuntimeException('Error executing npm: '.$out); 33 | } 34 | $io->write($out); 35 | } 36 | } -------------------------------------------------------------------------------- /src/Codegyre/PhantomShot/ThumbnailCollector.php: -------------------------------------------------------------------------------- 1 | getShot($tmpFile, $viewportSize, $zoomFactor, $timeout, $delay); 40 | 41 | $this->image = imagecreatefrompng($tmpFile); 42 | unlink($tmpFile); 43 | 44 | $this->sourceWidth = imagesx($this->image); 45 | $this->sourceHeight = imagesy($this->image); 46 | } 47 | 48 | /** 49 | * @param string $fileName 50 | * @param int $width 51 | * @param int $height 52 | * @param string $transform 53 | * @throws \Exception 54 | */ 55 | public function createThumbnail($fileName, $width, $height, $transform = self::TRANSFORM_FILL_WIDTH_TOP) { 56 | $dstImage = imagecreatetruecolor($width, $height); 57 | imagealphablending($dstImage, false); 58 | $transparency = imagecolorallocatealpha($dstImage, 0, 0, 0, 127); 59 | imagefill($dstImage, 0, 0, $transparency); 60 | imagesavealpha($dstImage, true); 61 | 62 | $ratioX = (float)$width / $this->sourceWidth; 63 | $ratioXHeight = $this->sourceHeight * $ratioX; 64 | if ($ratioXHeight > $height) { 65 | $calculatedSourceHeight = $this->sourceHeight - ($ratioXHeight - $height) / $ratioX; 66 | $calculatedDestHeight = $height; 67 | 68 | $destYTop = 0; 69 | $destYBottom = 0; 70 | 71 | $srcYTop = 0; 72 | $srcYBottom = ($this->sourceHeight - $height / $ratioX); 73 | } 74 | else { 75 | $calculatedSourceHeight = $this->sourceHeight; 76 | $calculatedDestHeight = $ratioXHeight; 77 | 78 | $destYTop = 0; 79 | $destYBottom = ($height - $ratioXHeight); 80 | 81 | $srcYTop = 0; 82 | $srcYBottom = 0; 83 | } 84 | $srcYMiddle = $srcYBottom / 2; 85 | $destYMiddle = $destYBottom / 2; 86 | 87 | $ratioY = (float)$height / $this->sourceHeight; 88 | $ratioYWidth = $this->sourceWidth * $ratioY; 89 | if ($ratioYWidth > $width) { 90 | $calculatedSourceWidth = $this->sourceWidth - ($ratioYWidth - $width) / $ratioY; 91 | $calculatedDestWidth = $width; 92 | 93 | $destXLeft = 0; 94 | $destXRight = 0; 95 | 96 | $srcXLeft = 0; 97 | $srcXRight = ($this->sourceWidth - $width / $ratioY); 98 | } 99 | else { 100 | $calculatedSourceWidth = $this->sourceWidth; 101 | $calculatedDestWidth = $ratioYWidth; 102 | 103 | $destXLeft = 0; 104 | $destXRight = ($width - $ratioYWidth); 105 | 106 | $srcXLeft = 0; 107 | $srcXRight = 0; 108 | } 109 | $srcXMiddle = $srcXRight / 2; 110 | $destXMiddle = $destXRight / 2; 111 | 112 | switch ($transform) { 113 | case self::TRANSFORM_FILL_WIDTH_TOP: 114 | imagecopyresampled($dstImage, $this->image, 0, $destYTop, 0, $srcYTop, $width, $calculatedDestHeight, $this->sourceWidth, $calculatedSourceHeight); 115 | break; 116 | case self::TRANSFORM_FILL_WIDTH_MIDDLE: 117 | imagecopyresampled($dstImage, $this->image, 0, $destYMiddle, 0, $srcYMiddle, $width, $calculatedDestHeight, $this->sourceWidth, $calculatedSourceHeight); 118 | break; 119 | case self::TRANSFORM_FILL_WIDTH_BOTTOM: 120 | imagecopyresampled($dstImage, $this->image, 0, $destYBottom, 0, $srcYBottom, $width, $calculatedDestHeight, $this->sourceWidth, $calculatedSourceHeight); 121 | break; 122 | case self::TRANSFORM_FILL_HEIGHT_LEFT: 123 | imagecopyresampled($dstImage, $this->image, $destXLeft, 0, $srcXLeft, 0, $calculatedDestWidth, $height, $calculatedSourceWidth, $this->sourceHeight); 124 | break; 125 | case self::TRANSFORM_FILL_HEIGHT_MIDDLE: 126 | imagecopyresampled($dstImage, $this->image, $destXMiddle, 0, $srcXMiddle, 0, $calculatedDestWidth, $height, $calculatedSourceWidth, $this->sourceHeight); 127 | break; 128 | case self::TRANSFORM_FILL_HEIGHT_RIGHT: 129 | imagecopyresampled($dstImage, $this->image, $destXRight, 0, $srcXRight, 0, $calculatedDestWidth, $height, $calculatedSourceWidth, $this->sourceHeight); 130 | break; 131 | default: 132 | throw new \Exception('Unknown transform'); 133 | } 134 | 135 | imagepng($dstImage, $fileName); 136 | } 137 | } -------------------------------------------------------------------------------- /src/Codegyre/PhantomShot/WebPage.php: -------------------------------------------------------------------------------- 1 | url = $url; 29 | } 30 | 31 | /** 32 | * Make screenshot of web-page 33 | * 34 | * @param string $destinationFile 35 | * @param string $size 36 | * @param int $zoomFactor 37 | * @param int $timeout 38 | * @param int $delay 39 | * @throws \Exception 40 | */ 41 | public function getShot($destinationFile, $size = self::SIZE_DESKTOP_19, $zoomFactor = 1, $timeout = 0, $delay = 0) { 42 | list($width, $height) = explode('*', $size); 43 | 44 | $cmdParts = array( 45 | './phantomshot.js', 46 | '--output='.escapeshellarg($destinationFile), 47 | '--width='.escapeshellarg($width), 48 | '--height='.escapeshellarg($height), 49 | '--zoom='.escapeshellarg($zoomFactor), 50 | '--timeout='.escapeshellarg($timeout), 51 | '--delay='.escapeshellarg($delay), 52 | escapeshellarg($this->url), 53 | ); 54 | $cmd = implode(' ', $cmdParts); 55 | 56 | $descriptorSpec = array( 57 | 0 => array("pipe", "r"), 58 | 1 => array("pipe", "w"), 59 | 2 => array("pipe", "w"), 60 | ); 61 | $process = proc_open($cmd, $descriptorSpec, $pipes, __DIR__.'/js'); 62 | 63 | if (is_resource($process)) { 64 | $error = stream_get_contents($pipes[2]); 65 | proc_close($process); 66 | if (!empty($error)) { 67 | throw new \Exception("Webshot creation failed: $error"); 68 | } 69 | } 70 | else { 71 | throw new \Exception("Can't run phantomshot.js"); 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /src/Codegyre/PhantomShot/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phantomshot", 3 | "description": "phantomshot cli + API", 4 | "author": "Nick Palamarchuk ", 5 | "contributors": [ 6 | "Juho Vepsalainen ", 7 | "Codegyre developers team (http://codegyre.com)" 8 | ], 9 | "version": "0.1.0", 10 | "main": "./src/js/phantomshot.js", 11 | "repository": { 12 | "type": "git", 13 | "url": "git@github.com:CodeGyre/phantomshot.git" 14 | }, 15 | "homepage": "https://github.com/Codegyre/phantomshot", 16 | "bugs": { 17 | "url": "https://github.com/Codegyre/phantomshot/issues" 18 | }, 19 | "keywords": [ 20 | "screenshot", 21 | "screengrab", 22 | "phantom" 23 | ], 24 | "dependencies": { 25 | "webshot": "0.8.0", 26 | "optimist": "0.6.0", 27 | "express": "3.4.6" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Codegyre/PhantomShot/js/phantomshot-service.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | (function() { 3 | var argv = require('optimist') 4 | .usage('Usage: [options] $0') 5 | .option('p', { 6 | alias: 'port', 7 | default: 3000 8 | }) 9 | .boolean('dev') 10 | .argv; 11 | 12 | var http = require('http'); 13 | var express = require('express'); 14 | var webshot = require('webshot'); 15 | 16 | var app = express(); 17 | 18 | // all environments 19 | app.set('port', argv.p); 20 | app.use(express.logger('dev')); 21 | app.use(app.router); 22 | 23 | // development only 24 | if (argv.dev) { 25 | app.use(express.errorHandler()); 26 | } 27 | 28 | app.get('/', function(req, res) { 29 | var url = req.query.url; 30 | 31 | if (!url) { 32 | res.send(403, 'Missing url parameter!'); 33 | return; 34 | } 35 | 36 | var width = parseInt(req.query['width']); 37 | if (!width) { 38 | width = 1024; 39 | } 40 | 41 | var height = parseInt(req.query['height']); 42 | if (!height) { 43 | height = 768; 44 | } 45 | 46 | var zoomFactor = parseFloat(req.query['zoom']); 47 | if (!zoomFactor) { 48 | zoomFactor = 1.0; 49 | } 50 | 51 | var options = { 52 | screenSize: { 53 | width: width, 54 | height: height 55 | }, 56 | shotSize: { 57 | width: width, 58 | height: height 59 | }, 60 | zoomFactor: zoomFactor 61 | }; 62 | 63 | webshot(url, options, function(err, stream) { 64 | if (err) { 65 | res.send(500, 'Failed to process the site!'); 66 | return; 67 | } 68 | stream.pipe(res); 69 | }); 70 | }); 71 | 72 | http.createServer(app).listen(app.get('port'), function(){ 73 | console.log('Express server listening on port ' + app.get('port')); 74 | }); 75 | })(); 76 | -------------------------------------------------------------------------------- /src/Codegyre/PhantomShot/js/phantomshot.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | (function() { 3 | var argv = require('optimist') 4 | .usage('Usage: $0 [options] ') 5 | .option('o', { 6 | alias: 'output', 7 | default: 'shot.png', 8 | describe: 'target output file' 9 | }) 10 | .option('w', { 11 | alias: 'width', 12 | default: 1024, 13 | describe: 'browser window width' 14 | }) 15 | .option('h', { 16 | alias: 'height', 17 | default: 768, 18 | describe: 'browser window height' 19 | }) 20 | .option('z', { 21 | alias: 'zoom', 22 | default: 1.0, 23 | describe: 'browser zoom factor' 24 | }) 25 | .option('t', { 26 | alias: 'timeout', 27 | default: 0, 28 | describe: 'number of milliseconds to wait before killing the phantomjs process and assuming webshotting has failed. (0 is no timeout.)' 29 | }) 30 | .option('d', { 31 | alias: 'delay', 32 | default: 0, 33 | describe: 'number of milliseconds to wait after a page loads before taking the screenshot' 34 | }) 35 | .demand(1) 36 | .argv; 37 | 38 | var options = { 39 | screenSize: { 40 | width: argv.w, 41 | height: argv.h 42 | }, 43 | shotSize: { 44 | width: argv.w, 45 | height: argv.h 46 | }, 47 | zoomFactor: argv.z, 48 | timeout: argv.t, 49 | renderDelay: argv.d 50 | }; 51 | 52 | var webshot = require('webshot'); 53 | webshot(argv._.pop(), argv.o, options, function(err) { 54 | if (err) { 55 | console.error(err); 56 | process.exit(1); 57 | } 58 | }); 59 | })(); --------------------------------------------------------------------------------