Passed
Push — master ( f5be59...b5f5af )
by Greg
06:01
created

ImportThumbnailsController   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 302
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 132
dl 0
loc 302
rs 10
c 1
b 0
f 0
wmc 29

7 Methods

Rating   Name   Duplication   Size   Complexity  
B imageDiff() 0 35 7
A scaledImagePixels() 0 32 5
B webtrees1ThumbnailsAction() 0 52 8
A webtrees1Thumbnails() 0 4 1
A __construct() 0 8 1
A findOriginalFileFromThumbnail() 0 15 4
A webtrees1ThumbnailsData() 0 78 3
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 Fisharebest\Webtrees\I18N;
23
use Fisharebest\Webtrees\Media;
24
use Fisharebest\Webtrees\Services\PendingChangesService;
25
use Fisharebest\Webtrees\Services\SearchService;
26
use Fisharebest\Webtrees\Services\TreeService;
27
use Fisharebest\Webtrees\Webtrees;
28
use Illuminate\Support\Collection;
29
use Intervention\Image\ImageManager;
30
use League\Flysystem\Filesystem;
31
use Psr\Http\Message\ResponseInterface;
32
use Psr\Http\Message\ServerRequestInterface;
33
use Throwable;
34
35
use function assert;
36
37
/**
38
 * Controller for importing custom thumbnails from webtrees 1.x.
39
 */
40
class ImportThumbnailsController extends AbstractAdminController
41
{
42
    /** @var PendingChangesService */
43
    private $pending_changes_service;
44
45
    /** @var SearchService */
46
    private $search_service;
47
48
    /** @var TreeService */
49
    private $tree_service;
50
51
    /**
52
     * ImportThumbnailsController constructor.
53
     *
54
     * @param PendingChangesService $pending_changes_service
55
     * @param SearchService         $search_service
56
     * @param TreeService           $tree_service
57
     */
58
    public function __construct(
59
        PendingChangesService $pending_changes_service,
60
        SearchService $search_service,
61
        TreeService $tree_service
62
    ) {
63
        $this->pending_changes_service = $pending_changes_service;
64
        $this->search_service          = $search_service;
65
        $this->tree_service            = $tree_service;
66
    }
67
68
    /**
69
     * Import custom thumbnails from webtres 1.x.
70
     *
71
     * @param ServerRequestInterface $request
72
     *
73
     * @return ResponseInterface
74
     */
75
    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

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