1<?php
2
3use dokuwiki\Extension\SyntaxPlugin;
4
5/**
6 * Embed an image gallery
7 *
8 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
9 * @author     Andreas Gohr <andi@splitbrain.org>
10 */
11class syntax_plugin_panoview extends SyntaxPlugin
12{
13    /**
14     * What kind of syntax are we?
15     */
16    public function getType()
17    {
18        return 'substition';
19    }
20
21    /**
22     * What about paragraphs?
23     */
24    public function getPType()
25    {
26        return 'block';
27    }
28
29    /**
30     * Where to sort in?
31     */
32    public function getSort()
33    {
34        return 301;
35    }
36
37    /**
38     * Connect pattern to lexer
39     */
40    public function connectTo($mode)
41    {
42        $this->Lexer->addSpecialPattern('\{\{panoview>[^}]*\}\}', $mode, 'plugin_panoview');
43    }
44
45    /**
46     * Handle the match
47     */
48    public function handle($match, $state, $pos, Doku_Handler $handler)
49    {
50        global $ID;
51
52        $data = ['width' => 500, 'height' => 250, 'align' => 0, 'initialZoom' => 1, 'tileBaseUri' => DOKU_BASE . 'lib/plugins/panoview/tiles.php', 'tileSize' => 256, 'maxZoom' => 10, 'blankTile' => DOKU_BASE . 'lib/plugins/panoview/gfx/blank.gif', 'loadingTile' => DOKU_BASE . 'lib/plugins/panoview/gfx/progress.gif'];
53
54        $match = substr($match, 11, -2); //strip markup from start and end
55
56        // alignment
57        $data['align'] = 0;
58        if (str_starts_with($match, ' ')) $data['align'] += 1;
59        if (str_ends_with($match, ' ')) $data['align'] += 2;
60
61        // extract params
62        [$img, $params] = explode('?', $match, 2);
63        $img = trim($img);
64
65        // resolving relatives
66        $data['image'] = resolve_id(getNS($ID), $img);
67
68        $file = mediaFN($data['image']);
69        [$data['imageWidth'], $data['imageHeight']] = @getimagesize($file);
70
71        // calculate maximum zoom
72        $data['maxZoom'] = ceil(sqrt(max($data['imageWidth'], $data['imageHeight']) / $data['tileSize']));
73
74        // size
75        if (preg_match('/\b(\d+)[xX](\d+)\b/', $params, $match)) {
76            $data['width'] = $match[1];
77            $data['height'] = $match[2];
78        }
79
80        // initial zoom
81        if (preg_match('/\b[zZ](\d+)\b/', $params, $match)) {
82            $data['initialZoom'] = $match[1];
83        }
84        if ($data['initialZoom'] < 0) $data['initialZoom'] = 0;
85        if ($data['initialZoom'] > $data['maxZoom']) $data['initialZoom'] = $data['maxZoom'];
86
87        return $data;
88    }
89
90    /**
91     * Create output
92     */
93    public function render($mode, Doku_Renderer $R, $data)
94    {
95        if ($mode != 'xhtml') return false;
96        global $ID;
97
98        $img = '<a href="' . ml($data['image'], ['id' => $ID], false) . '"><img src="' .
99            ml($data['image'], ['w' => $data['width'], 'h' => $data['height']]) . '" width="' .
100            $data['width'] . '" height="' . $data['height'] . '" alt="" /></a>';
101
102        if ($data['align'] == 1) {
103            $align = 'medialeft';
104        } elseif ($data['align'] == 2) {
105            $align = 'mediaright';
106        } else {
107            $align = 'mediacenter';
108        }
109
110        $R->doc .= '
111            <div class="panoview_plugin ' . $align . '" style="width: ' . $data['width'] . 'px; height: ' . $data['height'] . 'px;">
112              <div class="well"><!-- --></div>
113              <div class="surface">' . $img . '</div>
114              <p class="controls" style="display: none">
115                <span class="zoomIn" title="Zoom In">+</span>
116                <span class="zoomOut" title="Zoom Out">-</span>
117                <span class="maximize"><img src="' . DOKU_BASE . 'lib/plugins/panoview/gfx/window.gif" style="position: absolute; bottom: 4px; right: 5px;" title="Maximize"></span>
118              </p>
119                <div class="options" style="display:none">' . hsc(json_encode($data)) . '</div>
120            </div>
121        ';
122
123        return true;
124    }
125
126    // ----------- Tile Generator below ---------------
127
128    /**
129     * Create a tile using libGD
130     */
131    public function tileGD($d)
132    {
133        global $conf;
134
135        $img = null;
136        if (preg_match('/\.jpe?g$/', $d['file'])) {
137            $img = @imagecreatefromjpeg($d['file']);
138        } elseif (preg_match('/\.png$/', $d['file'])) {
139            $img = @imagecreatefrompng($d['file']);
140        } elseif (preg_match('/\.gif$/', $d['file'])) {
141            $img = @imagecreatefromgif($d['file']);
142        }
143        if ($img === null) $this->gfxError('generic');
144
145        $crop = $this->imageCrop($img, $d['width'], $d['height'], $d['tlx'], $d['tly'], $d['brx'], $d['bry']);
146        imagedestroy($img);
147
148        $scale = $this->imageScale($crop, abs($d['brx'] - $d['tlx']), abs($d['bry'] - $d['tly']), $d['ts'], $d['ts']);
149        imagedestroy($crop);
150
151        imagejpeg($scale, $d['cache'], $conf['jpg_quality']);
152        imagedestroy($scale);
153
154        if ($conf['fperm']) chmod($d['cache'], $conf['fperm']);
155    }
156
157    /**
158     * Create a tile using Image Magick
159     */
160    public function tileIM($d)
161    {
162        global $conf;
163
164        $cmd = $this->getConf('nice');
165        $cmd .= ' ' . $conf['im_convert'];
166        $cmd .= ' ' . escapeshellarg($d['file']);
167        $cmd .= ' -crop \'' . abs($d['brx'] - $d['tlx']) . 'x' . abs($d['bry'] - $d['tly']) .
168                '!+' . $d['tlx'] . '+' . $d['tly'] . '\'';
169        $cmd .= ' -background black';
170        $cmd .= ' -extent \'' . abs($d['brx'] - $d['tlx']) . 'x' . abs($d['bry'] - $d['tly']) . '!\'';
171        $cmd .= ' -resize \'' . $d['ts'] . 'x' . $d['ts'] . '!\'';
172
173        $cmd .= ' -quality ' . $conf['jpg_quality'];
174        $cmd .= ' ' . escapeshellarg($d['cache']);
175
176        #    dbg($cmd); exit;
177
178        @exec($cmd, $out, $retval);
179        if ($retval == 0) return true;
180        $this->gfxError('generic');
181    }
182
183    /**
184     * Scale an image with libGD
185     */
186    public function imageScale($image, $x, $y, $w, $h)
187    {
188        $scale = imagecreatetruecolor($w, $h);
189        imagecopyresampled($scale, $image, 0, 0, 0, 0, $w, $h, $x, $y);
190        return $scale;
191    }
192
193    /**
194     * Crop an image with libGD
195     */
196    public function imageCrop($image, $x, $y, $left, $upper, $right, $lower)
197    {
198        $w = abs($right - $left);
199        $h = abs($lower - $upper);
200        $crop = imagecreatetruecolor($w, $h);
201        imagecopy($crop, $image, 0, 0, $left, $upper, $w, $h);
202        return $crop;
203    }
204
205    /**
206     * Send a graphical error message and stop script
207     */
208    public function gfxError($type)
209    {
210        $file = __DIR__ . '/gfx/' . $type . '.gif';
211        $time = filemtime($file);
212        header('Content-type: image/gif');
213
214        http_conditionalRequest($time);
215        http_sendfile($file);
216        readfile($file);
217        exit;
218    }
219
220    /**
221     * Acquire a lock for the tile generator
222     */
223    public function tileLock($d)
224    {
225        global $conf;
226
227        $lockDir = $conf['lockdir'] . '/' . md5($d['id']) . '.panoview';
228        @ignore_user_abort(1);
229
230        $timeStart = time();
231        do {
232            //waited longer than 25 seconds? -> stale lock?
233            if ((time() - $timeStart) > 25) {
234                if (time() - @filemtime($lockDir) > 30) $this->tileUnlock($d);
235                send_redirect(
236                    DOKU_URL . 'lib/plugins/panoview/tiles.php?tile=' .
237                    $d['zoom'] . '-' . $d['col'] . '-' . $d['row'] . '&image=' . rawurlencode($d['id'])
238                );
239                exit;
240            }
241            $locked = @mkdir($lockDir, $conf['dmode']);
242            if ($locked) {
243                if (!empty($conf['dperm'])) chmod($lockDir, $conf['dperm']);
244                break;
245            }
246            usleep(random_int(500, 3000));
247        } while ($locked === false);
248    }
249
250    /**
251     * Unlock the tile generator
252     */
253    public function tileUnlock($d)
254    {
255        global $conf;
256
257        $lockDir = $conf['lockdir'] . '/' . md5($d['id']) . '.panoview';
258        @rmdir($lockDir);
259        @ignore_user_abort(0);
260    }
261}
262