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