8d9693cbab028d8e9904d50a97617ea115551d44
[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
87         $maxWaitTime = 30;//seconds
88         if (isset($this->config->cutycapt['maxWaitTime'])) {
89             $maxWaitTime = (int) $this->config->cutycapt['maxWaitTime'];
90         }
91
92         $parameters = '';
93         if (isset($this->config->cutycapt['parameters'])) {
94             $parameters = $this->config->cutycapt['parameters'];
95         }
96
97         $serverNumber = $this->getServerNumber($options);
98         $tmpPath = $img->getPath() . '-tmp';
99         $cmd = 'cutycapt'
100             . ' --url=' . escapeshellarg($options->values['url'])
101             . ' --out-format=' . escapeshellarg($format)
102             . ' --out=' . escapeshellarg($tmpPath)
103             . ' --max-wait=' . (($maxWaitTime - 1) * 1000)
104             . ' --min-width=' . $options->values['bwidth'];
105         if ($options->values['bheight'] !== null) {
106             $cmd .= ' --min-height=' . $options->values['bheight'];
107         }
108         if (strlen($parameters) > 0) {
109             $cmd .= ' ' . $parameters;
110         }
111
112         $xvfbcmd = 'xvfb-run'
113             . ' -e /dev/stdout'
114             . ' --server-args="-screen 0, 1024x768x24"'
115             . ' --server-num=' . $serverNumber;
116         //cutycapt hangs sometimes - https://sourceforge.net/p/cutycapt/bugs/8/
117         // we kill it if it does not exit itself
118         Executor::runForSomeTime($xvfbcmd . ' ' . $cmd, $maxWaitTime);
119
120         //cutycapt does not report timeouts via exit status
121         // https://sourceforge.net/p/cutycapt/bugs/11/
122         if (!file_exists($tmpPath)) {
123             throw new \Exception('Error running cutycapt (wait timeout)', 1);
124         }
125
126         $this->resize($tmpPath, $img, $options);
127     }
128
129     /**
130      * Get a free X server number.
131      *
132      * Each xvfb-run process needs its own free server number.
133      * Needed for multiple parallel requests.
134      *
135      * @return integer Server number
136      */
137     protected function getServerNumber()
138     {
139         //clean stale lock files
140         $this->cleanup();
141
142         $num = 100;
143         $bFound = false;
144         do {
145             ++$num;
146             $f = $this->config->cacheDir . 'tmp-curlycapt-server-' . $num . '.lock';
147             $this->lockHdl = fopen($f, 'w');
148             if (flock($this->lockHdl, LOCK_EX | LOCK_NB)) {
149                 $this->lockFile = $f;
150                 $bFound = true;
151                 break;
152             } else {
153                 fclose($this->lockHdl);
154             }
155         } while ($num < 200);
156
157         if (!$bFound) {
158             throw new \Exception('Too many requests running');
159         }
160
161         $this->lockFile = $f;
162         return $num;
163     }
164
165     /**
166      * Unlock lock file and clean up old lock files
167      *
168      * @return void
169      */
170     public function cleanup()
171     {
172         if ($this->lockFile !== null && $this->lockHdl) {
173             flock($this->lockHdl, LOCK_UN);
174             unlink($this->lockFile);
175         }
176
177         $lockFiles = glob(
178             $this->config->cacheDir . 'tmp-curlycapt-server-*.lock'
179         );
180
181         $now = time();
182         foreach ($lockFiles as $file) {
183             if ($now - filemtime($file) > 120) {
184                 //delete stale lock file; probably something crashed.
185                 unlink($file);
186             }
187         }
188     }
189
190     /**
191      * Convert an image to the given size.
192      *
193      * Target file is the path of $img.
194      *
195      * @param string  $tmpPath Path of image to be scaled.
196      * @param Image   $img     Image configuration
197      * @param Options $options Screenshot configuration
198      *
199      * @return void
200      */
201     protected function resize($tmpPath, Image $img, Options $options)
202     {
203         if ($options->values['sformat'] == 'pdf') {
204             //nothing to resize.
205             rename($tmpPath, $img->getPath());
206             return;
207         }
208
209         $crop = '';
210         if ($options->values['smode'] == 'screen') {
211             $crop = ' -crop ' . $options->values['swidth']
212                 . 'x' . $options->values['sheight']
213                 . '+0x0';
214         }
215
216         $convertcmd = 'convert'
217             . ' ' . escapeshellarg($tmpPath)
218             . ' -resize ' . $options->values['swidth']
219             . $crop
220             . ' ' . escapeshellarg($img->getPath());
221         Executor::run($convertcmd);
222         //var_dump($convertcmd);die();
223         unlink($tmpPath);
224     }
225
226     /**
227      * Set phancap configuration
228      *
229      * @param Config $config Phancap configuration
230      *
231      * @return void
232      */
233     public function setConfig(Config $config)
234     {
235         $this->config = $config;
236     }
237 }
238 ?>