ImageProcessor.cs 37 KB

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