ImageProcessor.cs 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870
  1. using MediaBrowser.Common.Extensions;
  2. using MediaBrowser.Common.IO;
  3. using MediaBrowser.Controller;
  4. using MediaBrowser.Controller.Drawing;
  5. using MediaBrowser.Controller.Entities;
  6. using MediaBrowser.Controller.IO;
  7. using MediaBrowser.Controller.Providers;
  8. using MediaBrowser.Model.Drawing;
  9. using MediaBrowser.Model.Entities;
  10. using MediaBrowser.Model.Logging;
  11. using System;
  12. using System.Collections.Concurrent;
  13. using System.Collections.Generic;
  14. using System.Drawing;
  15. using System.Drawing.Drawing2D;
  16. using System.Drawing.Imaging;
  17. using System.Globalization;
  18. using System.IO;
  19. using System.Linq;
  20. using System.Threading;
  21. using System.Threading.Tasks;
  22. namespace MediaBrowser.Server.Implementations.Drawing
  23. {
  24. /// <summary>
  25. /// Class ImageProcessor
  26. /// </summary>
  27. public class ImageProcessor : IImageProcessor
  28. {
  29. /// <summary>
  30. /// The us culture
  31. /// </summary>
  32. protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
  33. /// <summary>
  34. /// The _cached imaged sizes
  35. /// </summary>
  36. private readonly ConcurrentDictionary<string, ImageSize> _cachedImagedSizes = new ConcurrentDictionary<string, ImageSize>();
  37. /// <summary>
  38. /// Gets the list of currently registered image processors
  39. /// Image processors are specialized metadata providers that run after the normal ones
  40. /// </summary>
  41. /// <value>The image enhancers.</value>
  42. public IEnumerable<IImageEnhancer> ImageEnhancers { get; private set; }
  43. /// <summary>
  44. /// The _logger
  45. /// </summary>
  46. private readonly ILogger _logger;
  47. /// <summary>
  48. /// The _app paths
  49. /// </summary>
  50. private readonly IServerApplicationPaths _appPaths;
  51. private readonly IFileSystem _fileSystem;
  52. private readonly string _imageSizeCachePath;
  53. private readonly string _croppedWhitespaceImageCachePath;
  54. private readonly string _enhancedImageCachePath;
  55. private readonly string _resizedImageCachePath;
  56. public ImageProcessor(ILogger logger, IServerApplicationPaths appPaths, IFileSystem fileSystem)
  57. {
  58. _logger = logger;
  59. _appPaths = appPaths;
  60. _fileSystem = fileSystem;
  61. _imageSizeCachePath = Path.Combine(_appPaths.ImageCachePath, "image-sizes");
  62. _croppedWhitespaceImageCachePath = Path.Combine(_appPaths.ImageCachePath, "cropped-images");
  63. _enhancedImageCachePath = Path.Combine(_appPaths.ImageCachePath, "enhanced-images");
  64. _resizedImageCachePath = Path.Combine(_appPaths.ImageCachePath, "resized-images");
  65. }
  66. public void AddParts(IEnumerable<IImageEnhancer> enhancers)
  67. {
  68. ImageEnhancers = enhancers.ToArray();
  69. }
  70. public async Task ProcessImage(ImageProcessingOptions options, Stream toStream)
  71. {
  72. if (options == null)
  73. {
  74. throw new ArgumentNullException("options");
  75. }
  76. if (toStream == null)
  77. {
  78. throw new ArgumentNullException("toStream");
  79. }
  80. var originalImagePath = options.OriginalImagePath;
  81. var dateModified = options.OriginalImageDateModified;
  82. if (options.CropWhiteSpace)
  83. {
  84. originalImagePath = await GetWhitespaceCroppedImage(originalImagePath, dateModified).ConfigureAwait(false);
  85. }
  86. // No enhancement - don't cache
  87. if (options.Enhancers.Count > 0)
  88. {
  89. var tuple = await GetEnhancedImage(originalImagePath, dateModified, options.Item, options.ImageType, options.ImageIndex, options.Enhancers).ConfigureAwait(false);
  90. originalImagePath = tuple.Item1;
  91. dateModified = tuple.Item2;
  92. }
  93. var originalImageSize = GetImageSize(originalImagePath, dateModified);
  94. // Determine the output size based on incoming parameters
  95. var newSize = DrawingUtils.Resize(originalImageSize, options.Width, options.Height, options.MaxWidth, options.MaxHeight);
  96. var quality = options.Quality ?? 90;
  97. var cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, options.OutputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.BackgroundColor);
  98. try
  99. {
  100. using (var fileStream = _fileSystem.GetFileStream(cacheFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, true))
  101. {
  102. await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
  103. return;
  104. }
  105. }
  106. catch (IOException)
  107. {
  108. // Cache file doesn't exist or is currently being written to
  109. }
  110. var semaphore = GetLock(cacheFilePath);
  111. await semaphore.WaitAsync().ConfigureAwait(false);
  112. // Check again in case of lock contention
  113. try
  114. {
  115. using (var fileStream = _fileSystem.GetFileStream(cacheFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, true))
  116. {
  117. await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
  118. semaphore.Release();
  119. return;
  120. }
  121. }
  122. catch (IOException)
  123. {
  124. // Cache file doesn't exist or is currently being written to
  125. }
  126. catch
  127. {
  128. semaphore.Release();
  129. throw;
  130. }
  131. try
  132. {
  133. using (var fileStream = _fileSystem.GetFileStream(originalImagePath, FileMode.Open, FileAccess.Read, FileShare.Read, true))
  134. {
  135. // Copy to memory stream to avoid Image locking file
  136. using (var memoryStream = new MemoryStream())
  137. {
  138. await fileStream.CopyToAsync(memoryStream).ConfigureAwait(false);
  139. using (var originalImage = Image.FromStream(memoryStream, true, false))
  140. {
  141. var newWidth = Convert.ToInt32(newSize.Width);
  142. var newHeight = Convert.ToInt32(newSize.Height);
  143. // Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here
  144. using (var thumbnail = new Bitmap(newWidth, newHeight, PixelFormat.Format32bppPArgb))
  145. {
  146. // Preserve the original resolution
  147. thumbnail.SetResolution(originalImage.HorizontalResolution, originalImage.VerticalResolution);
  148. using (var thumbnailGraph = Graphics.FromImage(thumbnail))
  149. {
  150. thumbnailGraph.CompositingQuality = CompositingQuality.HighQuality;
  151. thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality;
  152. thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic;
  153. thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality;
  154. thumbnailGraph.CompositingMode = string.IsNullOrEmpty(options.BackgroundColor) && !options.PercentPlayed.HasValue && !options.AddPlayedIndicator ? CompositingMode.SourceCopy : CompositingMode.SourceOver;
  155. SetBackgroundColor(thumbnailGraph, options);
  156. thumbnailGraph.DrawImage(originalImage, 0, 0, newWidth, newHeight);
  157. DrawIndicator(thumbnailGraph, newWidth, newHeight, options);
  158. var outputFormat = GetOutputFormat(originalImage, options.OutputFormat);
  159. using (var outputMemoryStream = new MemoryStream())
  160. {
  161. // Save to the memory stream
  162. thumbnail.Save(outputFormat, outputMemoryStream, quality);
  163. var bytes = outputMemoryStream.ToArray();
  164. await toStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
  165. // kick off a task to cache the result
  166. CacheResizedImage(cacheFilePath, bytes, semaphore);
  167. }
  168. }
  169. }
  170. }
  171. }
  172. }
  173. }
  174. catch
  175. {
  176. semaphore.Release();
  177. throw;
  178. }
  179. }
  180. /// <summary>
  181. /// Caches the resized image.
  182. /// </summary>
  183. /// <param name="cacheFilePath">The cache file path.</param>
  184. /// <param name="bytes">The bytes.</param>
  185. /// <param name="semaphore">The semaphore.</param>
  186. private void CacheResizedImage(string cacheFilePath, byte[] bytes, SemaphoreSlim semaphore)
  187. {
  188. Task.Run(async () =>
  189. {
  190. try
  191. {
  192. var parentPath = Path.GetDirectoryName(cacheFilePath);
  193. Directory.CreateDirectory(parentPath);
  194. // Save to the cache location
  195. using (var cacheFileStream = _fileSystem.GetFileStream(cacheFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, true))
  196. {
  197. // Save to the filestream
  198. await cacheFileStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
  199. }
  200. }
  201. catch (Exception ex)
  202. {
  203. _logger.ErrorException("Error writing to image cache file {0}", ex, cacheFilePath);
  204. }
  205. finally
  206. {
  207. semaphore.Release();
  208. }
  209. });
  210. }
  211. /// <summary>
  212. /// Sets the color of the background.
  213. /// </summary>
  214. /// <param name="graphics">The graphics.</param>
  215. /// <param name="options">The options.</param>
  216. private void SetBackgroundColor(Graphics graphics, ImageProcessingOptions options)
  217. {
  218. var color = options.BackgroundColor;
  219. if (!string.IsNullOrEmpty(color))
  220. {
  221. Color drawingColor;
  222. try
  223. {
  224. drawingColor = ColorTranslator.FromHtml(color);
  225. }
  226. catch
  227. {
  228. drawingColor = ColorTranslator.FromHtml("#" + color);
  229. }
  230. graphics.Clear(drawingColor);
  231. }
  232. }
  233. /// <summary>
  234. /// Draws the indicator.
  235. /// </summary>
  236. /// <param name="graphics">The graphics.</param>
  237. /// <param name="imageWidth">Width of the image.</param>
  238. /// <param name="imageHeight">Height of the image.</param>
  239. /// <param name="options">The options.</param>
  240. private void DrawIndicator(Graphics graphics, int imageWidth, int imageHeight, ImageProcessingOptions options)
  241. {
  242. if (!options.AddPlayedIndicator && !options.PercentPlayed.HasValue)
  243. {
  244. return;
  245. }
  246. try
  247. {
  248. var percentOffset = 0;
  249. if (options.AddPlayedIndicator)
  250. {
  251. var currentImageSize = new Size(imageWidth, imageHeight);
  252. new WatchedIndicatorDrawer().Process(graphics, currentImageSize);
  253. percentOffset = 0 - WatchedIndicatorDrawer.IndicatorWidth;
  254. }
  255. if (options.PercentPlayed.HasValue)
  256. {
  257. var currentImageSize = new Size(imageWidth, imageHeight);
  258. new PercentPlayedDrawer().Process(graphics, currentImageSize, options.PercentPlayed.Value, percentOffset);
  259. }
  260. }
  261. catch (Exception ex)
  262. {
  263. _logger.ErrorException("Error drawing indicator overlay", ex);
  264. }
  265. }
  266. /// <summary>
  267. /// Gets the output format.
  268. /// </summary>
  269. /// <param name="image">The image.</param>
  270. /// <param name="outputFormat">The output format.</param>
  271. /// <returns>ImageFormat.</returns>
  272. private ImageFormat GetOutputFormat(Image image, ImageOutputFormat outputFormat)
  273. {
  274. switch (outputFormat)
  275. {
  276. case ImageOutputFormat.Bmp:
  277. return ImageFormat.Bmp;
  278. case ImageOutputFormat.Gif:
  279. return ImageFormat.Gif;
  280. case ImageOutputFormat.Jpg:
  281. return ImageFormat.Jpeg;
  282. case ImageOutputFormat.Png:
  283. return ImageFormat.Png;
  284. default:
  285. return image.RawFormat;
  286. }
  287. }
  288. /// <summary>
  289. /// Crops whitespace from an image, caches the result, and returns the cached path
  290. /// </summary>
  291. /// <param name="originalImagePath">The original image path.</param>
  292. /// <param name="dateModified">The date modified.</param>
  293. /// <returns>System.String.</returns>
  294. private async Task<string> GetWhitespaceCroppedImage(string originalImagePath, DateTime dateModified)
  295. {
  296. var name = originalImagePath;
  297. name += "datemodified=" + dateModified.Ticks;
  298. var croppedImagePath = GetCachePath(_croppedWhitespaceImageCachePath, name, Path.GetExtension(originalImagePath));
  299. var semaphore = GetLock(croppedImagePath);
  300. await semaphore.WaitAsync().ConfigureAwait(false);
  301. // Check again in case of contention
  302. if (File.Exists(croppedImagePath))
  303. {
  304. semaphore.Release();
  305. return croppedImagePath;
  306. }
  307. try
  308. {
  309. using (var fileStream = _fileSystem.GetFileStream(originalImagePath, FileMode.Open, FileAccess.Read, FileShare.Read, true))
  310. {
  311. // Copy to memory stream to avoid Image locking file
  312. using (var memoryStream = new MemoryStream())
  313. {
  314. await fileStream.CopyToAsync(memoryStream).ConfigureAwait(false);
  315. using (var originalImage = (Bitmap)Image.FromStream(memoryStream, true, false))
  316. {
  317. var outputFormat = originalImage.RawFormat;
  318. using (var croppedImage = originalImage.CropWhitespace())
  319. {
  320. var parentPath = Path.GetDirectoryName(croppedImagePath);
  321. Directory.CreateDirectory(parentPath);
  322. using (var outputStream = _fileSystem.GetFileStream(croppedImagePath, FileMode.Create, FileAccess.Write, FileShare.Read, false))
  323. {
  324. croppedImage.Save(outputFormat, outputStream, 100);
  325. }
  326. }
  327. }
  328. }
  329. }
  330. }
  331. catch (Exception ex)
  332. {
  333. // We have to have a catch-all here because some of the .net image methods throw a plain old Exception
  334. _logger.ErrorException("Error cropping image {0}", ex, originalImagePath);
  335. return originalImagePath;
  336. }
  337. finally
  338. {
  339. semaphore.Release();
  340. }
  341. return croppedImagePath;
  342. }
  343. /// <summary>
  344. /// Gets the cache file path based on a set of parameters
  345. /// </summary>
  346. private string GetCacheFilePath(string originalPath, ImageSize outputSize, int quality, DateTime dateModified, ImageOutputFormat format, bool addPlayedIndicator, int? percentPlayed, string backgroundColor)
  347. {
  348. var filename = originalPath;
  349. filename += "width=" + outputSize.Width;
  350. filename += "height=" + outputSize.Height;
  351. filename += "quality=" + quality;
  352. filename += "datemodified=" + dateModified.Ticks;
  353. if (format != ImageOutputFormat.Original)
  354. {
  355. filename += "f=" + format;
  356. }
  357. if (addPlayedIndicator)
  358. {
  359. filename += "pl=true";
  360. }
  361. if (percentPlayed.HasValue)
  362. {
  363. filename += "p=" + percentPlayed.Value;
  364. }
  365. if (!string.IsNullOrEmpty(backgroundColor))
  366. {
  367. filename += "b=" + backgroundColor;
  368. }
  369. return GetCachePath(_resizedImageCachePath, filename, Path.GetExtension(originalPath));
  370. }
  371. /// <summary>
  372. /// Gets the size of the image.
  373. /// </summary>
  374. /// <param name="path">The path.</param>
  375. /// <returns>ImageSize.</returns>
  376. public ImageSize GetImageSize(string path)
  377. {
  378. return GetImageSize(path, File.GetLastWriteTimeUtc(path));
  379. }
  380. /// <summary>
  381. /// Gets the size of the image.
  382. /// </summary>
  383. /// <param name="path">The path.</param>
  384. /// <param name="imageDateModified">The image date modified.</param>
  385. /// <returns>ImageSize.</returns>
  386. /// <exception cref="System.ArgumentNullException">path</exception>
  387. public ImageSize GetImageSize(string path, DateTime imageDateModified)
  388. {
  389. if (string.IsNullOrEmpty(path))
  390. {
  391. throw new ArgumentNullException("path");
  392. }
  393. var name = path + "datemodified=" + imageDateModified.Ticks;
  394. ImageSize size;
  395. if (!_cachedImagedSizes.TryGetValue(name, out size))
  396. {
  397. size = GetImageSizeInternal(name, path);
  398. _cachedImagedSizes.AddOrUpdate(name, size, (keyName, oldValue) => size);
  399. }
  400. return size;
  401. }
  402. /// <summary>
  403. /// Gets the image size internal.
  404. /// </summary>
  405. /// <param name="cacheKey">The cache key.</param>
  406. /// <param name="path">The path.</param>
  407. /// <returns>ImageSize.</returns>
  408. private ImageSize GetImageSizeInternal(string cacheKey, string path)
  409. {
  410. // Now check the file system cache
  411. var fullCachePath = GetCachePath(_imageSizeCachePath, cacheKey, ".txt");
  412. try
  413. {
  414. var result = File.ReadAllText(fullCachePath).Split('|');
  415. return new ImageSize
  416. {
  417. Width = double.Parse(result[0], UsCulture),
  418. Height = double.Parse(result[1], UsCulture)
  419. };
  420. }
  421. catch (IOException)
  422. {
  423. // Cache file doesn't exist or is currently being written to
  424. }
  425. var syncLock = GetObjectLock(fullCachePath);
  426. lock (syncLock)
  427. {
  428. try
  429. {
  430. var result = File.ReadAllText(fullCachePath).Split('|');
  431. return new ImageSize
  432. {
  433. Width = double.Parse(result[0], UsCulture),
  434. Height = double.Parse(result[1], UsCulture)
  435. };
  436. }
  437. catch (FileNotFoundException)
  438. {
  439. // Cache file doesn't exist no biggie
  440. }
  441. catch (DirectoryNotFoundException)
  442. {
  443. // Cache file doesn't exist no biggie
  444. }
  445. var size = ImageHeader.GetDimensions(path, _logger, _fileSystem);
  446. var parentPath = Path.GetDirectoryName(fullCachePath);
  447. Directory.CreateDirectory(parentPath);
  448. // Update the file system cache
  449. File.WriteAllText(fullCachePath, size.Width.ToString(UsCulture) + @"|" + size.Height.ToString(UsCulture));
  450. return new ImageSize { Width = size.Width, Height = size.Height };
  451. }
  452. }
  453. /// <summary>
  454. /// Gets the image cache tag.
  455. /// </summary>
  456. /// <param name="item">The item.</param>
  457. /// <param name="imageType">Type of the image.</param>
  458. /// <param name="imagePath">The image path.</param>
  459. /// <returns>Guid.</returns>
  460. /// <exception cref="System.ArgumentNullException">item</exception>
  461. public Guid GetImageCacheTag(BaseItem item, ImageType imageType, string imagePath)
  462. {
  463. if (item == null)
  464. {
  465. throw new ArgumentNullException("item");
  466. }
  467. if (string.IsNullOrEmpty(imagePath))
  468. {
  469. throw new ArgumentNullException("imagePath");
  470. }
  471. var dateModified = item.GetImageDateModified(imagePath);
  472. var supportedEnhancers = GetSupportedEnhancers(item, imageType);
  473. return GetImageCacheTag(item, imageType, imagePath, dateModified, supportedEnhancers);
  474. }
  475. /// <summary>
  476. /// Gets the image cache tag.
  477. /// </summary>
  478. /// <param name="item">The item.</param>
  479. /// <param name="imageType">Type of the image.</param>
  480. /// <param name="originalImagePath">The original image path.</param>
  481. /// <param name="dateModified">The date modified of the original image file.</param>
  482. /// <param name="imageEnhancers">The image enhancers.</param>
  483. /// <returns>Guid.</returns>
  484. /// <exception cref="System.ArgumentNullException">item</exception>
  485. public Guid GetImageCacheTag(BaseItem item, ImageType imageType, string originalImagePath, DateTime dateModified, IEnumerable<IImageEnhancer> imageEnhancers)
  486. {
  487. if (item == null)
  488. {
  489. throw new ArgumentNullException("item");
  490. }
  491. if (imageEnhancers == null)
  492. {
  493. throw new ArgumentNullException("imageEnhancers");
  494. }
  495. if (string.IsNullOrEmpty(originalImagePath))
  496. {
  497. throw new ArgumentNullException("originalImagePath");
  498. }
  499. // Cache name is created with supported enhancers combined with the last config change so we pick up new config changes
  500. var cacheKeys = imageEnhancers.Select(i => i.GetConfigurationCacheKey(item, imageType)).ToList();
  501. cacheKeys.Add(originalImagePath + dateModified.Ticks);
  502. return string.Join("|", cacheKeys.ToArray()).GetMD5();
  503. }
  504. /// <summary>
  505. /// Gets the enhanced image.
  506. /// </summary>
  507. /// <param name="item">The item.</param>
  508. /// <param name="imageType">Type of the image.</param>
  509. /// <param name="imageIndex">Index of the image.</param>
  510. /// <returns>Task{System.String}.</returns>
  511. public async Task<string> GetEnhancedImage(BaseItem item, ImageType imageType, int imageIndex)
  512. {
  513. var enhancers = GetSupportedEnhancers(item, imageType).ToList();
  514. var imagePath = item.GetImagePath(imageType, imageIndex);
  515. var dateModified = item.GetImageDateModified(imagePath);
  516. var result = await GetEnhancedImage(imagePath, dateModified, item, imageType, imageIndex, enhancers);
  517. return result.Item1;
  518. }
  519. private async Task<Tuple<string, DateTime>> GetEnhancedImage(string originalImagePath, DateTime dateModified, BaseItem item,
  520. ImageType imageType, int imageIndex,
  521. List<IImageEnhancer> enhancers)
  522. {
  523. try
  524. {
  525. // Enhance if we have enhancers
  526. var ehnancedImagePath = await GetEnhancedImageInternal(originalImagePath, dateModified, item, imageType, imageIndex, enhancers).ConfigureAwait(false);
  527. // If the path changed update dateModified
  528. if (!ehnancedImagePath.Equals(originalImagePath, StringComparison.OrdinalIgnoreCase))
  529. {
  530. dateModified = File.GetLastWriteTimeUtc(ehnancedImagePath);
  531. return new Tuple<string, DateTime>(ehnancedImagePath, dateModified);
  532. }
  533. }
  534. catch (Exception ex)
  535. {
  536. _logger.Error("Error enhancing image", ex);
  537. }
  538. return new Tuple<string, DateTime>(originalImagePath, dateModified);
  539. }
  540. /// <summary>
  541. /// Runs an image through the image enhancers, caches the result, and returns the cached path
  542. /// </summary>
  543. /// <param name="originalImagePath">The original image path.</param>
  544. /// <param name="dateModified">The date modified of the original image file.</param>
  545. /// <param name="item">The item.</param>
  546. /// <param name="imageType">Type of the image.</param>
  547. /// <param name="imageIndex">Index of the image.</param>
  548. /// <param name="supportedEnhancers">The supported enhancers.</param>
  549. /// <returns>System.String.</returns>
  550. /// <exception cref="System.ArgumentNullException">originalImagePath</exception>
  551. private async Task<string> GetEnhancedImageInternal(string originalImagePath, DateTime dateModified, BaseItem item, ImageType imageType, int imageIndex, List<IImageEnhancer> supportedEnhancers)
  552. {
  553. if (string.IsNullOrEmpty(originalImagePath))
  554. {
  555. throw new ArgumentNullException("originalImagePath");
  556. }
  557. if (item == null)
  558. {
  559. throw new ArgumentNullException("item");
  560. }
  561. var cacheGuid = GetImageCacheTag(item, imageType, originalImagePath, dateModified, supportedEnhancers);
  562. // All enhanced images are saved as png to allow transparency
  563. var enhancedImagePath = GetCachePath(_enhancedImageCachePath, cacheGuid + ".png");
  564. var semaphore = GetLock(enhancedImagePath);
  565. await semaphore.WaitAsync().ConfigureAwait(false);
  566. // Check again in case of contention
  567. if (File.Exists(enhancedImagePath))
  568. {
  569. semaphore.Release();
  570. return enhancedImagePath;
  571. }
  572. try
  573. {
  574. using (var fileStream = _fileSystem.GetFileStream(originalImagePath, FileMode.Open, FileAccess.Read, FileShare.Read, true))
  575. {
  576. // Copy to memory stream to avoid Image locking file
  577. using (var memoryStream = new MemoryStream())
  578. {
  579. await fileStream.CopyToAsync(memoryStream).ConfigureAwait(false);
  580. using (var originalImage = Image.FromStream(memoryStream, true, false))
  581. {
  582. //Pass the image through registered enhancers
  583. using (var newImage = await ExecuteImageEnhancers(supportedEnhancers, originalImage, item, imageType, imageIndex).ConfigureAwait(false))
  584. {
  585. var parentDirectory = Path.GetDirectoryName(enhancedImagePath);
  586. Directory.CreateDirectory(parentDirectory);
  587. //And then save it in the cache
  588. using (var outputStream = _fileSystem.GetFileStream(enhancedImagePath, FileMode.Create, FileAccess.Write, FileShare.Read, false))
  589. {
  590. newImage.Save(ImageFormat.Png, outputStream, 100);
  591. }
  592. }
  593. }
  594. }
  595. }
  596. }
  597. finally
  598. {
  599. semaphore.Release();
  600. }
  601. return enhancedImagePath;
  602. }
  603. /// <summary>
  604. /// Executes the image enhancers.
  605. /// </summary>
  606. /// <param name="imageEnhancers">The image enhancers.</param>
  607. /// <param name="originalImage">The original image.</param>
  608. /// <param name="item">The item.</param>
  609. /// <param name="imageType">Type of the image.</param>
  610. /// <param name="imageIndex">Index of the image.</param>
  611. /// <returns>Task{EnhancedImage}.</returns>
  612. private async Task<Image> ExecuteImageEnhancers(IEnumerable<IImageEnhancer> imageEnhancers, Image originalImage, BaseItem item, ImageType imageType, int imageIndex)
  613. {
  614. var result = originalImage;
  615. // Run the enhancers sequentially in order of priority
  616. foreach (var enhancer in imageEnhancers)
  617. {
  618. var typeName = enhancer.GetType().Name;
  619. try
  620. {
  621. result = await enhancer.EnhanceImageAsync(item, result, imageType, imageIndex).ConfigureAwait(false);
  622. }
  623. catch (Exception ex)
  624. {
  625. _logger.ErrorException("{0} failed enhancing {1}", ex, typeName, item.Name);
  626. throw;
  627. }
  628. }
  629. return result;
  630. }
  631. /// <summary>
  632. /// The _semaphoreLocks
  633. /// </summary>
  634. private readonly ConcurrentDictionary<string, object> _locks = new ConcurrentDictionary<string, object>();
  635. /// <summary>
  636. /// Gets the lock.
  637. /// </summary>
  638. /// <param name="filename">The filename.</param>
  639. /// <returns>System.Object.</returns>
  640. private object GetObjectLock(string filename)
  641. {
  642. return _locks.GetOrAdd(filename, key => new object());
  643. }
  644. /// <summary>
  645. /// The _semaphoreLocks
  646. /// </summary>
  647. private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks = new ConcurrentDictionary<string, SemaphoreSlim>();
  648. /// <summary>
  649. /// Gets the lock.
  650. /// </summary>
  651. /// <param name="filename">The filename.</param>
  652. /// <returns>System.Object.</returns>
  653. private SemaphoreSlim GetLock(string filename)
  654. {
  655. return _semaphoreLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1));
  656. }
  657. /// <summary>
  658. /// Gets the cache path.
  659. /// </summary>
  660. /// <param name="path">The path.</param>
  661. /// <param name="uniqueName">Name of the unique.</param>
  662. /// <param name="fileExtension">The file extension.</param>
  663. /// <returns>System.String.</returns>
  664. /// <exception cref="System.ArgumentNullException">
  665. /// path
  666. /// or
  667. /// uniqueName
  668. /// or
  669. /// fileExtension
  670. /// </exception>
  671. public string GetCachePath(string path, string uniqueName, string fileExtension)
  672. {
  673. if (string.IsNullOrEmpty(path))
  674. {
  675. throw new ArgumentNullException("path");
  676. }
  677. if (string.IsNullOrEmpty(uniqueName))
  678. {
  679. throw new ArgumentNullException("uniqueName");
  680. }
  681. if (string.IsNullOrEmpty(fileExtension))
  682. {
  683. throw new ArgumentNullException("fileExtension");
  684. }
  685. var filename = uniqueName.GetMD5() + fileExtension;
  686. return GetCachePath(path, filename);
  687. }
  688. /// <summary>
  689. /// Gets the cache path.
  690. /// </summary>
  691. /// <param name="path">The path.</param>
  692. /// <param name="filename">The filename.</param>
  693. /// <returns>System.String.</returns>
  694. /// <exception cref="System.ArgumentNullException">
  695. /// path
  696. /// or
  697. /// filename
  698. /// </exception>
  699. public string GetCachePath(string path, string filename)
  700. {
  701. if (string.IsNullOrEmpty(path))
  702. {
  703. throw new ArgumentNullException("path");
  704. }
  705. if (string.IsNullOrEmpty(filename))
  706. {
  707. throw new ArgumentNullException("filename");
  708. }
  709. var prefix = filename.Substring(0, 1);
  710. path = Path.Combine(path, prefix);
  711. return Path.Combine(path, filename);
  712. }
  713. public IEnumerable<IImageEnhancer> GetSupportedEnhancers(BaseItem item, ImageType imageType)
  714. {
  715. return ImageEnhancers.Where(i =>
  716. {
  717. try
  718. {
  719. return i.Supports(item as BaseItem, imageType);
  720. }
  721. catch (Exception ex)
  722. {
  723. _logger.ErrorException("Error in image enhancer: {0}", ex, i.GetType().Name);
  724. return false;
  725. }
  726. }).ToList();
  727. }
  728. }
  729. }