Thumbnail.php 10.9 KB
Newer Older
1
2
3
4
5
6
<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 2/10/18
 * Time: 3:14 PM
7
8
 *
 * See: doc/THUMBNAIL.md
9
10
 */

Marc Egger's avatar
Marc Egger committed
11
namespace IMATHUZH\Qfq\Core\Report;
12

Marc Egger's avatar
Marc Egger committed
13
14
 
use IMATHUZH\Qfq\Core\Helper\HelperFile;
15
use IMATHUZH\Qfq\Core\Helper\Path;
Marc Egger's avatar
Marc Egger committed
16
17
18
19
use IMATHUZH\Qfq\Core\Store\Store;
use IMATHUZH\Qfq\Core\Helper\Support;
use IMATHUZH\Qfq\Core\Helper\Token;
use IMATHUZH\Qfq\Core\Helper\OnString;
20
21
22

//use qfq;

23
24
25
26
/**
 * Class Thumbnail
 * @package qfq
 */
27
28
29
30
31
32
33
34
35
class Thumbnail {

    /**
     * @var Store
     */
    private $store = null;

    private $inkscape = '';
    private $convert = '';
36
37
    private $absoluteThumbnailDirSecure = '';
    private $absoluteThumbnailDirPublic = '';
38
39
40

    /**
     * @param bool|false $phpUnit
Marc Egger's avatar
Marc Egger committed
41
42
43
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
44
45
     */
    public function __construct($phpUnit = false) {
46
47

        #TODO: rewrite $phpUnit to: "if (!defined('PHPUNIT_QFQ')) {...}"
48
49
50
51
        $this->store = Store::getInstance();

        $this->inkscape = $this->store->getVar(SYSTEM_CMD_INKSCAPE, STORE_SYSTEM);
        $this->convert = $this->store->getVar(SYSTEM_CMD_CONVERT, STORE_SYSTEM);
52
53
        $this->absoluteThumbnailDirSecure = Path::absoluteApp($this->store->getVar(SYSTEM_THUMBNAIL_DIR_SECURE_REL_TO_APP, STORE_SYSTEM));
        $this->absoluteThumbnailDirPublic = Path::absoluteApp($this->store->getVar(SYSTEM_THUMBNAIL_DIR_PUBLIC_REL_TO_APP, STORE_SYSTEM));
54
55
56
    }

    /**
57
58
59
60
61
62
63
     * Renders a thumbnail based on $str.
     * $str: "T:<pathfilename source>|W:<dimension>|s:<0|1>"
     *
     * Argument 'T' is mandatory.
     * Argument 'W' is optional. Defaults to 'W:150x'.
     * Argument 's' is optional. Defaults to 's:1'
     *
64
     * @param string $str
65
     * @param string $modeRender
66
     * @return string
Marc Egger's avatar
Marc Egger committed
67
68
69
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
70
     */
71
    public function process($str, $modeRender = THUMBNAIL_PREPARE) {
72
73
74
75
76
77

        $control = Token::explodeTokenString($str);
        Support::setIfNotSet($control, TOKEN_SIP);
        $control[TOKEN_SIP] = Token::checkForEmptyValue(TOKEN_SIP, $control[TOKEN_SIP]);

        if (empty($control[TOKEN_THUMBNAIL])) {
78
            return '';
79
80
        }

81
82
        if (empty($control[TOKEN_THUMBNAIL_DIMENSION])) {
            $control[TOKEN_THUMBNAIL_DIMENSION] = THUMBNAIL_WIDTH_DEFAULT;
83
84
85
86
        }

        $pathFilenameSource = $control[TOKEN_THUMBNAIL];

87
        $dir = ($control[TOKEN_SIP] == "1") ? $this->absoluteThumbnailDirSecure : $this->absoluteThumbnailDirPublic;
88
        $pathFilenameThumbnail = Support::joinPath($dir, md5($pathFilenameSource . $control[TOKEN_THUMBNAIL_DIMENSION]) . '.png');
89

90
91
92
93
        // Check if the file has to exist now.
        if ($modeRender == THUMBNAIL_VIA_DOWNLOAD || $control[TOKEN_SIP] != "1") {
            $pathFilenameThumbnail = $this->getOrCreateThumbnail($pathFilenameSource, $pathFilenameThumbnail, $control, $modeRender);
        }
94

95
        return $this->buildImageTag($str, $modeRender, $control, $pathFilenameThumbnail);
96
97
98
    }

    /**
99
100
101
     * Creates a thumbnail (saved under $pathFilenameThumbnail) based on $pathFilenameSource.
     * Returns the pathFilename of the thumbnail.
     *
102
103
     * @param string $pathFilenameSource
     * @param string $pathFilenameThumbnail
104
105
     * @param array $control
     * @param string $modeRender DOWNLOAD_RENDER_AUTO | DOWNLOAD_RENDER_NOW
106
     * @return string
Marc Egger's avatar
Marc Egger committed
107
108
109
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
110
     */
111
    private function getOrCreateThumbnail($pathFilenameSource, $pathFilenameThumbnail, array $control, $modeRender) {
112
        $debugMode = false;
113
        $statThumbnail=false;
114

115
116
        // Using stat() on non existing files throws an exception
        if (!file_exists($pathFilenameSource) || false === ($statSource = stat($pathFilenameSource))) {
Marc Egger's avatar
Marc Egger committed
117
            throw new \UserFormException('File not found: "' . OnString::strrstr($pathFilenameSource, '/') . '"', ERROR_IO_FILE_NOT_FOUND);
118
119
        }

120
121
122
123
        if(file_exists($pathFilenameThumbnail)) {
            $statThumbnail = stat($pathFilenameThumbnail);
        }

124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
        // thumbnail already exist?
        if ($statThumbnail !== false) {

            // Another process already creating this thumbnail? Just wait until it's finished.
            if ($statThumbnail['size'] == 0) {

                if (time() - $statThumbnail['mtime'] > THUMBNAIL_MAX_SECONDS) {
                    unlink($pathFilenameThumbnail); // remove old empty files: this gives a chance to rerender the image on the next call.
                    return 'brokenimage';
                }

                $max = THUMBNAIL_MAX_SECONDS;
                while ($statThumbnail['size'] == 0 && $max-- > 0) {
                    sleep(1);
                    clearstatcache();
                    $statThumbnail = stat($pathFilenameThumbnail);
                }
                sleep(1); // additional time to be sure that the whole file is written.
            }
143

144
145
146
            // Check if the file is recent.
            if ($statSource['mtime'] < $statThumbnail['mtime']) {
                return $pathFilenameThumbnail;
147
            }
148
149
        }

150
151
152
153
        // Render thumbnail: either it's a) public thumbnail or b) requested via download.php
        if (($modeRender == THUMBNAIL_PREPARE && $control[TOKEN_SIP] != '1') || ($modeRender == THUMBNAIL_VIA_DOWNLOAD)) {

            $pathFilenameThumbnail = $this->createThumbnail($pathFilenameSource, $pathFilenameThumbnail, $control[TOKEN_THUMBNAIL_DIMENSION], $debugMode);
154
155
156
157
158
159
160
161
162
163
164
165
166
        }

        return $pathFilenameThumbnail;
    }

    /**
     * Creates a thumbnail from '$pathFilenameSource' under '$pathFilenameThumbnail'.
     * SVG will be rendered via 'inkscape', all others via 'convert'.
     * Creates an empty '$pathFilenameThumbnail' before the rendering starts. Will be overwritten through the rendering.
     *
     * @param string $pathFilenameSource
     * @param string $pathFilenameThumbnail
     * @param string $dimension
167
     * @param $debugMode
168
     * @return string
Marc Egger's avatar
Marc Egger committed
169
170
171
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
172
     */
173
    private function createThumbnail($pathFilenameSource, $pathFilenameThumbnail, $dimension, $debugMode) {
174
175
176
177
178
179
        $outputInkscape = '';
        $cmdInkscape = '';

        // Indicates a running thumbnail rendering process.
        if (false === touch($pathFilenameThumbnail)) {
            // Be sure that the target directory exist
Carsten  Rose's avatar
Carsten Rose committed
180
            HelperFile::mkDirParent($pathFilenameThumbnail);
181
            if (false === touch($pathFilenameThumbnail)) {
Marc Egger's avatar
Marc Egger committed
182
                throw new \UserReportException('Could not create file: ' . OnString::strrstr($pathFilenameSource, '/'), ERROR_IO_CREATE_FILE);
183
184
185
186
187
188
189
            }
        }

        // Get extension.
        $ext = strtolower(OnString::strrstr($pathFilenameSource, '.'));

        // SVG files are best to thumbnail via 'inkscape'
190
191
//        if ( ($ext == 'svg' || $ext == 'pdf') && $this->inkscape != '') {
        if (($ext == 'svg') && $this->inkscape != '') {
192
            $inkscapeDimension = Token::explodeDimension($dimension);
193
            // Automatically cut white border: --export-area-drawing
194
            // --export-text-to-path
195
196
            $cmdInkscape = $this->inkscape . " --without-gui $inkscapeDimension --export-png $pathFilenameThumbnail $pathFilenameSource";
            $outputInkscape = Support::qfqExec($cmdInkscape, $rc);
197
198
199
200
201
202
203
204
205
206
207
            if ($rc == 0) {
                return $pathFilenameThumbnail;
            }
            // if process failed, try 'convert' below.
        }

        // On multi page files: just take the first page
        if ($ext == 'pdf') {
            $pathFilenameSource .= '[0]';
        }

208
209
        $cmd = $this->convert . " -scale $dimension $pathFilenameSource $pathFilenameThumbnail";
        $output = Support::qfqExec($cmd, $rc);
210
211
212
213
214
215
216
217
218
219
220
221
222
223

        if ($rc != 0) {
            // Check if the extension is from a supported filetype
            switch ($ext) {

                // The following files should always be thumbnail'able - if not, throw an exception.
                case 'pdf':
                case 'jpg':
                case 'gif':
                case 'png':
                case 'svg':
                    break;

                default:
224
                    $placeholder = Support::joinPath(Path::absoluteApp(Path::APP_TO_THUMBNAIL_UNKNOWN_TYPE), $ext . '.gif');
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
                    if (is_readable($placeholder)) {
                        return $placeholder;
                    }
            }

            $debug = 'Source: ' . $pathFilenameSource . PHP_EOL;
            $debug .= 'Thumbnail: ' . $pathFilenameThumbnail . PHP_EOL;
            if ($outputInkscape) {
                $debug .= $cmdInkscape . PHP_EOL;
                $debug .= $outputInkscape . PHP_EOL;
            }
            $debug .= $cmd . PHP_EOL;
            $debug .= $output . PHP_EOL;
            $this->store->setVar(SYSTEM_MESSAGE_DEBUG, $debug, STORE_SYSTEM);

Marc Egger's avatar
Marc Egger committed
240
            throw new \UserReportException("Thumbnail creation failed: " . OnString::strrstr($pathFilenameSource, '/'), ERROR_THUMBNAIL_RENDER);
241
242
243
244
245
246
        }

        return $pathFilenameThumbnail;
    }

    /**
247
     * Returns the thumbnail URL, either:
Marc Egger's avatar
Marc Egger committed
248
     * secure) s:1 >> '<img src=".../Api/download.php?s=....">'
249
     * public) s:0 >> '<img src="fileadmin/<thumb dir>/....png">'
Marc Egger's avatar
Marc Egger committed
250
     * pure URL secure) s:1 >> '.../Api/download.php?s=....'
251
252
253
254
     * pure URL public) s:0 >> 'fileadmin/<thumb dir>/....png'
     *
     * @param string $str
     * @param string $modeRender THUMBNAIL_PREPARE | THUMBNAIL_RENDERED_FILE
255
     * @param array $control
256
     * @param string $pathFilenameThumbnail
257
     * @return string
Marc Egger's avatar
Marc Egger committed
258
259
     * @throws \CodeException
     * @throws \UserFormException
260
     * @internal param bool $flagSecure
261
     */
262
263
264
265
266
    private function buildImageTag($str, $modeRender, array $control, $pathFilenameThumbnail) {

        if ($modeRender == THUMBNAIL_VIA_DOWNLOAD) {
            return $pathFilenameThumbnail;
        }
267

268
        $src = ($control[TOKEN_SIP] == "1") ? $this->buildSecureDownloadLink($pathFilenameThumbnail, $str) : $pathFilenameThumbnail;
269
270
271
272
273

        // With RENDER_MODE_7 return only the URL
        if (isset($control[TOKEN_RENDER]) && $control[TOKEN_RENDER] == RENDER_MODE_7) {
            return $src;
        }
274
275
276
277
278
279
280
281

        $attribute = Support::doAttribute('src', $src);

        return "<img $attribute>";

    }

    /**
282
283
     * Creates a SIP Url to be used as download.
     *
284
     * @param string $pathFilenameThumbnail
285
     * @param $str
286
     * @return string
Marc Egger's avatar
Marc Egger committed
287
288
     * @throws \CodeException
     * @throws \UserFormException
289
     */
290
    private function buildSecureDownloadLink($pathFilenameThumbnail, $str) {
291

292
        $urlParam = Path::urlApi(API_DOWNLOAD_PHP) . '?' . DOWNLOAD_MODE . '=' . DOWNLOAD_MODE_THUMBNAIL;
293
        $urlParam .= '&' . SIP_DOWNLOAD_PARAMETER . '=' . base64_encode(TOKEN_FILE . ':' . $pathFilenameThumbnail . '|' . $str);
294
295
296
297
298
299

        $sip = $this->store->getSipInstance();

        return $sip->queryStringToSip($urlParam);
    }
}