Thumbnail.php 10.7 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
15
16
17
18
 
use IMATHUZH\Qfq\Core\Helper\HelperFile;
use IMATHUZH\Qfq\Core\Store\Store;
use IMATHUZH\Qfq\Core\Helper\Support;
use IMATHUZH\Qfq\Core\Helper\Token;
use IMATHUZH\Qfq\Core\Helper\OnString;
19
20
21

//use qfq;

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

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

    private $inkscape = '';
    private $convert = '';
    private $thumbnailDirSecure = '';
    private $thumbnailDirPublic = '';

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

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

        $this->inkscape = $this->store->getVar(SYSTEM_CMD_INKSCAPE, STORE_SYSTEM);
        $this->convert = $this->store->getVar(SYSTEM_CMD_CONVERT, STORE_SYSTEM);
        $this->thumbnailDirSecure = $this->store->getVar(SYSTEM_THUMBNAIL_DIR_SECURE, STORE_SYSTEM);
        $this->thumbnailDirPublic = $this->store->getVar(SYSTEM_THUMBNAIL_DIR_PUBLIC, STORE_SYSTEM);
    }

    /**
56
57
58
59
60
61
62
     * 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'
     *
63
     * @param string $str
64
     * @param string $modeRender
65
     * @return string
Marc Egger's avatar
Marc Egger committed
66
67
68
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
69
     */
70
    public function process($str, $modeRender = THUMBNAIL_PREPARE) {
71
72
73
74
75
76

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

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

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

        $pathFilenameSource = $control[TOKEN_THUMBNAIL];

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

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

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

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

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

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

123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
        // 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.
            }
142

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

149
150
151
152
        // 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);
153
154
155
156
157
158
159
160
161
162
163
164
165
        }

        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
166
     * @param $debugMode
167
     * @return string
Marc Egger's avatar
Marc Egger committed
168
169
170
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
171
     */
172
    private function createThumbnail($pathFilenameSource, $pathFilenameThumbnail, $dimension, $debugMode) {
173
174
175
176
177
178
        $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
179
            HelperFile::mkDirParent($pathFilenameThumbnail);
180
            if (false === touch($pathFilenameThumbnail)) {
Marc Egger's avatar
Marc Egger committed
181
                throw new \UserReportException('Could not create file: ' . OnString::strrstr($pathFilenameSource, '/'), ERROR_IO_CREATE_FILE);
182
183
184
185
186
187
188
            }
        }

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

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

207
208
        $cmd = $this->convert . " -scale $dimension $pathFilenameSource $pathFilenameThumbnail";
        $output = Support::qfqExec($cmd, $rc);
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238

        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:
                    $placeholder = Support::joinPath(THUMBNAIL_UNKNOWN_TYPE, $ext . '.gif');
                    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
239
            throw new \UserReportException("Thumbnail creation failed: " . OnString::strrstr($pathFilenameSource, '/'), ERROR_THUMBNAIL_RENDER);
240
241
242
243
244
245
        }

        return $pathFilenameThumbnail;
    }

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

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

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

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

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

        return "<img $attribute>";

    }

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

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

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

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