Passed
Push — master ( 618eec...a04bb9 )
by Greg
05:12
created

webtrees1ThumbnailsData()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 78
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 50
nc 2
nop 1
dl 0
loc 78
rs 9.0909
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\TreeService;
26
use Fisharebest\Webtrees\Webtrees;
27
use Illuminate\Database\Capsule\Manager as DB;
28
use Illuminate\Database\Query\Expression;
29
use Illuminate\Database\Query\JoinClause;
30
use Illuminate\Support\Collection;
31
use Intervention\Image\ImageManager;
32
use League\Flysystem\Filesystem;
33
use Psr\Http\Message\ResponseInterface;
34
use Psr\Http\Message\ServerRequestInterface;
35
use Throwable;
36
use function assert;
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
    /** @var PendingChangesService */
47
    private $pending_changes_service;
48
49
    /**
50
     * ImportThumbnailsController constructor.
51
     *
52
     * @param PendingChangesService $pending_changes_service
53
     * @param TreeService           $tree_service
54
     */
55
    public function __construct(PendingChangesService $pending_changes_service, TreeService $tree_service)
56
    {
57
        $this->pending_changes_service = $pending_changes_service;
58
        $this->tree_service            = $tree_service;
59
    }
60
61
    /**
62
     * Import custom thumbnails from webtres 1.x.
63
     *
64
     * @param ServerRequestInterface $request
65
     *
66
     * @return ResponseInterface
67
     */
68
    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

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