Passed
Push — master ( b9de05...5e23c3 )
by Greg
06:49
created

ImportThumbnailsController::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2019 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Http\Controllers\Admin;
21
22
use FilesystemIterator;
23
use Fisharebest\Webtrees\Functions\FunctionsImport;
24
use Fisharebest\Webtrees\I18N;
25
use Fisharebest\Webtrees\Media;
26
use Fisharebest\Webtrees\Services\TreeService;
27
use Fisharebest\Webtrees\Webtrees;
28
use Illuminate\Database\Capsule\Manager as DB;
29
use Illuminate\Database\Query\Expression;
30
use Illuminate\Database\Query\JoinClause;
31
use Intervention\Image\ImageManager;
32
use Psr\Http\Message\ResponseInterface;
33
use Psr\Http\Message\ServerRequestInterface;
34
use RecursiveDirectoryIterator;
35
use RecursiveIteratorIterator;
36
use Throwable;
37
38
/**
39
 * Controller for importing custom thumbnails from webtrees 1.x.
40
 */
41
class ImportThumbnailsController extends AbstractAdminController
42
{
43
    /** @var TreeService */
44
    private $tree_service;
45
46
    /**
47
     * ImportThumbnailsController constructor.
48
     *
49
     * @param TreeService $tree_service
50
     */
51
    public function __construct(TreeService $tree_service)
52
    {
53
        $this->tree_service = $tree_service;
54
    }
55
56
    /**
57
     * Import custom thumbnails from webtres 1.x.
58
     *
59
     * @param ServerRequestInterface $request
60
     *
61
     * @return ResponseInterface
62
     */
63
    public function webtrees1Thumbnails(ServerRequestInterface $request): ResponseInterface
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

63
    public function webtrees1Thumbnails(/** @scrutinizer ignore-unused */ ServerRequestInterface $request): ResponseInterface

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
64
    {
65
        return $this->viewResponse('admin/webtrees1-thumbnails', [
66
            'title' => I18N::translate('Import custom thumbnails from webtrees version 1'),
67
        ]);
68
    }
69
70
    /**
71
     * Import custom thumbnails from webtres 1.x.
72
     *
73
     * @param ServerRequestInterface $request
74
     *
75
     * @return ResponseInterface
76
     */
77
    public function webtrees1ThumbnailsAction(ServerRequestInterface $request): ResponseInterface
78
    {
79
        $thumbnail = $request->getParsedBody()['thumbnail'];
80
        $action    = $request->getParsedBody()['action'];
81
        $xrefs     = $request->getParsedBody()['xref'];
82
        $geds      = $request->getParsedBody()['ged'];
83
84
        $media_objects = [];
85
86
        foreach ($xrefs as $key => $xref) {
87
            $tree            = $this->tree_service->findByName($geds[$key]);
88
            $media_objects[] = Media::getInstance($xref, $tree);
0 ignored issues
show
Bug introduced by
It seems like $tree can also be of type null; however, parameter $tree of Fisharebest\Webtrees\Media::getInstance() does only seem to accept Fisharebest\Webtrees\Tree, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

88
            $media_objects[] = Media::getInstance($xref, /** @scrutinizer ignore-type */ $tree);
Loading history...
89
        }
90
91
        $thumbnail = WT_DATA_DIR . $thumbnail;
92
93
        switch ($action) {
94
            case 'delete':
95
                if (file_exists($thumbnail)) {
96
                    unlink($thumbnail);
97
                }
98
                break;
99
100
            case 'add':
101
                $image_size = getimagesize($thumbnail);
102
                [, $extension] = explode('/', $image_size['mime']);
103
                $move_to = dirname($thumbnail, 2) . '/' . sha1_file($thumbnail) . '.' . $extension;
104
                rename($thumbnail, $move_to);
105
106
                foreach ($media_objects as $media_object) {
107
                    $prefix = WT_DATA_DIR . $media_object->tree()->getPreference('MEDIA_DIRECTORY');
108
                    $gedcom = '1 FILE ' . substr($move_to, strlen($prefix)) . "\n2 FORM " . $extension;
109
110
                    if ($media_object->firstImageFile() === null) {
111
                        // The media object doesn't have an image.  Add this as a secondary file.
112
                        $media_object->createFact($gedcom, true);
113
                    } else {
114
                        // The media object already has an image.  Show this custom one in preference.
115
                        $gedcom = '0 @' . $media_object->xref() . "@ OBJE\n" . $gedcom;
116
                        foreach ($media_object->facts() as $fact) {
117
                            $gedcom .= "\n" . $fact->gedcom();
118
                        }
119
                        $media_object->updateRecord($gedcom, true);
120
                    }
121
122
                    // Accept the changes, to keep the filesystem in sync with the GEDCOM data.
123
                    FunctionsImport::acceptAllChanges($media_object->xref(), $media_object->tree());
124
                }
125
                break;
126
        }
127
128
        return response([]);
129
    }
130
131
    /**
132
     * Import custom thumbnails from webtres 1.x.
133
     *
134
     * @param ServerRequestInterface $request
135
     *
136
     * @return ResponseInterface
137
     */
138
    public function webtrees1ThumbnailsData(ServerRequestInterface $request): ResponseInterface
139
    {
140
        $start  = (int) $request->getQueryParams()['start'];
141
        $length = (int) $request->getQueryParams()['length'];
142
        $search = $request->getQueryParams()['search']['value'];
143
144
        // Fetch all thumbnails
145
        $thumbnails = [];
146
147
        $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(WT_DATA_DIR, FilesystemIterator::FOLLOW_SYMLINKS));
148
149
        foreach ($iterator as $iteration) {
150
            if ($iteration->isFile() && strpos($iteration->getPathname(), '/thumbs/') !== false) {
151
                $thumbnails[] = $iteration->getPathname();
152
            }
153
        }
154
155
        $recordsTotal = count($thumbnails);
156
157
        if ($search !== '') {
158
            $thumbnails = array_filter($thumbnails, static function (string $thumbnail) use ($search): bool {
159
                return stripos($thumbnail, $search) !== false;
160
            });
161
        }
162
163
        $recordsFiltered = count($thumbnails);
164
165
        $thumbnails = array_slice($thumbnails, $start, $length);
166
167
        // Turn each filename into a row for the table
168
        $data = array_map(function (string $thumbnail): array {
169
            $original = $this->findOriginalFileFromThumbnail($thumbnail);
170
171
            $original_url  = route('unused-media-thumbnail', [
172
                'folder' => dirname($original),
173
                'file'   => basename($original),
174
                'w'      => 100,
175
                'h'      => 100,
176
            ]);
177
            $thumbnail_url = route('unused-media-thumbnail', [
178
                'folder' => dirname($thumbnail),
179
                'file'   => basename($thumbnail),
180
                'w'      => 100,
181
                'h'      => 100,
182
            ]);
183
184
            $difference = $this->imageDiff($thumbnail, $original);
185
186
            $original_path  = substr($original, strlen(WT_DATA_DIR));
187
            $thumbnail_path = substr($thumbnail, strlen(WT_DATA_DIR));
188
189
            $media = $this->findMediaObjectsForMediaFile($original_path);
190
191
            $media_links = array_map(static function (Media $media): string {
192
                return '<a href="' . e($media->url()) . '">' . $media->fullName() . '</a>';
193
            }, $media);
194
195
            $media_links = implode('<br>', $media_links);
196
197
            $action = view('admin/webtrees1-thumbnails-form', [
198
                'difference' => $difference,
199
                'media'      => $media,
200
                'thumbnail'  => $thumbnail_path,
201
            ]);
202
203
            return [
204
                '<img src="' . e($thumbnail_url) . '" title="' . e($thumbnail_path) . '">',
205
                '<img src="' . e($original_url) . '" title="' . e($original_path) . '">',
206
                $media_links,
207
                I18N::percentage($difference / 100.0, 0),
208
                $action,
209
            ];
210
        }, $thumbnails);
211
212
        return response([
213
            'draw'            => (int) $request->getQueryParams()['draw'],
214
            'recordsTotal'    => $recordsTotal,
215
            'recordsFiltered' => $recordsFiltered,
216
            'data'            => $data,
217
        ]);
218
    }
219
220
    /**
221
     * Find the original image that corresponds to a (webtrees 1.x) thumbnail file.
222
     *
223
     * @param string $thumbnail
224
     *
225
     * @return string
226
     */
227
    private function findOriginalFileFromThumbnail(string $thumbnail): string
228
    {
229
        // First option - a file with the same name
230
        $original = str_replace('/thumbs/', '/', $thumbnail);
231
232
        // Second option - a .PNG thumbnail for some other image type
233
        if (substr_compare($original, '.png', -4, 4) === 0) {
234
            $pattern = substr($original, 0, -3) . '*';
235
            $matches = glob($pattern, GLOB_NOSORT);
236
            if (!empty($matches) && is_file($matches[0])) {
237
                $original = $matches[0];
238
            }
239
        }
240
241
        return $original;
242
    }
243
244
    /**
245
     * Find the media object that uses a particular media file.
246
     *
247
     * @param string $file
248
     *
249
     * @return Media[]
250
     */
251
    private function findMediaObjectsForMediaFile(string $file): array
252
    {
253
        return DB::table('media')
254
            ->join('media_file', static function (JoinClause $join): void {
255
                $join
256
                    ->on('media_file.m_file', '=', 'media.m_file')
257
                    ->on('media_file.m_id', '=', 'media.m_id');
258
            })
259
            ->join('gedcom_setting', 'media.m_file', '=', 'gedcom_setting.gedcom_id')
260
            ->where(new Expression('setting_value || multimedia_file_refn'), '=', $file)
261
            ->select(['media.*'])
262
            ->distinct()
263
            ->get()
264
            ->map(Media::rowMapper())
265
            ->all();
266
    }
267
268
    /**
269
     * Compare two images, and return a quantified difference.
270
     * 0 (different) ... 100 (same)
271
     *
272
     * @param string $thumbanil
273
     * @param string $original
274
     *
275
     * @return int
276
     */
277
    private function imageDiff($thumbanil, $original): int
278
    {
279
        try {
280
            if (getimagesize($thumbanil) === false) {
281
                return 100;
282
            }
283
        } catch (Throwable $ex) {
284
            // If the first file is not an image then similarity is unimportant.
285
            // Response with an exact match, so the GUI will recommend deleting it.
286
            return 100;
287
        }
288
289
        try {
290
            if (getimagesize($original) === false) {
291
                return 0;
292
            }
293
        } catch (Throwable $ex) {
294
            // If the first file is not an image then the thumbnail .
295
            // Response with an exact mismatch, so the GUI will recommend importing it.
296
            return 0;
297
        }
298
299
        $pixels1 = $this->scaledImagePixels($thumbanil);
300
        $pixels2 = $this->scaledImagePixels($original);
301
302
        $max_difference = 0;
303
304
        foreach ($pixels1 as $x => $row) {
305
            foreach ($row as $y => $pixel) {
306
                $max_difference = max($max_difference, abs($pixel - $pixels2[$x][$y]));
307
            }
308
        }
309
310
        // The maximum difference is 255 (black versus white).
311
        return 100 - intdiv($max_difference * 100, 255);
312
    }
313
314
    /**
315
     * Scale an image to 10x10 and read the individual pixels.
316
     * This is a slow operation, add we will do it many times on
317
     * the "import wetbrees 1 thumbnails" page so cache the results.
318
     *
319
     * @param string $path
320
     *
321
     * @return int[][]
322
     */
323
    private function scaledImagePixels($path): array
324
    {
325
        $size       = 10;
326
        $sha1       = sha1_file($path);
327
328
        $cache_dir  = Webtrees::DATA_DIR . 'cache/';
329
330
        if (!is_dir($cache_dir)) {
331
            mkdir($cache_dir);
332
        }
333
334
        $cache_file = $cache_dir . $sha1 . '.php';
335
336
        if (file_exists($cache_file)) {
337
            return include $cache_file;
338
        }
339
340
        $manager = new ImageManager();
341
        $image   = $manager->make($path)->resize($size, $size);
342
343
        $pixels = [];
344
        for ($x = 0; $x < $size; ++$x) {
345
            $pixels[$x] = [];
346
            for ($y = 0; $y < $size; ++$y) {
347
                $pixel          = $image->pickColor($x, $y);
348
                $pixels[$x][$y] = (int) (($pixel[0] + $pixel[1] + $pixel[2]) / 3);
349
            }
350
        }
351
352
        file_put_contents($cache_file, '<?php return ' . var_export($pixels, true) . ';');
353
354
        return $pixels;
355
    }
356
}
357