PageRenderTime 57ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs

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