ImageProcessor.cs 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990
  1. using MediaBrowser.Common.Extensions;
  2. using MediaBrowser.Controller;
  3. using MediaBrowser.Controller.Drawing;
  4. using MediaBrowser.Controller.Entities;
  5. using MediaBrowser.Controller.Providers;
  6. using MediaBrowser.Model.Drawing;
  7. using MediaBrowser.Model.Entities;
  8. using MediaBrowser.Model.Logging;
  9. using MediaBrowser.Model.Serialization;
  10. using System;
  11. using System.Collections.Concurrent;
  12. using System.Collections.Generic;
  13. using System.Globalization;
  14. using System.IO;
  15. using System.Linq;
  16. using System.Threading;
  17. using System.Threading.Tasks;
  18. using MediaBrowser.Model.IO;
  19. using Emby.Drawing.Common;
  20. using MediaBrowser.Controller.Library;
  21. using MediaBrowser.Controller.MediaEncoding;
  22. using MediaBrowser.Model.Net;
  23. using MediaBrowser.Model.Threading;
  24. using MediaBrowser.Model.Extensions;
  25. namespace Emby.Drawing
  26. {
  27. /// <summary>
  28. /// Class ImageProcessor
  29. /// </summary>
  30. public class ImageProcessor : IImageProcessor, IDisposable
  31. {
  32. /// <summary>
  33. /// The us culture
  34. /// </summary>
  35. protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
  36. /// <summary>
  37. /// The _cached imaged sizes
  38. /// </summary>
  39. private readonly ConcurrentDictionary<Guid, ImageSize> _cachedImagedSizes;
  40. /// <summary>
  41. /// Gets the list of currently registered image processors
  42. /// Image processors are specialized metadata providers that run after the normal ones
  43. /// </summary>
  44. /// <value>The image enhancers.</value>
  45. public IImageEnhancer[] ImageEnhancers { get; private set; }
  46. /// <summary>
  47. /// The _logger
  48. /// </summary>
  49. private readonly ILogger _logger;
  50. private readonly IFileSystem _fileSystem;
  51. private readonly IJsonSerializer _jsonSerializer;
  52. private readonly IServerApplicationPaths _appPaths;
  53. private IImageEncoder _imageEncoder;
  54. private readonly Func<ILibraryManager> _libraryManager;
  55. private readonly Func<IMediaEncoder> _mediaEncoder;
  56. public ImageProcessor(ILogger logger,
  57. IServerApplicationPaths appPaths,
  58. IFileSystem fileSystem,
  59. IJsonSerializer jsonSerializer,
  60. IImageEncoder imageEncoder,
  61. Func<ILibraryManager> libraryManager, ITimerFactory timerFactory, Func<IMediaEncoder> mediaEncoder)
  62. {
  63. _logger = logger;
  64. _fileSystem = fileSystem;
  65. _jsonSerializer = jsonSerializer;
  66. _imageEncoder = imageEncoder;
  67. _libraryManager = libraryManager;
  68. _mediaEncoder = mediaEncoder;
  69. _appPaths = appPaths;
  70. ImageEnhancers = new IImageEnhancer[] { };
  71. _saveImageSizeTimer = timerFactory.Create(SaveImageSizeCallback, null, Timeout.Infinite, Timeout.Infinite);
  72. ImageHelper.ImageProcessor = this;
  73. Dictionary<Guid, ImageSize> sizeDictionary;
  74. try
  75. {
  76. sizeDictionary = jsonSerializer.DeserializeFromFile<Dictionary<Guid, ImageSize>>(ImageSizeFile) ??
  77. new Dictionary<Guid, ImageSize>();
  78. }
  79. catch (FileNotFoundException)
  80. {
  81. // No biggie
  82. sizeDictionary = new Dictionary<Guid, ImageSize>();
  83. }
  84. catch (IOException)
  85. {
  86. // No biggie
  87. sizeDictionary = new Dictionary<Guid, ImageSize>();
  88. }
  89. catch (Exception ex)
  90. {
  91. logger.ErrorException("Error parsing image size cache file", ex);
  92. sizeDictionary = new Dictionary<Guid, ImageSize>();
  93. }
  94. _cachedImagedSizes = new ConcurrentDictionary<Guid, ImageSize>(sizeDictionary);
  95. }
  96. public IImageEncoder ImageEncoder
  97. {
  98. get { return _imageEncoder; }
  99. set
  100. {
  101. if (value == null)
  102. {
  103. throw new ArgumentNullException("value");
  104. }
  105. _imageEncoder = value;
  106. }
  107. }
  108. public string[] SupportedInputFormats
  109. {
  110. get
  111. {
  112. return new string[]
  113. {
  114. "tiff",
  115. "tif",
  116. "jpeg",
  117. "jpg",
  118. "png",
  119. "aiff",
  120. "cr2",
  121. "crw",
  122. "dng",
  123. // Remove until supported
  124. //"nef",
  125. "orf",
  126. "pef",
  127. "arw",
  128. "webp",
  129. "gif",
  130. "bmp",
  131. "erf",
  132. "raf",
  133. "rw2",
  134. "nrw",
  135. "dng",
  136. "ico",
  137. "astc",
  138. "ktx",
  139. "pkm",
  140. "wbmp"
  141. };
  142. }
  143. }
  144. public bool SupportsImageCollageCreation
  145. {
  146. get
  147. {
  148. return _imageEncoder.SupportsImageCollageCreation;
  149. }
  150. }
  151. private string ResizedImageCachePath
  152. {
  153. get
  154. {
  155. return Path.Combine(_appPaths.ImageCachePath, "resized-images");
  156. }
  157. }
  158. private string EnhancedImageCachePath
  159. {
  160. get
  161. {
  162. return Path.Combine(_appPaths.ImageCachePath, "enhanced-images");
  163. }
  164. }
  165. public void AddParts(IEnumerable<IImageEnhancer> enhancers)
  166. {
  167. ImageEnhancers = enhancers.ToArray();
  168. }
  169. public async Task ProcessImage(ImageProcessingOptions options, Stream toStream)
  170. {
  171. var file = await ProcessImage(options).ConfigureAwait(false);
  172. using (var fileStream = _fileSystem.GetFileStream(file.Item1, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read, true))
  173. {
  174. await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
  175. }
  176. }
  177. public ImageFormat[] GetSupportedImageOutputFormats()
  178. {
  179. return _imageEncoder.SupportedOutputFormats;
  180. }
  181. private static readonly string[] TransparentImageTypes = new string[] { ".png", ".webp" };
  182. private bool SupportsTransparency(string path)
  183. {
  184. return TransparentImageTypes.Contains(Path.GetExtension(path) ?? string.Empty);
  185. ;
  186. }
  187. public async Task<Tuple<string, string, DateTime>> ProcessImage(ImageProcessingOptions options)
  188. {
  189. if (options == null)
  190. {
  191. throw new ArgumentNullException("options");
  192. }
  193. var originalImage = options.Image;
  194. IHasMetadata item = options.Item;
  195. if (!originalImage.IsLocalFile)
  196. {
  197. if (item == null)
  198. {
  199. item = _libraryManager().GetItemById(options.ItemId);
  200. }
  201. originalImage = await _libraryManager().ConvertImageToLocal(item, originalImage, options.ImageIndex).ConfigureAwait(false);
  202. }
  203. var originalImagePath = originalImage.Path;
  204. var dateModified = originalImage.DateModified;
  205. if (!_imageEncoder.SupportsImageEncoding)
  206. {
  207. return new Tuple<string, string, DateTime>(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
  208. }
  209. var supportedImageInfo = await GetSupportedImage(originalImagePath, dateModified).ConfigureAwait(false);
  210. originalImagePath = supportedImageInfo.Item1;
  211. dateModified = supportedImageInfo.Item2;
  212. if (options.Enhancers.Count > 0)
  213. {
  214. if (item == null)
  215. {
  216. item = _libraryManager().GetItemById(options.ItemId);
  217. }
  218. var tuple = await GetEnhancedImage(new ItemImageInfo
  219. {
  220. DateModified = dateModified,
  221. Type = originalImage.Type,
  222. Path = originalImagePath
  223. }, item, options.ImageIndex, options.Enhancers).ConfigureAwait(false);
  224. originalImagePath = tuple.Item1;
  225. dateModified = tuple.Item2;
  226. }
  227. var photo = item as Photo;
  228. var autoOrient = false;
  229. ImageOrientation? orientation = null;
  230. if (photo != null && photo.Orientation.HasValue && photo.Orientation.Value != ImageOrientation.TopLeft)
  231. {
  232. autoOrient = true;
  233. orientation = photo.Orientation;
  234. }
  235. if (options.HasDefaultOptions(originalImagePath) && !autoOrient)
  236. {
  237. // Just spit out the original file if all the options are default
  238. return new Tuple<string, string, DateTime>(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
  239. }
  240. ImageSize? originalImageSize = GetSavedImageSize(originalImagePath, dateModified);
  241. if (originalImageSize.HasValue && options.HasDefaultOptions(originalImagePath, originalImageSize.Value) && !autoOrient)
  242. {
  243. // Just spit out the original file if all the options are default
  244. _logger.Info("Returning original image {0}", originalImagePath);
  245. return new Tuple<string, string, DateTime>(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
  246. }
  247. var newSize = ImageHelper.GetNewImageSize(options, originalImageSize);
  248. var quality = options.Quality;
  249. var outputFormat = GetOutputFormat(options.SupportedOutputFormats[0]);
  250. var cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, outputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.Blur, options.BackgroundColor, options.ForegroundLayer);
  251. try
  252. {
  253. CheckDisposed();
  254. if (!_fileSystem.FileExists(cacheFilePath))
  255. {
  256. var tmpPath = Path.ChangeExtension(Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString("N")), Path.GetExtension(cacheFilePath));
  257. _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(tmpPath));
  258. if (options.CropWhiteSpace && !SupportsTransparency(originalImagePath))
  259. {
  260. options.CropWhiteSpace = false;
  261. }
  262. var resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, tmpPath, autoOrient, orientation, quality, options, outputFormat);
  263. if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase))
  264. {
  265. return new Tuple<string, string, DateTime>(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
  266. }
  267. _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(cacheFilePath));
  268. CopyFile(tmpPath, cacheFilePath);
  269. return new Tuple<string, string, DateTime>(tmpPath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(tmpPath));
  270. }
  271. return new Tuple<string, string, DateTime>(cacheFilePath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(cacheFilePath));
  272. }
  273. catch (ArgumentOutOfRangeException ex)
  274. {
  275. // Decoder failed to decode it
  276. #if DEBUG
  277. _logger.ErrorException("Error encoding image", ex);
  278. #endif
  279. // Just spit out the original file if all the options are default
  280. return new Tuple<string, string, DateTime>(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
  281. }
  282. catch (Exception ex)
  283. {
  284. // If it fails for whatever reason, return the original image
  285. _logger.ErrorException("Error encoding image", ex);
  286. // Just spit out the original file if all the options are default
  287. return new Tuple<string, string, DateTime>(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
  288. }
  289. }
  290. private void CopyFile(string src, string destination)
  291. {
  292. try
  293. {
  294. _fileSystem.CopyFile(src, destination, true);
  295. }
  296. catch
  297. {
  298. }
  299. }
  300. //private static int[][] OPERATIONS = new int[][] {
  301. // TopLeft
  302. //new int[] { 0, NONE},
  303. // TopRight
  304. //new int[] { 0, HORIZONTAL},
  305. //new int[] {180, NONE},
  306. // LeftTop
  307. //new int[] { 0, VERTICAL},
  308. //new int[] { 90, HORIZONTAL},
  309. // RightTop
  310. //new int[] { 90, NONE},
  311. //new int[] {-90, HORIZONTAL},
  312. //new int[] {-90, NONE},
  313. //};
  314. private string GetMimeType(ImageFormat format, string path)
  315. {
  316. if (format == ImageFormat.Bmp)
  317. {
  318. return MimeTypes.GetMimeType("i.bmp");
  319. }
  320. if (format == ImageFormat.Gif)
  321. {
  322. return MimeTypes.GetMimeType("i.gif");
  323. }
  324. if (format == ImageFormat.Jpg)
  325. {
  326. return MimeTypes.GetMimeType("i.jpg");
  327. }
  328. if (format == ImageFormat.Png)
  329. {
  330. return MimeTypes.GetMimeType("i.png");
  331. }
  332. if (format == ImageFormat.Webp)
  333. {
  334. return MimeTypes.GetMimeType("i.webp");
  335. }
  336. return MimeTypes.GetMimeType(path);
  337. }
  338. private ImageFormat GetOutputFormat(ImageFormat requestedFormat)
  339. {
  340. if (requestedFormat == ImageFormat.Webp && !_imageEncoder.SupportedOutputFormats.Contains(ImageFormat.Webp))
  341. {
  342. return ImageFormat.Png;
  343. }
  344. return requestedFormat;
  345. }
  346. private Tuple<string, DateTime> GetResult(string path)
  347. {
  348. return new Tuple<string, DateTime>(path, _fileSystem.GetLastWriteTimeUtc(path));
  349. }
  350. /// <summary>
  351. /// Increment this when there's a change requiring caches to be invalidated
  352. /// </summary>
  353. private const string Version = "3";
  354. /// <summary>
  355. /// Gets the cache file path based on a set of parameters
  356. /// </summary>
  357. private string GetCacheFilePath(string originalPath, ImageSize outputSize, int quality, DateTime dateModified, ImageFormat format, bool addPlayedIndicator, double percentPlayed, int? unwatchedCount, int? blur, string backgroundColor, string foregroundLayer)
  358. {
  359. var filename = originalPath;
  360. filename += "width=" + outputSize.Width;
  361. filename += "height=" + outputSize.Height;
  362. filename += "quality=" + quality;
  363. filename += "datemodified=" + dateModified.Ticks;
  364. filename += "f=" + format;
  365. if (addPlayedIndicator)
  366. {
  367. filename += "pl=true";
  368. }
  369. if (percentPlayed > 0)
  370. {
  371. filename += "p=" + percentPlayed;
  372. }
  373. if (unwatchedCount.HasValue)
  374. {
  375. filename += "p=" + unwatchedCount.Value;
  376. }
  377. if (blur.HasValue)
  378. {
  379. filename += "blur=" + blur.Value;
  380. }
  381. if (!string.IsNullOrEmpty(backgroundColor))
  382. {
  383. filename += "b=" + backgroundColor;
  384. }
  385. if (!string.IsNullOrEmpty(foregroundLayer))
  386. {
  387. filename += "fl=" + foregroundLayer;
  388. }
  389. filename += "v=" + Version;
  390. return GetCachePath(ResizedImageCachePath, filename, "." + format.ToString().ToLower());
  391. }
  392. public ImageSize GetImageSize(ItemImageInfo info, bool allowSlowMethods)
  393. {
  394. return GetImageSize(info.Path, info.DateModified, allowSlowMethods);
  395. }
  396. public ImageSize GetImageSize(ItemImageInfo info)
  397. {
  398. return GetImageSize(info.Path, info.DateModified, false);
  399. }
  400. public ImageSize GetImageSize(string path)
  401. {
  402. return GetImageSize(path, _fileSystem.GetLastWriteTimeUtc(path), false);
  403. }
  404. /// <summary>
  405. /// Gets the size of the image.
  406. /// </summary>
  407. /// <param name="path">The path.</param>
  408. /// <param name="imageDateModified">The image date modified.</param>
  409. /// <param name="allowSlowMethod">if set to <c>true</c> [allow slow method].</param>
  410. /// <returns>ImageSize.</returns>
  411. /// <exception cref="System.ArgumentNullException">path</exception>
  412. private ImageSize GetImageSize(string path, DateTime imageDateModified, bool allowSlowMethod)
  413. {
  414. if (string.IsNullOrEmpty(path))
  415. {
  416. throw new ArgumentNullException("path");
  417. }
  418. ImageSize size;
  419. var cacheHash = GetImageSizeKey(path, imageDateModified);
  420. if (!_cachedImagedSizes.TryGetValue(cacheHash, out size))
  421. {
  422. size = GetImageSizeInternal(path, allowSlowMethod);
  423. SaveImageSize(size, cacheHash, false);
  424. }
  425. return size;
  426. }
  427. public void SaveImageSize(string path, DateTime imageDateModified, ImageSize size)
  428. {
  429. var cacheHash = GetImageSizeKey(path, imageDateModified);
  430. SaveImageSize(size, cacheHash, true);
  431. }
  432. private void SaveImageSize(ImageSize size, Guid cacheHash, bool checkExists)
  433. {
  434. if (size.Width <= 0 || size.Height <= 0)
  435. {
  436. return;
  437. }
  438. if (checkExists && _cachedImagedSizes.ContainsKey(cacheHash))
  439. {
  440. return;
  441. }
  442. if (checkExists)
  443. {
  444. if (_cachedImagedSizes.TryAdd(cacheHash, size))
  445. {
  446. StartSaveImageSizeTimer();
  447. }
  448. }
  449. else
  450. {
  451. StartSaveImageSizeTimer();
  452. _cachedImagedSizes.AddOrUpdate(cacheHash, size, (keyName, oldValue) => size);
  453. }
  454. }
  455. private Guid GetImageSizeKey(string path, DateTime imageDateModified)
  456. {
  457. var name = path + "datemodified=" + imageDateModified.Ticks;
  458. return name.GetMD5();
  459. }
  460. public ImageSize? GetSavedImageSize(string path, DateTime imageDateModified)
  461. {
  462. ImageSize size;
  463. var cacheHash = GetImageSizeKey(path, imageDateModified);
  464. if (_cachedImagedSizes.TryGetValue(cacheHash, out size))
  465. {
  466. return size;
  467. }
  468. return null;
  469. }
  470. /// <summary>
  471. /// Gets the image size internal.
  472. /// </summary>
  473. /// <param name="path">The path.</param>
  474. /// <param name="allowSlowMethod">if set to <c>true</c> [allow slow method].</param>
  475. /// <returns>ImageSize.</returns>
  476. private ImageSize GetImageSizeInternal(string path, bool allowSlowMethod)
  477. {
  478. //try
  479. //{
  480. // using (var fileStream = _fileSystem.OpenRead(path))
  481. // {
  482. // using (var file = TagLib.File.Create(new StreamFileAbstraction(Path.GetFileName(path), fileStream, null)))
  483. // {
  484. // var image = file as TagLib.Image.File;
  485. // if (image != null)
  486. // {
  487. // var properties = image.Properties;
  488. // return new ImageSize
  489. // {
  490. // Height = properties.PhotoHeight,
  491. // Width = properties.PhotoWidth
  492. // };
  493. // }
  494. // }
  495. // }
  496. //}
  497. //catch
  498. //{
  499. //}
  500. try
  501. {
  502. return ImageHeader.GetDimensions(path, _logger, _fileSystem);
  503. }
  504. catch
  505. {
  506. if (allowSlowMethod)
  507. {
  508. return _imageEncoder.GetImageSize(path);
  509. }
  510. throw;
  511. }
  512. }
  513. private readonly ITimer _saveImageSizeTimer;
  514. private const int SaveImageSizeTimeout = 5000;
  515. private readonly object _saveImageSizeLock = new object();
  516. private void StartSaveImageSizeTimer()
  517. {
  518. _saveImageSizeTimer.Change(SaveImageSizeTimeout, Timeout.Infinite);
  519. }
  520. private void SaveImageSizeCallback(object state)
  521. {
  522. lock (_saveImageSizeLock)
  523. {
  524. try
  525. {
  526. var path = ImageSizeFile;
  527. _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path));
  528. _jsonSerializer.SerializeToFile(_cachedImagedSizes, path);
  529. }
  530. catch (Exception ex)
  531. {
  532. _logger.ErrorException("Error saving image size file", ex);
  533. }
  534. }
  535. }
  536. private string ImageSizeFile
  537. {
  538. get
  539. {
  540. return Path.Combine(_appPaths.DataPath, "imagesizes.json");
  541. }
  542. }
  543. /// <summary>
  544. /// Gets the image cache tag.
  545. /// </summary>
  546. /// <param name="item">The item.</param>
  547. /// <param name="image">The image.</param>
  548. /// <returns>Guid.</returns>
  549. /// <exception cref="System.ArgumentNullException">item</exception>
  550. public string GetImageCacheTag(IHasMetadata item, ItemImageInfo image)
  551. {
  552. if (item == null)
  553. {
  554. throw new ArgumentNullException("item");
  555. }
  556. if (image == null)
  557. {
  558. throw new ArgumentNullException("image");
  559. }
  560. var supportedEnhancers = GetSupportedEnhancers(item, image.Type);
  561. return GetImageCacheTag(item, image, supportedEnhancers);
  562. }
  563. /// <summary>
  564. /// Gets the image cache tag.
  565. /// </summary>
  566. /// <param name="item">The item.</param>
  567. /// <param name="image">The image.</param>
  568. /// <param name="imageEnhancers">The image enhancers.</param>
  569. /// <returns>Guid.</returns>
  570. /// <exception cref="System.ArgumentNullException">item</exception>
  571. public string GetImageCacheTag(IHasMetadata item, ItemImageInfo image, List<IImageEnhancer> imageEnhancers)
  572. {
  573. if (item == null)
  574. {
  575. throw new ArgumentNullException("item");
  576. }
  577. if (imageEnhancers == null)
  578. {
  579. throw new ArgumentNullException("imageEnhancers");
  580. }
  581. if (image == null)
  582. {
  583. throw new ArgumentNullException("image");
  584. }
  585. var originalImagePath = image.Path;
  586. var dateModified = image.DateModified;
  587. var imageType = image.Type;
  588. // Optimization
  589. if (imageEnhancers.Count == 0)
  590. {
  591. return (originalImagePath + dateModified.Ticks).GetMD5().ToString("N");
  592. }
  593. // Cache name is created with supported enhancers combined with the last config change so we pick up new config changes
  594. var cacheKeys = imageEnhancers.Select(i => i.GetConfigurationCacheKey(item, imageType)).ToList();
  595. cacheKeys.Add(originalImagePath + dateModified.Ticks);
  596. return string.Join("|", cacheKeys.ToArray(cacheKeys.Count)).GetMD5().ToString("N");
  597. }
  598. private async Task<Tuple<string, DateTime>> GetSupportedImage(string originalImagePath, DateTime dateModified)
  599. {
  600. var inputFormat = (Path.GetExtension(originalImagePath) ?? string.Empty)
  601. .TrimStart('.')
  602. .Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase);
  603. if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat, StringComparer.OrdinalIgnoreCase))
  604. {
  605. try
  606. {
  607. var filename = (originalImagePath + dateModified.Ticks.ToString(UsCulture)).GetMD5().ToString("N");
  608. var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + ".webp");
  609. var file = _fileSystem.GetFileInfo(outputPath);
  610. if (!file.Exists)
  611. {
  612. await _mediaEncoder().ConvertImage(originalImagePath, outputPath).ConfigureAwait(false);
  613. dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath);
  614. }
  615. else
  616. {
  617. dateModified = file.LastWriteTimeUtc;
  618. }
  619. originalImagePath = outputPath;
  620. }
  621. catch (Exception ex)
  622. {
  623. _logger.ErrorException("Image conversion failed for {0}", ex, originalImagePath);
  624. }
  625. }
  626. return new Tuple<string, DateTime>(originalImagePath, dateModified);
  627. }
  628. /// <summary>
  629. /// Gets the enhanced image.
  630. /// </summary>
  631. /// <param name="item">The item.</param>
  632. /// <param name="imageType">Type of the image.</param>
  633. /// <param name="imageIndex">Index of the image.</param>
  634. /// <returns>Task{System.String}.</returns>
  635. public async Task<string> GetEnhancedImage(IHasMetadata item, ImageType imageType, int imageIndex)
  636. {
  637. var enhancers = GetSupportedEnhancers(item, imageType);
  638. var imageInfo = item.GetImageInfo(imageType, imageIndex);
  639. var result = await GetEnhancedImage(imageInfo, item, imageIndex, enhancers);
  640. return result.Item1;
  641. }
  642. private async Task<Tuple<string, DateTime>> GetEnhancedImage(ItemImageInfo image,
  643. IHasMetadata item,
  644. int imageIndex,
  645. List<IImageEnhancer> enhancers)
  646. {
  647. var originalImagePath = image.Path;
  648. var dateModified = image.DateModified;
  649. var imageType = image.Type;
  650. try
  651. {
  652. var cacheGuid = GetImageCacheTag(item, image, enhancers);
  653. // Enhance if we have enhancers
  654. var ehnancedImagePath = await GetEnhancedImageInternal(originalImagePath, item, imageType, imageIndex, enhancers, cacheGuid).ConfigureAwait(false);
  655. // If the path changed update dateModified
  656. if (!string.Equals(ehnancedImagePath, originalImagePath, StringComparison.OrdinalIgnoreCase))
  657. {
  658. return GetResult(ehnancedImagePath);
  659. }
  660. }
  661. catch (Exception ex)
  662. {
  663. _logger.Error("Error enhancing image", ex);
  664. }
  665. return new Tuple<string, DateTime>(originalImagePath, dateModified);
  666. }
  667. /// <summary>
  668. /// Gets the enhanced image internal.
  669. /// </summary>
  670. /// <param name="originalImagePath">The original image path.</param>
  671. /// <param name="item">The item.</param>
  672. /// <param name="imageType">Type of the image.</param>
  673. /// <param name="imageIndex">Index of the image.</param>
  674. /// <param name="supportedEnhancers">The supported enhancers.</param>
  675. /// <param name="cacheGuid">The cache unique identifier.</param>
  676. /// <returns>Task&lt;System.String&gt;.</returns>
  677. /// <exception cref="ArgumentNullException">
  678. /// originalImagePath
  679. /// or
  680. /// item
  681. /// </exception>
  682. private async Task<string> GetEnhancedImageInternal(string originalImagePath,
  683. IHasMetadata item,
  684. ImageType imageType,
  685. int imageIndex,
  686. IEnumerable<IImageEnhancer> supportedEnhancers,
  687. string cacheGuid)
  688. {
  689. if (string.IsNullOrEmpty(originalImagePath))
  690. {
  691. throw new ArgumentNullException("originalImagePath");
  692. }
  693. if (item == null)
  694. {
  695. throw new ArgumentNullException("item");
  696. }
  697. // All enhanced images are saved as png to allow transparency
  698. var enhancedImagePath = GetCachePath(EnhancedImageCachePath, cacheGuid + ".png");
  699. // Check again in case of contention
  700. if (_fileSystem.FileExists(enhancedImagePath))
  701. {
  702. return enhancedImagePath;
  703. }
  704. _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(enhancedImagePath));
  705. var tmpPath = Path.Combine(_appPaths.TempDirectory, Path.ChangeExtension(Guid.NewGuid().ToString(), Path.GetExtension(enhancedImagePath)));
  706. _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(tmpPath));
  707. await ExecuteImageEnhancers(supportedEnhancers, originalImagePath, tmpPath, item, imageType, imageIndex).ConfigureAwait(false);
  708. try
  709. {
  710. _fileSystem.CopyFile(tmpPath, enhancedImagePath, true);
  711. }
  712. catch
  713. {
  714. }
  715. return tmpPath;
  716. }
  717. /// <summary>
  718. /// Executes the image enhancers.
  719. /// </summary>
  720. /// <param name="imageEnhancers">The image enhancers.</param>
  721. /// <param name="inputPath">The input path.</param>
  722. /// <param name="outputPath">The output path.</param>
  723. /// <param name="item">The item.</param>
  724. /// <param name="imageType">Type of the image.</param>
  725. /// <param name="imageIndex">Index of the image.</param>
  726. /// <returns>Task{EnhancedImage}.</returns>
  727. private async Task ExecuteImageEnhancers(IEnumerable<IImageEnhancer> imageEnhancers, string inputPath, string outputPath, IHasMetadata item, ImageType imageType, int imageIndex)
  728. {
  729. // Run the enhancers sequentially in order of priority
  730. foreach (var enhancer in imageEnhancers)
  731. {
  732. await enhancer.EnhanceImageAsync(item, inputPath, outputPath, imageType, imageIndex).ConfigureAwait(false);
  733. // Feed the output into the next enhancer as input
  734. inputPath = outputPath;
  735. }
  736. }
  737. /// <summary>
  738. /// Gets the cache path.
  739. /// </summary>
  740. /// <param name="path">The path.</param>
  741. /// <param name="uniqueName">Name of the unique.</param>
  742. /// <param name="fileExtension">The file extension.</param>
  743. /// <returns>System.String.</returns>
  744. /// <exception cref="System.ArgumentNullException">
  745. /// path
  746. /// or
  747. /// uniqueName
  748. /// or
  749. /// fileExtension
  750. /// </exception>
  751. public string GetCachePath(string path, string uniqueName, string fileExtension)
  752. {
  753. if (string.IsNullOrEmpty(path))
  754. {
  755. throw new ArgumentNullException("path");
  756. }
  757. if (string.IsNullOrEmpty(uniqueName))
  758. {
  759. throw new ArgumentNullException("uniqueName");
  760. }
  761. if (string.IsNullOrEmpty(fileExtension))
  762. {
  763. throw new ArgumentNullException("fileExtension");
  764. }
  765. var filename = uniqueName.GetMD5() + fileExtension;
  766. return GetCachePath(path, filename);
  767. }
  768. /// <summary>
  769. /// Gets the cache path.
  770. /// </summary>
  771. /// <param name="path">The path.</param>
  772. /// <param name="filename">The filename.</param>
  773. /// <returns>System.String.</returns>
  774. /// <exception cref="System.ArgumentNullException">
  775. /// path
  776. /// or
  777. /// filename
  778. /// </exception>
  779. public string GetCachePath(string path, string filename)
  780. {
  781. if (string.IsNullOrEmpty(path))
  782. {
  783. throw new ArgumentNullException("path");
  784. }
  785. if (string.IsNullOrEmpty(filename))
  786. {
  787. throw new ArgumentNullException("filename");
  788. }
  789. var prefix = filename.Substring(0, 1);
  790. path = Path.Combine(path, prefix);
  791. return Path.Combine(path, filename);
  792. }
  793. public void CreateImageCollage(ImageCollageOptions options)
  794. {
  795. _logger.Info("Creating image collage and saving to {0}", options.OutputPath);
  796. _imageEncoder.CreateImageCollage(options);
  797. _logger.Info("Completed creation of image collage and saved to {0}", options.OutputPath);
  798. }
  799. public List<IImageEnhancer> GetSupportedEnhancers(IHasMetadata item, ImageType imageType)
  800. {
  801. var list = new List<IImageEnhancer>();
  802. foreach (var i in ImageEnhancers)
  803. {
  804. try
  805. {
  806. if (i.Supports(item, imageType))
  807. {
  808. list.Add(i);
  809. }
  810. }
  811. catch (Exception ex)
  812. {
  813. _logger.ErrorException("Error in image enhancer: {0}", ex, i.GetType().Name);
  814. }
  815. }
  816. return list;
  817. }
  818. private bool _disposed;
  819. public void Dispose()
  820. {
  821. _disposed = true;
  822. var disposable = _imageEncoder as IDisposable;
  823. if (disposable != null)
  824. {
  825. disposable.Dispose();
  826. }
  827. _saveImageSizeTimer.Dispose();
  828. GC.SuppressFinalize(this);
  829. }
  830. private void CheckDisposed()
  831. {
  832. if (_disposed)
  833. {
  834. throw new ObjectDisposedException(GetType().Name);
  835. }
  836. }
  837. }
  838. }