Skip to content

Commit 3e9ba2a

Browse files
committed
Import thumbnails from webtrees1
1 parent fb48698 commit 3e9ba2a

File tree

5 files changed

+349
-1
lines changed

5 files changed

+349
-1
lines changed

app/Http/Controllers/AdminController.php

+285-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
namespace Fisharebest\Webtrees\Http\Controllers;
1919

20+
use BigV\ImageCompare;
2021
use DirectoryIterator;
2122
use Fisharebest\Algorithm\MyersDiff;
2223
use Fisharebest\Webtrees\Auth;
@@ -40,12 +41,16 @@
4041
use Fisharebest\Webtrees\Tree;
4142
use Fisharebest\Webtrees\User;
4243
use Fisharebest\Webtrees\View;
43-
use stdClass;
44+
use Intervention\Image\Image;
45+
use Intervention\Image\ImageManager;
46+
use RecursiveDirectoryIterator;
47+
use RecursiveIteratorIterator;
4448
use Symfony\Component\HttpFoundation\JsonResponse;
4549
use Symfony\Component\HttpFoundation\RedirectResponse;
4650
use Symfony\Component\HttpFoundation\Request;
4751
use Symfony\Component\HttpFoundation\Response;
4852
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
53+
use stdClass;
4954

5055
/**
5156
* Controller for the administration pages
@@ -1020,6 +1025,145 @@ public function fixLevel0MediaData(Request $request): JsonResponse {
10201025
]);
10211026
}
10221027

1028+
/**
1029+
* Import custom thumbnails from webtres 1.x.
1030+
*
1031+
* @return Response
1032+
*/
1033+
public function webtrees1Thumbnails(): Response {
1034+
return $this->viewResponse('admin/webtrees1-thumbnails', [
1035+
'title' => I18N::translate('Import custom thumbnails from webtrees version 1'),
1036+
]);
1037+
}
1038+
1039+
/**
1040+
* Import custom thumbnails from webtres 1.x.
1041+
*
1042+
* @param Request $request
1043+
*
1044+
* @return Response
1045+
*/
1046+
public function webtrees1ThumbnailsAction(Request $request): Response {
1047+
$fact_id = $request->get('fact_id');
1048+
$indi_xref = $request->get('indi_xref');
1049+
$obje_xref = $request->get('obje_xref');
1050+
$tree_id = $request->get('tree_id');
1051+
1052+
$tree = Tree::findById($tree_id);
1053+
if ($tree !== null) {
1054+
$individual = Individual::getInstance($indi_xref, $tree);
1055+
$media = Media::getInstance($obje_xref, $tree);
1056+
if ($individual !== null && $media !== null) {
1057+
foreach ($individual->getFacts() as $fact1) {
1058+
if ($fact1->getFactId() === $fact_id) {
1059+
$individual->updateFact($fact_id, $fact1->getGedcom() . "\n2 OBJE @" . $obje_xref . '@', false);
1060+
foreach ($individual->getFacts('OBJE') as $fact2) {
1061+
if ($fact2->getTarget() === $media) {
1062+
$individual->deleteFact($fact2->getFactId(), false);
1063+
}
1064+
}
1065+
break;
1066+
}
1067+
}
1068+
}
1069+
}
1070+
1071+
return new Response;
1072+
}
1073+
1074+
/**
1075+
* Import custom thumbnails from webtres 1.x.
1076+
*
1077+
* @param Request $request
1078+
*
1079+
* @return JsonResponse
1080+
*/
1081+
public function webtrees1ThumbnailsData(Request $request): JsonResponse {
1082+
$start = (int) $request->get('start', 0);
1083+
$length = (int) $request->get('length', 20);
1084+
$search = $request->get('search', []);
1085+
$search = $search['value'] ?? '';
1086+
1087+
// Fetch all thumbnails
1088+
$thumbnails = [];
1089+
1090+
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(WT_DATA_DIR));
1091+
1092+
foreach ($iterator as $iteration) {
1093+
if ($iteration->isFile() && basename(dirname($iteration->getPathname())) === 'thumbs') {
1094+
$thumbnails[] = $iteration->getPathname();
1095+
}
1096+
}
1097+
1098+
$recordsTotal = count($thumbnails);
1099+
1100+
if ($search !== '') {
1101+
$thumbnails = array_filter($thumbnails, function (string $thumbnail) use ($search) {
1102+
return stripos($thumbnail, $search) !== false;
1103+
});
1104+
}
1105+
1106+
$recordsFiltered = count($thumbnails);
1107+
1108+
$thumbnails = array_slice($thumbnails, $start, $length);
1109+
1110+
// Turn each filename into a row for the table
1111+
$data = array_map(function (string $thumbnail) {
1112+
$original = $this->findOriginalFileFromThumbnail($thumbnail);
1113+
1114+
$original_url = route('unused-media-thumbnail', [
1115+
'folder' => dirname($original),
1116+
'file' => basename($original),
1117+
'w' => 100,
1118+
'h' => 100,
1119+
]);
1120+
$thumbnail_url = route('unused-media-thumbnail', [
1121+
'folder' => dirname($thumbnail),
1122+
'file' => basename($thumbnail),
1123+
'w' => 100,
1124+
'h' => 100,
1125+
]);
1126+
1127+
$custom = $this->isCustomWebtrees1Thumbnail($original, $thumbnail);
1128+
1129+
$original_path = substr($original, strlen(WT_DATA_DIR));
1130+
$thumbnail_path = substr($thumbnail, strlen(WT_DATA_DIR));
1131+
1132+
$media = $this->findMediaObjectsForMediaFile($original_path);
1133+
1134+
$media = array_map(function (Media $media) {
1135+
return '<a href="' . e($media->getRawUrl()) . '">' . $media->getFullName() . '</a>';
1136+
}, $media);
1137+
1138+
$media = implode('<br>', $media);
1139+
1140+
if ($custom) {
1141+
$status = I18n::translate('Custom');
1142+
$import = '<a href="#" class="btn btn-primary" onclick="alert(\'@TODO\');">' . I18N::translate('Import') . '</a>';
1143+
$delete = '<a href="#" class="btn btn-secondary" onclick="alert(\'@TODO\');">' . I18N::translate('Delete') . '</a>';
1144+
} else {
1145+
$status = I18n::translate('Default');
1146+
$import = '<a href="#" class="btn btn-secondary" onclick="alert(\'@TODO\');">' . I18N::translate('Import') . '</a>';
1147+
$delete = '<a href="#" class="btn btn-primary" onclick="alert(\'@TODO\');">' . I18N::translate('Delete') . '</a>';
1148+
}
1149+
1150+
1151+
return [
1152+
'<img src="' . e($thumbnail_url) . '" title="' . e($thumbnail_path) . '">',
1153+
'<img src="' . e($original_url) . '" title="' . e($original_path) . '">',
1154+
$media,
1155+
$status . ' ' . $import . ' ' . $delete . ' ' . @$this->imageDiff($original, $thumbnail),
1156+
];
1157+
}, $thumbnails);
1158+
1159+
return new JsonResponse([
1160+
'draw' => (int) $request->get('draw'),
1161+
'recordsTotal' => $recordsTotal,
1162+
'recordsFiltered' => $recordsFiltered,
1163+
'data' => $data,
1164+
]);
1165+
}
1166+
10231167
/**
10241168
* Merge two genealogy records.
10251169
*
@@ -1652,6 +1796,146 @@ private function filesToDelete() {
16521796
return $files_to_delete;
16531797
}
16541798

1799+
/**
1800+
* Find the media object that uses a particular media file.
1801+
*
1802+
* @param string $file
1803+
*
1804+
* @return Media[]
1805+
*/
1806+
private function findMediaObjectsForMediaFile(string $file): array {
1807+
$rows = Database::prepare(
1808+
"SELECT m.*" .
1809+
" FROM `##media` as m" .
1810+
" JOIN `##media_file` USING (m_file, m_id)" .
1811+
" JOIN `##gedcom_setting` ON (m_file = gedcom_id AND setting_name = 'MEDIA_DIRECTORY')" .
1812+
" WHERE CONCAT(setting_value, multimedia_file_refn) = :file"
1813+
)->execute([
1814+
'file' => $file,
1815+
])->fetchAll();
1816+
1817+
$media = [];
1818+
1819+
foreach ($rows as $row) {
1820+
$tree = Tree::findById($row->m_file);
1821+
$media[] = Media::getInstance($row->m_id, $tree, $row->m_gedcom);
1822+
}
1823+
1824+
return array_filter($media);
1825+
}
1826+
1827+
/**
1828+
* Find the original image that corresponds to a (webtrees 1.x) thumbnail file.
1829+
*
1830+
* @param string $thumbnail
1831+
*
1832+
* @return string
1833+
*/
1834+
private function findOriginalFileFromThumbnail(string $thumbnail): string {
1835+
// First option - a file with the same name
1836+
$original = dirname(dirname($thumbnail)) . '/' . basename($thumbnail);
1837+
1838+
// Second option - a .PNG thumbnail for some other image type
1839+
if (substr_compare($original, '.png', -4, 4) === 0) {
1840+
$pattern = substr($original, 0, -3) . '*';
1841+
$matches = glob($pattern);
1842+
if (!empty($matches) && is_file($matches[0])) {
1843+
$original = $matches[0];
1844+
}
1845+
}
1846+
1847+
return $original;
1848+
}
1849+
1850+
/**
1851+
* Compare two images, and return a quantified difference.
1852+
*
1853+
* 0 (different) ... 100 (same)
1854+
*
1855+
* @param $image1
1856+
* @param $image2
1857+
*
1858+
* @return int
1859+
*/
1860+
private function imageDiff($image1, $image2): int {
1861+
$size = 10;
1862+
1863+
// Convert images to 10x10
1864+
try {
1865+
$manager = new ImageManager;
1866+
$image1 = $manager->make($image1)->resize($size, $size);
1867+
$image2 = $manager->make($image2)->resize($size, $size);
1868+
} catch (\Throwable $ex) {
1869+
//var_dump($image1, $image2);
1870+
//throw $ex;
1871+
return -1;
1872+
}
1873+
1874+
$max_difference = 0;
1875+
// Compare each pixel
1876+
for ($x = 0; $x < $size; ++$x) {
1877+
for ($y = 0; $y < $size; ++$y) {
1878+
// Sum the RGB channels to convert to grayscale.
1879+
$pixel1 = $image1->pickColor($x, $y);
1880+
$pixel2 = $image2->pickColor($x, $y);
1881+
$value1 = $pixel1[0] + $pixel1[1] + $pixel1[2];
1882+
$value2 = $pixel2[0] + $pixel2[1] + $pixel2[2];
1883+
1884+
$max_difference = max($max_difference, abs($value1 - $value2));
1885+
}
1886+
}
1887+
1888+
// The maximum difference is 3 x 255 = 765 (black versus white).
1889+
1890+
return 100 - (int) ($max_difference * 100 / 765);
1891+
}
1892+
1893+
/**
1894+
* Does the thumbnail file appear to be custom generated.
1895+
* If yes (true), we should probably import it.
1896+
* If no (false), we should probably delete it.
1897+
*
1898+
* @param string $original
1899+
* @param string $thumbnail
1900+
*
1901+
* @return bool
1902+
*/
1903+
private function isCustomWebtrees1Thumbnail(string $original, string $thumbnail): bool {
1904+
// Original file no longer exists?
1905+
if (!file_exists($original)) {
1906+
return false;
1907+
}
1908+
1909+
$original_attributes = getimagesize($original);
1910+
$thumbnail_attributes = getimagesize($thumbnail);
1911+
1912+
// Not a thumbnail image?
1913+
if ($thumbnail_attributes === false) {
1914+
return false;
1915+
}
1916+
1917+
// Thumbnail of a non-image?
1918+
if ($original_attributes === false) {
1919+
return true;
1920+
}
1921+
1922+
// Different aspect ratio? Use exact same algorithm as webtrees 1.x
1923+
$original_width = $original_attributes[0];
1924+
$original_height = $original_attributes[1];
1925+
$thumbnail_width = $thumbnail_attributes[0];
1926+
$thumbnail_height = $thumbnail_attributes[1];
1927+
$calculated_height = round($original_height * ($thumbnail_width / $original_width));
1928+
1929+
if (abs($calculated_height - $thumbnail_height) > 1) {
1930+
return true;
1931+
}
1932+
1933+
// Do a pixel-by-pixel comparison
1934+
$image_compare = new ImageCompare;
1935+
1936+
return $image_compare->compare($original, $thumbnail) >= 10;
1937+
}
1938+
16551939
/**
16561940
* Look for the latest version of webtrees.
16571941
*

app/Http/Controllers/MediaController.php

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Fisharebest\Webtrees\MediaFile;
2323
use Fisharebest\Webtrees\Site;
2424
use Fisharebest\Webtrees\Tree;
25+
use Intervention\Image\Exception\NotReadableException;
2526
use League\Flysystem\Adapter\Local;
2627
use League\Flysystem\Filesystem;
2728
use League\Glide\Filesystem\FileNotFoundException;
@@ -100,6 +101,8 @@ public function unusedMediaThumbnail(Request $request): Response {
100101
]);
101102
} catch (FileNotFoundException $ex) {
102103
return $this->httpStatusAsImage(Response::HTTP_NOT_FOUND);
104+
} catch (NotReadableException $ex) {
105+
return $this->httpStatusAsImage(Response::HTTP_INTERNAL_SERVER_ERROR);
103106
} catch (ErrorException $ex) {
104107
return $this->httpStatusAsImage(Response::HTTP_INTERNAL_SERVER_ERROR);
105108
}

resources/views/admin/control-panel.php

+6
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,12 @@
436436
<?= I18N::translate('Link media objects to facts and events') ?>
437437
</a>
438438
</li>
439+
<li>
440+
<?= FontAwesome::decorativeIcon('preferences', ['class' => 'fa-li']) ?>
441+
<a href="<?= e(route('admin-webtrees1-thumbs')) ?>">
442+
<?= I18N::translate('Import custom thumbnails from webtrees version 1') ?>
443+
</a>
444+
</li>
439445
</ul>
440446
</div>
441447
</div>

0 commit comments

Comments
 (0)