/// <summary> /// Processes an image by resizing to target dimensions /// </summary> /// <param name="entity">The entity that owns the image</param> /// <param name="imageType">The image type</param> /// <param name="imageIndex">The image index (currently only used with backdrops)</param> /// <param name="originalImagePath">The original image path.</param> /// <param name="cropWhitespace">if set to <c>true</c> [crop whitespace].</param> /// <param name="dateModified">The last date modified of the original image file</param> /// <param name="toStream">The stream to save the new image to</param> /// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param> /// <param name="height">Use if a fixed height is required. Aspect ratio will be preserved.</param> /// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param> /// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param> /// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param> /// <param name="enhancers">The enhancers.</param> /// <returns>Task.</returns> /// <exception cref="System.ArgumentNullException">entity</exception> public async Task ProcessImage(BaseItem entity, ImageType imageType, int imageIndex, string originalImagePath, bool cropWhitespace, DateTime dateModified, Stream toStream, int?width, int?height, int?maxWidth, int?maxHeight, int?quality, List <IImageEnhancer> enhancers) { if (entity == null) { throw new ArgumentNullException("entity"); } if (toStream == null) { throw new ArgumentNullException("toStream"); } if (cropWhitespace) { originalImagePath = await GetCroppedImage(originalImagePath, dateModified).ConfigureAwait(false); } // No enhancement - don't cache if (enhancers.Count > 0) { try { // Enhance if we have enhancers var ehnancedImagePath = await GetEnhancedImage(originalImagePath, dateModified, entity, imageType, imageIndex, enhancers).ConfigureAwait(false); // If the path changed update dateModified if (!ehnancedImagePath.Equals(originalImagePath, StringComparison.OrdinalIgnoreCase)) { dateModified = File.GetLastWriteTimeUtc(ehnancedImagePath); originalImagePath = ehnancedImagePath; } } catch (Exception ex) { _logger.Error("Error enhancing image", ex); } } var originalImageSize = await GetImageSize(originalImagePath, dateModified).ConfigureAwait(false); // Determine the output size based on incoming parameters var newSize = DrawingUtils.Resize(originalImageSize, width, height, maxWidth, maxHeight); if (!quality.HasValue) { quality = 90; } var cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality.Value, dateModified); try { using (var fileStream = new FileStream(cacheFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous)) { await fileStream.CopyToAsync(toStream).ConfigureAwait(false); return; } } catch (IOException) { // Cache file doesn't exist or is currently being written ro } var semaphore = GetLock(cacheFilePath); await semaphore.WaitAsync().ConfigureAwait(false); // Check again in case of lock contention if (File.Exists(cacheFilePath)) { try { using (var fileStream = new FileStream(cacheFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous)) { await fileStream.CopyToAsync(toStream).ConfigureAwait(false); return; } } finally { semaphore.Release(); } } try { using (var fileStream = new FileStream(originalImagePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, true)) { // Copy to memory stream to avoid Image locking file using (var memoryStream = new MemoryStream()) { await fileStream.CopyToAsync(memoryStream).ConfigureAwait(false); using (var originalImage = Image.FromStream(memoryStream, true, false)) { var newWidth = Convert.ToInt32(newSize.Width); var newHeight = Convert.ToInt32(newSize.Height); // Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here using (var thumbnail = !ImageExtensions.IsPixelFormatSupportedByGraphicsObject(originalImage.PixelFormat) ? new Bitmap(originalImage, newWidth, newHeight) : new Bitmap(newWidth, newHeight, originalImage.PixelFormat)) { // Preserve the original resolution thumbnail.SetResolution(originalImage.HorizontalResolution, originalImage.VerticalResolution); using (var thumbnailGraph = Graphics.FromImage(thumbnail)) { thumbnailGraph.CompositingQuality = CompositingQuality.HighQuality; thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality; thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic; thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality; thumbnailGraph.CompositingMode = CompositingMode.SourceOver; thumbnailGraph.DrawImage(originalImage, 0, 0, newWidth, newHeight); var outputFormat = originalImage.RawFormat; using (var outputMemoryStream = new MemoryStream()) { // Save to the memory stream thumbnail.Save(outputFormat, outputMemoryStream, quality.Value); var bytes = outputMemoryStream.ToArray(); var outputTask = toStream.WriteAsync(bytes, 0, bytes.Length); // kick off a task to cache the result var cacheTask = CacheResizedImage(cacheFilePath, bytes); await Task.WhenAll(outputTask, cacheTask).ConfigureAwait(false); } } } } } } } finally { semaphore.Release(); } }
/// <summary> /// Processes an image by resizing to target dimensions /// </summary> /// <param name="entity">The entity that owns the image</param> /// <param name="imageType">The image type</param> /// <param name="imageIndex">The image index (currently only used with backdrops)</param> /// <param name="cropWhitespace">if set to <c>true</c> [crop whitespace].</param> /// <param name="dateModified">The last date modified of the original image file</param> /// <param name="toStream">The stream to save the new image to</param> /// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param> /// <param name="height">Use if a fixed height is required. Aspect ratio will be preserved.</param> /// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param> /// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param> /// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param> /// <returns>Task.</returns> /// <exception cref="System.ArgumentNullException">entity</exception> public async Task ProcessImage(BaseItem entity, ImageType imageType, int imageIndex, bool cropWhitespace, DateTime dateModified, Stream toStream, int?width, int?height, int?maxWidth, int?maxHeight, int?quality) { if (entity == null) { throw new ArgumentNullException("entity"); } if (toStream == null) { throw new ArgumentNullException("toStream"); } var originalImagePath = GetImagePath(entity, imageType, imageIndex); if (cropWhitespace) { try { originalImagePath = await GetCroppedImage(originalImagePath, dateModified).ConfigureAwait(false); } catch (Exception ex) { // We have to have a catch-all here because some of the .net image methods throw a plain old Exception _logger.ErrorException("Error cropping image", ex); } } try { // Enhance if we have enhancers var ehnancedImagePath = await GetEnhancedImage(originalImagePath, dateModified, entity, imageType, imageIndex).ConfigureAwait(false); // If the path changed update dateModified if (!ehnancedImagePath.Equals(originalImagePath, StringComparison.OrdinalIgnoreCase)) { dateModified = File.GetLastWriteTimeUtc(ehnancedImagePath); originalImagePath = ehnancedImagePath; } } catch { _logger.Error("Error enhancing image"); } var originalImageSize = await GetImageSize(originalImagePath, dateModified).ConfigureAwait(false); // Determine the output size based on incoming parameters var newSize = DrawingUtils.Resize(originalImageSize, width, height, maxWidth, maxHeight); if (!quality.HasValue) { quality = 90; } var cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality.Value, dateModified); // Grab the cache file if it already exists try { using (var fileStream = new FileStream(cacheFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous)) { await fileStream.CopyToAsync(toStream).ConfigureAwait(false); } return; } catch (FileNotFoundException) { // Cache file doesn't exist. No biggie. } using (var fileStream = File.OpenRead(originalImagePath)) { using (var originalImage = Bitmap.FromStream(fileStream, true, false)) { var newWidth = Convert.ToInt32(newSize.Width); var newHeight = Convert.ToInt32(newSize.Height); // Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here var thumbnail = !ImageExtensions.IsPixelFormatSupportedByGraphicsObject(originalImage.PixelFormat) ? new Bitmap(originalImage, newWidth, newHeight) : new Bitmap(newWidth, newHeight, originalImage.PixelFormat); // Preserve the original resolution thumbnail.SetResolution(originalImage.HorizontalResolution, originalImage.VerticalResolution); var thumbnailGraph = Graphics.FromImage(thumbnail); thumbnailGraph.CompositingQuality = CompositingQuality.HighQuality; thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality; thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic; thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality; thumbnailGraph.CompositingMode = CompositingMode.SourceOver; thumbnailGraph.DrawImage(originalImage, 0, 0, newWidth, newHeight); var outputFormat = originalImage.RawFormat; using (var memoryStream = new MemoryStream { }) { // Save to the memory stream thumbnail.Save(outputFormat, memoryStream, quality.Value); var bytes = memoryStream.ToArray(); var outputTask = Task.Run(async() => await toStream.WriteAsync(bytes, 0, bytes.Length)); // Save to the cache location using (var cacheFileStream = new FileStream(cacheFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous)) { // Save to the filestream await cacheFileStream.WriteAsync(bytes, 0, bytes.Length); } await outputTask.ConfigureAwait(false); } thumbnailGraph.Dispose(); thumbnail.Dispose(); } } }