|
17 | 17 |
|
18 | 18 | namespace Fisharebest\Webtrees\Http\Controllers;
|
19 | 19 |
|
| 20 | +use BigV\ImageCompare; |
20 | 21 | use DirectoryIterator;
|
21 | 22 | use Fisharebest\Algorithm\MyersDiff;
|
22 | 23 | use Fisharebest\Webtrees\Auth;
|
|
40 | 41 | use Fisharebest\Webtrees\Tree;
|
41 | 42 | use Fisharebest\Webtrees\User;
|
42 | 43 | use Fisharebest\Webtrees\View;
|
43 |
| -use stdClass; |
| 44 | +use Intervention\Image\Image; |
| 45 | +use Intervention\Image\ImageManager; |
| 46 | +use RecursiveDirectoryIterator; |
| 47 | +use RecursiveIteratorIterator; |
44 | 48 | use Symfony\Component\HttpFoundation\JsonResponse;
|
45 | 49 | use Symfony\Component\HttpFoundation\RedirectResponse;
|
46 | 50 | use Symfony\Component\HttpFoundation\Request;
|
47 | 51 | use Symfony\Component\HttpFoundation\Response;
|
48 | 52 | use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
| 53 | +use stdClass; |
49 | 54 |
|
50 | 55 | /**
|
51 | 56 | * Controller for the administration pages
|
@@ -1020,6 +1025,145 @@ public function fixLevel0MediaData(Request $request): JsonResponse {
|
1020 | 1025 | ]);
|
1021 | 1026 | }
|
1022 | 1027 |
|
| 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 | + |
1023 | 1167 | /**
|
1024 | 1168 | * Merge two genealogy records.
|
1025 | 1169 | *
|
@@ -1652,6 +1796,146 @@ private function filesToDelete() {
|
1652 | 1796 | return $files_to_delete;
|
1653 | 1797 | }
|
1654 | 1798 |
|
| 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 | + |
1655 | 1939 | /**
|
1656 | 1940 | * Look for the latest version of webtrees.
|
1657 | 1941 | *
|
|
0 commit comments