a006a44574955ec4f3f6191b2e3e7097977a898c
[phancap.git] / src / phancap / Adapter / Cutycapt.php
1 <?php
2 /**
3  * Part of phancap
4  *
5  * PHP version 5
6  *
7  * @category  Tools
8  * @package   Adapter_Cutycapt
9  * @author    Christian Weiske <cweiske@cweiske.de>
10  * @copyright 2014 Christian Weiske
11  * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL v3
12  * @link      http://cweiske.de/phancap.htm
13  */
14 namespace phancap;
15
16 /**
17  * Screenshot rendering using the "cutycapt" command line tool.
18  *
19  * @category  Tools
20  * @package   Adapter_Cutycapt
21  * @author    Christian Weiske <cweiske@cweiske.de>
22  * @copyright 2014 Christian Weiske
23  * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL v3
24  * @version   Release: @package_version@
25  * @link      http://cweiske.de/phancap.htm
26  */
27 class Adapter_Cutycapt
28 {
29     /**
30      * Lock file handle
31      * @var resourece
32      */
33     protected $lockHdl;
34
35     /**
36      * Lock file path
37      * @var string
38      */
39     protected $lockFile = null;
40
41     /**
42      * Check if all dependencies are available.
43      *
44      * @return mixed TRUE if all is fine, array with error messages otherwise
45      */
46     public function isAvailable()
47     {
48         $old = error_reporting(error_reporting() & ~E_STRICT);
49         $arErrors = array();
50         if (\System::which('xvfb-run') === false) {
51             $arErrors[] = '"xvfb-run" is not installed';
52         }
53         if (\System::which('cutycapt') === false) {
54             $arErrors[] = '"cutycapt" is not installed';
55         }
56         if (\System::which('convert') === false) {
57             $arErrors[] = '"convert" (imagemagick) is not installed';
58         }
59         if (\System::which('timeout') === false) {
60             $arErrors[] = '"timeout" (GNU coreutils) is not installed';
61         }
62
63         error_reporting($old);
64         if (count($arErrors)) {
65             return $arErrors;
66         }
67
68         return true;
69     }
70
71     /**
72      * Render a website screenshot
73      *
74      * @param Image   $img     Image configuration
75      * @param Options $options Screenshot configuration
76      *
77      * @return void
78      * @throws \Exception When something fails
79      */
80     public function render(Image $img, Options $options)
81     {
82         $format = $options->values['sformat'];
83         if ($format == 'jpg') {
84             $format = 'jpeg';
85         }
86         $maxWaitTime = 30;//seconds
87
88         $serverNumber = $this->getServerNumber($options);
89         $tmpPath = $img->getPath() . '-tmp';
90         $cmd = 'cutycapt'
91             . ' --url=' . escapeshellarg($options->values['url'])
92             . ' --out-format=' . escapeshellarg($format)
93             . ' --out=' . escapeshellarg($tmpPath)
94             . ' --max-wait=' . (($maxWaitTime - 1) * 1000)
95             . ' --min-width=' . $options->values['bwidth'];
96         if ($options->values['bheight'] !== null) {
97             $cmd .= ' --min-height=' . $options->values['bheight'];
98         }
99
100         $xvfbcmd = 'xvfb-run'
101             . ' -e /dev/stdout'
102             . ' --server-args="-screen 0, 1024x768x24"'
103             . ' --server-num=' . $serverNumber;
104         //cutycapt hangs sometimes - https://sourceforge.net/p/cutycapt/bugs/8/
105         // we kill it if it does not exit itself
106         Executor::runForSomeTime($xvfbcmd . ' ' . $cmd, $maxWaitTime);
107
108         $this->resize($tmpPath, $img, $options);
109     }
110
111     /**
112      * Get a free X server number.
113      *
114      * Each xvfb-run process needs its own free server number.
115      * Needed for multiple parallel requests.
116      *
117      * @return integer Server number
118      */
119     protected function getServerNumber()
120     {
121         //clean stale lock files
122         $this->cleanup();
123
124         $num = 100;
125         $bFound = false;
126         do {
127             ++$num;
128             $f = $this->config->cacheDir . 'tmp-curlycapt-server-' . $num . '.lock';
129             $this->lockHdl = fopen($f, 'w');
130             if (flock($this->lockHdl, LOCK_EX | LOCK_NB)) {
131                 $this->lockFile = $f;
132                 $bFound = true;
133                 break;
134             } else {
135                 fclose($this->lockHdl);
136             }
137         } while ($num < 200);
138
139         if (!$bFound) {
140             throw new \Exception('Too many requests running');
141         }
142
143         $this->lockFile = $f;
144         return $num;
145     }
146
147     /**
148      * Unlock lock file and clean up old lock files
149      *
150      * @return void
151      */
152     public function cleanup()
153     {
154         if ($this->lockFile !== null && $this->lockHdl) {
155             flock($this->lockHdl, LOCK_UN);
156             unlink($this->lockFile);
157         }
158
159         $lockFiles = glob(
160             $this->config->cacheDir . 'tmp-curlycapt-server-*.lock'
161         );
162
163         $now = time();
164         foreach ($lockFiles as $file) {
165             if ($now - filemtime($file) > 120) {
166                 //delete stale lock file; probably something crashed.
167                 unlink($file);
168             }
169         }
170     }
171
172     /**
173      * Convert an image to the given size.
174      *
175      * Target file is the path of $img.
176      *
177      * @param string  $tmpPath Path of image to be scaled.
178      * @param Image   $img     Image configuration
179      * @param Options $options Screenshot configuration
180      *
181      * @return void
182      */
183     protected function resize($tmpPath, Image $img, Options $options)
184     {
185         if ($options->values['sformat'] == 'pdf') {
186             //nothing to resize.
187             rename($tmpPath, $img->getPath());
188             return;
189         }
190
191         $crop = '';
192         if ($options->values['smode'] == 'screen') {
193             $crop = ' -crop ' . $options->values['swidth']
194                 . 'x' . $options->values['sheight']
195                 . '+0x0';
196         }
197
198         $convertcmd = 'convert'
199             . ' ' . escapeshellarg($tmpPath)
200             . ' -resize ' . $options->values['swidth']
201             . $crop
202             . ' ' . escapeshellarg($img->getPath());
203         Executor::run($convertcmd);
204         //var_dump($convertcmd);die();
205         unlink($tmpPath);
206     }
207
208     /**
209      * Set phancap configuration
210      *
211      * @param Config $config Phancap configuration
212      *
213      * @return void
214      */
215     public function setConfig(Config $config)
216     {
217         $this->config = $config;
218     }
219 }
220 ?>