ImageLoader.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. using ImageMagick;
  2. using PicView.Avalonia.ImageHandling;
  3. using PicView.Avalonia.Input;
  4. using PicView.Avalonia.UI;
  5. using PicView.Avalonia.ViewModels;
  6. using PicView.Core.ArchiveHandling;
  7. using PicView.Core.DebugTools;
  8. using PicView.Core.FileHandling;
  9. using PicView.Core.FileHistory;
  10. using PicView.Core.Gallery;
  11. using PicView.Core.Http;
  12. using PicView.Core.ImageDecoding;
  13. using PicView.Core.Localization;
  14. using PicView.Core.Navigation;
  15. namespace PicView.Avalonia.Navigation;
  16. public static class ImageLoader
  17. {
  18. #region Load Pic From String
  19. /// <summary>
  20. /// Loads a picture from a given string source, which can be a file path, directory path, or URL.
  21. /// </summary>
  22. public static async Task LoadPicFromStringAsync(string source, MainViewModel vm, ImageIterator imageIterator)
  23. {
  24. if (string.IsNullOrWhiteSpace(source) || vm is null)
  25. {
  26. return;
  27. }
  28. MenuManager.CloseMenus(vm);
  29. vm.IsLoading = true;
  30. TitleManager.SetLoadingTitle(vm);
  31. // Starting in new task makes it more responsive and works better
  32. await Task.Run(async () =>
  33. {
  34. var check = ErrorHelper.CheckIfLoadableString(source);
  35. if (check == null)
  36. {
  37. await ErrorHandling.ReloadAsync(vm).ConfigureAwait(false);
  38. vm.IsLoading = false;
  39. ArchiveExtraction.Cleanup();
  40. return;
  41. }
  42. switch (check.Value.Type)
  43. {
  44. case ErrorHelper.LoadAbleFileType.File:
  45. vm.CurrentView = vm.ImageViewer;
  46. await LoadPicFromFile(check.Value.Data, vm, imageIterator).ConfigureAwait(false);
  47. vm.IsLoading = false;
  48. ArchiveExtraction.Cleanup();
  49. return;
  50. case ErrorHelper.LoadAbleFileType.Directory:
  51. vm.CurrentView = vm.ImageViewer;
  52. await LoadPicFromDirectoryAsync(check.Value.Data, vm).ConfigureAwait(false);
  53. vm.IsLoading = false;
  54. ArchiveExtraction.Cleanup();
  55. return;
  56. case ErrorHelper.LoadAbleFileType.Web:
  57. vm.CurrentView = vm.ImageViewer;
  58. await LoadPicFromUrlAsync(check.Value.Data, vm, imageIterator).ConfigureAwait(false);
  59. vm.IsLoading = false;
  60. ArchiveExtraction.Cleanup();
  61. return;
  62. case ErrorHelper.LoadAbleFileType.Base64:
  63. vm.CurrentView = vm.ImageViewer;
  64. await LoadPicFromBase64Async(check.Value.Data, vm, imageIterator).ConfigureAwait(false);
  65. vm.IsLoading = false;
  66. ArchiveExtraction.Cleanup();
  67. return;
  68. case ErrorHelper.LoadAbleFileType.Zip:
  69. vm.CurrentView = vm.ImageViewer;
  70. await LoadPicFromArchiveAsync(check.Value.Data, vm, imageIterator).ConfigureAwait(false);
  71. vm.IsLoading = false;
  72. return;
  73. default:
  74. await ErrorHandling.ReloadAsync(vm).ConfigureAwait(false);
  75. vm.IsLoading = false;
  76. ArchiveExtraction.Cleanup();
  77. return;
  78. }
  79. });
  80. }
  81. #endregion
  82. #region Load Pic From File
  83. /// <summary>
  84. /// Loads an image from a specified file and manages navigation within the directory or recreates the iterator.
  85. /// </summary>
  86. /// <param name="fileName">The full path of the file to load.</param>
  87. /// <param name="vm">The main view model instance associated with the application context.</param>
  88. /// <param name="imageIterator">An iterator for navigating through images in the directory.</param>
  89. /// <param name="fileInfo">Optional file information, defaults to a new <c>FileInfo</c> instance for the given file name if not provided.</param>
  90. public static async Task LoadPicFromFile(string fileName, MainViewModel vm, ImageIterator imageIterator,
  91. FileInfo? fileInfo = null)
  92. {
  93. fileInfo ??= new FileInfo(fileName);
  94. if (!fileInfo.Exists)
  95. {
  96. return;
  97. }
  98. await CancelAsync().ConfigureAwait(false);
  99. if (imageIterator is not null)
  100. {
  101. // If image is in same directory as is being browsed, navigate to it. Otherwise, load without iterator.
  102. if (fileInfo.DirectoryName == imageIterator.InitialFileInfo.DirectoryName)
  103. {
  104. var index = imageIterator.ImagePaths.IndexOf(fileInfo.FullName);
  105. if (index != -1)
  106. {
  107. await imageIterator.IterateToIndex(index, _cancellationTokenSource).ConfigureAwait(false);
  108. await NavigationManager.CheckIfTiffAndUpdate(vm, fileInfo, index);
  109. if (Settings.Gallery.IsBottomGalleryShown && NavigationManager.GetCount > 0)
  110. {
  111. vm.GalleryMode = GalleryMode.ClosedToBottom;
  112. }
  113. }
  114. else
  115. {
  116. await LoadWithoutIterator();
  117. }
  118. }
  119. else
  120. {
  121. await LoadWithoutIterator();
  122. }
  123. }
  124. else
  125. {
  126. await LoadWithoutIterator();
  127. }
  128. return;
  129. async Task LoadWithoutIterator()
  130. {
  131. if (Settings.UIProperties.IsTaskbarProgressEnabled)
  132. {
  133. vm.PlatformService.StopTaskbarProgress();
  134. }
  135. await NavigationManager.LoadWithoutImageIterator(fileInfo, vm).ConfigureAwait(false);
  136. }
  137. }
  138. #endregion
  139. #region Load Pic From Directory
  140. /// <summary>
  141. /// Loads a picture from a directory.
  142. /// </summary>
  143. /// <param name="file">The path to the directory containing the picture.</param>
  144. /// <param name="vm">The main view model instance.</param>
  145. /// <param name="fileInfo">Optional: FileInfo object for the directory.</param>
  146. public static async Task LoadPicFromDirectoryAsync(string file, MainViewModel vm, FileInfo? fileInfo = null)
  147. {
  148. vm.IsLoading = true;
  149. TitleManager.SetLoadingTitle(vm);
  150. if (_cancellationTokenSource is not null)
  151. {
  152. await _cancellationTokenSource.CancelAsync().ConfigureAwait(false);
  153. }
  154. _cancellationTokenSource = new CancellationTokenSource();
  155. if (Settings.UIProperties.IsTaskbarProgressEnabled)
  156. {
  157. vm.PlatformService.StopTaskbarProgress();
  158. }
  159. fileInfo ??= new FileInfo(file);
  160. var newFileList = await Task.Run(() =>
  161. {
  162. var fileList = vm.PlatformService.GetFiles(fileInfo);
  163. if (fileList.Count > 0)
  164. {
  165. return fileList;
  166. }
  167. // Attempt to reload with subdirectories and reset the setting
  168. if (Settings.Sorting.IncludeSubDirectories)
  169. {
  170. return null;
  171. }
  172. Settings.Sorting.IncludeSubDirectories = true;
  173. fileList = vm.PlatformService.GetFiles(fileInfo);
  174. if (fileList.Count <= 0)
  175. {
  176. return null;
  177. }
  178. Settings.Sorting.IncludeSubDirectories = false;
  179. return fileList;
  180. }).ConfigureAwait(false);
  181. if (newFileList is null)
  182. {
  183. await ErrorHandling.ReloadAsync(vm).ConfigureAwait(false);
  184. return;
  185. }
  186. var firstFileInfo = new FileInfo(newFileList[0]);
  187. await NavigationManager.LoadWithoutImageIterator(firstFileInfo, vm, newFileList);
  188. }
  189. #endregion
  190. #region Load Pic From Archive
  191. /// <summary>
  192. /// Asynchronously loads pictures from the specified archive file.
  193. /// </summary>
  194. /// <param name="path">The path to the archive file containing the picture(s) to load.</param>
  195. /// <param name="vm">The main view model instance used to manage UI state and operations.</param>
  196. /// <param name="imageIterator">The image iterator to use for navigation.</param>
  197. public static async Task LoadPicFromArchiveAsync(string path, MainViewModel vm, ImageIterator imageIterator)
  198. {
  199. if (_cancellationTokenSource is not null)
  200. {
  201. await _cancellationTokenSource.CancelAsync().ConfigureAwait(false);
  202. }
  203. vm.IsLoading = true;
  204. TitleManager.SetLoadingTitle(vm);
  205. string? prevArchiveLocation = null;
  206. var previousArchiveExist = !string.IsNullOrEmpty(ArchiveExtraction.TempZipDirectory);
  207. if (previousArchiveExist)
  208. {
  209. prevArchiveLocation = ArchiveExtraction.TempZipDirectory;
  210. }
  211. var extraction = await ArchiveExtraction
  212. .ExtractArchiveAsync(path, vm.PlatformService.ExtractWithLocalSoftwareAsync).ConfigureAwait(false);
  213. if (!extraction)
  214. {
  215. await ErrorHandling.ReloadAsync(vm);
  216. Clean();
  217. return;
  218. }
  219. if (Directory.Exists(ArchiveExtraction.TempZipDirectory))
  220. {
  221. var dirInfo = new DirectoryInfo(ArchiveExtraction.TempZipDirectory);
  222. if (dirInfo.EnumerateDirectories().Any())
  223. {
  224. var firstDir = dirInfo.EnumerateDirectories().First();
  225. var firstFile = firstDir.EnumerateFiles().First();
  226. await LoadPicFromFile(firstFile.FullName, vm, imageIterator, firstFile).ConfigureAwait(false);
  227. }
  228. else
  229. {
  230. await LoadPicFromDirectoryAsync(ArchiveExtraction.TempZipDirectory, vm).ConfigureAwait(false);
  231. }
  232. FileHistoryManager.Add(path);
  233. MainKeyboardShortcuts.ClearKeyDownModifiers(); // Fix possible modifier key state issue
  234. if (previousArchiveExist)
  235. {
  236. try
  237. {
  238. Directory.Delete(prevArchiveLocation, true);
  239. }
  240. catch (Exception e)
  241. {
  242. DebugHelper.LogDebug(nameof(ImageLoader), nameof(LoadPicFromArchiveAsync), e);
  243. }
  244. }
  245. }
  246. else
  247. {
  248. await imageIterator.DisposeAsync();
  249. await ErrorHandling.ReloadAsync(vm);
  250. Clean();
  251. }
  252. return;
  253. void Clean()
  254. {
  255. TempFileHelper.DeleteTempFiles();
  256. ArchiveExtraction.Cleanup();
  257. }
  258. }
  259. #endregion
  260. #region Load Pic From URL
  261. /// <summary>
  262. /// Loads a picture from a given URL.
  263. /// </summary>
  264. /// <param name="url">The URL of the picture to load.</param>
  265. /// <param name="vm">The main view model instance.</param>
  266. /// <param name="imageIterator">The image iterator to use for navigation.</param>
  267. public static async Task LoadPicFromUrlAsync(string url, MainViewModel vm, ImageIterator imageIterator)
  268. {
  269. var tasks = new List<Task>();
  270. if (_cancellationTokenSource is not null)
  271. {
  272. tasks.Add(_cancellationTokenSource.CancelAsync());
  273. }
  274. string destination;
  275. try
  276. {
  277. vm.PlatformService.StopTaskbarProgress();
  278. var httpDownload = HttpManager.GetDownloadClient(url);
  279. using var client = httpDownload.Client;
  280. var fileName = Path.GetFileName(url);
  281. client.ProgressChanged += (totalFileSize, totalBytesDownloaded, progressPercentage) =>
  282. {
  283. if (totalFileSize is null || totalBytesDownloaded is null || progressPercentage is null)
  284. {
  285. return;
  286. }
  287. var displayProgress = HttpManager.GetProgressDisplay(totalFileSize, totalBytesDownloaded,
  288. progressPercentage);
  289. var title = $"{fileName} {TranslationManager.Translation.Downloading} {displayProgress}";
  290. vm.PicViewer.Title = title;
  291. vm.PicViewer.TitleTooltip = title;
  292. vm.PicViewer.WindowTitle = title;
  293. if (Settings.UIProperties.IsTaskbarProgressEnabled)
  294. {
  295. vm.PlatformService.SetTaskbarProgress((ulong)totalBytesDownloaded, (ulong)totalFileSize);
  296. }
  297. };
  298. tasks.Add(client.StartDownloadAsync());
  299. if (imageIterator is not null)
  300. {
  301. tasks.Add(imageIterator.DisposeAsync().AsTask());
  302. }
  303. await Task.WhenAll(tasks).ConfigureAwait(false);
  304. destination = httpDownload.DownloadPath;
  305. }
  306. catch (Exception e)
  307. {
  308. DebugHelper.LogDebug(nameof(ImageLoader), nameof(LoadPicFromUrlAsync), e);
  309. await ErrorHandling.ReloadAsync(vm);
  310. await TooltipHelper.ShowTooltipMessageAsync(e.Message, true);
  311. return;
  312. }
  313. var fileInfo = new FileInfo(destination);
  314. if (!fileInfo.Exists)
  315. {
  316. await ErrorHandling.ReloadAsync(vm);
  317. return;
  318. }
  319. var imageModel = await GetImageModel.GetImageModelAsync(fileInfo).ConfigureAwait(false);
  320. await UpdateImage.SetSingleImageAsync(imageModel.Image, imageModel.ImageType, url, vm);
  321. vm.IsLoading = false;
  322. vm.PicViewer.FileInfo = fileInfo;
  323. vm.PicViewer.ExifOrientation = imageModel.EXIFOrientation;
  324. FileHistoryManager.Add(url);
  325. await NavigationManager.DisposeImageIteratorAsync();
  326. TempFileHelper.TempFilePath = destination;
  327. }
  328. #endregion
  329. #region Load Pic From Base64
  330. /// <summary>
  331. /// Loads a picture from a Base64-encoded string.
  332. /// </summary>
  333. /// <param name="base64">The Base64-encoded string representing the picture.</param>
  334. /// <param name="vm">The main view model instance.</param>
  335. /// <param name="imageIterator">The image iterator to use for navigation.</param>
  336. public static async Task LoadPicFromBase64Async(string base64, MainViewModel vm, ImageIterator imageIterator)
  337. {
  338. TitleManager.SetLoadingTitle(vm);
  339. vm.IsLoading = true;
  340. vm.PicViewer.ImageSource = null;
  341. vm.PicViewer.FileInfo = null;
  342. if (_cancellationTokenSource is not null)
  343. {
  344. await _cancellationTokenSource.CancelAsync().ConfigureAwait(false);
  345. }
  346. await NavigationManager.DisposeImageIteratorAsync().ConfigureAwait(false);
  347. await Task.Run(async () =>
  348. {
  349. try
  350. {
  351. var magickImage = ImageDecoder.Base64ToMagickImage(base64);
  352. magickImage.Format = MagickFormat.Png;
  353. var bitmap = magickImage.ToWriteableBitmap();
  354. var imageModel = new ImageModel
  355. {
  356. Image = bitmap,
  357. PixelWidth = bitmap?.PixelSize.Width ?? 0,
  358. PixelHeight = bitmap?.PixelSize.Height ?? 0,
  359. ImageType = ImageType.Bitmap
  360. };
  361. await UpdateImage.SetSingleImageAsync(imageModel.Image, imageModel.ImageType,
  362. TranslationManager.Translation.Base64Image, vm);
  363. }
  364. catch (Exception e)
  365. {
  366. DebugHelper.LogDebug(nameof(ImageLoader), nameof(LoadPicFromBase64Async), e);
  367. await imageIterator.DisposeAsync();
  368. await ErrorHandling.ReloadAsync(vm);
  369. }
  370. });
  371. vm.IsLoading = false;
  372. }
  373. #endregion
  374. #region Cancellation
  375. private static CancellationTokenSource? _cancellationTokenSource;
  376. public static void Cancel()
  377. {
  378. if (_cancellationTokenSource is not null)
  379. {
  380. _cancellationTokenSource.Cancel();
  381. _cancellationTokenSource.Dispose();
  382. _cancellationTokenSource = null;
  383. }
  384. _cancellationTokenSource = new CancellationTokenSource();
  385. }
  386. public static async Task CancelAsync()
  387. {
  388. if (_cancellationTokenSource is not null)
  389. {
  390. await _cancellationTokenSource.CancelAsync().ConfigureAwait(false);
  391. _cancellationTokenSource.Dispose();
  392. _cancellationTokenSource = null;
  393. }
  394. _cancellationTokenSource = new CancellationTokenSource();
  395. }
  396. #endregion
  397. #region Image Iterator Loading
  398. /// <inheritdoc cref="ImageIterator.NextIteration(NavigateTo, CancellationTokenSource)" />
  399. public static async Task LastIterationAsync(ImageIterator imageIterator) =>
  400. await imageIterator
  401. .NextIteration(NavigateTo.Last, _cancellationTokenSource)
  402. .ConfigureAwait(false);
  403. /// <inheritdoc cref="ImageIterator.NextIteration(NavigateTo, CancellationTokenSource)" />
  404. public static async Task FirstIterationAsync(ImageIterator imageIterator) =>
  405. await imageIterator
  406. .NextIteration(NavigateTo.First, _cancellationTokenSource)
  407. .ConfigureAwait(false);
  408. /// <summary>
  409. /// Checks if the previous iteration has been canceled and starts the iteration at the given index
  410. /// </summary>
  411. /// <param name="index">The index to iterate to.</param>
  412. /// <param name="imageIterator">The ImageIterator instance.</param>
  413. public static async Task CheckCancellationAndStartIterateToIndex(int index, ImageIterator imageIterator)
  414. {
  415. if (_cancellationTokenSource is not null)
  416. {
  417. await _cancellationTokenSource.CancelAsync().ConfigureAwait(false);
  418. }
  419. // Need to start in a new task. This makes it more responsive, since it can get laggy when loading large images
  420. await Task.Run(async () =>
  421. {
  422. _cancellationTokenSource = new CancellationTokenSource();
  423. await imageIterator.NextIteration(index, _cancellationTokenSource).ConfigureAwait(false);
  424. }).ConfigureAwait(false);
  425. }
  426. #endregion
  427. }