Quellcode durchsuchen

Cleanup ImageProcessor

Bond_009 vor 6 Jahren
Ursprung
Commit
8fc8fc0622

+ 93 - 138
Emby.Drawing/ImageProcessor.cs

@@ -1,4 +1,3 @@
-using SkiaSharp;
 using System;
 using System.Collections.Generic;
 using System.Globalization;
@@ -18,9 +17,8 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Extensions;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Threading;
 using Microsoft.Extensions.Logging;
+using SkiaSharp;
 
 namespace Emby.Drawing
 {
@@ -47,28 +45,27 @@ namespace Emby.Drawing
         private readonly ILogger _logger;
 
         private readonly IFileSystem _fileSystem;
-        private readonly IJsonSerializer _jsonSerializer;
         private readonly IServerApplicationPaths _appPaths;
         private IImageEncoder _imageEncoder;
         private readonly Func<ILibraryManager> _libraryManager;
         private readonly Func<IMediaEncoder> _mediaEncoder;
 
-        public ImageProcessor(ILogger logger,
+        public ImageProcessor(
+            ILogger logger,
             IServerApplicationPaths appPaths,
             IFileSystem fileSystem,
-            IJsonSerializer jsonSerializer,
             IImageEncoder imageEncoder,
-            Func<ILibraryManager> libraryManager, ITimerFactory timerFactory, Func<IMediaEncoder> mediaEncoder)
+            Func<ILibraryManager> libraryManager,
+            Func<IMediaEncoder> mediaEncoder)
         {
             _logger = logger;
+            _appPaths = appPaths;
             _fileSystem = fileSystem;
-            _jsonSerializer = jsonSerializer;
             _imageEncoder = imageEncoder;
             _libraryManager = libraryManager;
             _mediaEncoder = mediaEncoder;
-            _appPaths = appPaths;
 
-            ImageEnhancers = new IImageEnhancer[] { };
+            ImageEnhancers = Array.Empty<IImageEnhancer>();
             ImageHelper.ImageProcessor = this;
         }
 
@@ -145,21 +142,19 @@ namespace Emby.Drawing
             return _imageEncoder.SupportedOutputFormats;
         }
 
-        private readonly string[] TransparentImageTypes = new string[] { ".png", ".webp", ".gif" };
+        private static readonly string[] TransparentImageTypes = new string[] { ".png", ".webp", ".gif" };
         public bool SupportsTransparency(string path)
-        {
-            return TransparentImageTypes.Contains(Path.GetExtension(path) ?? string.Empty);
-        }
+            => TransparentImageTypes.Contains(Path.GetExtension(path).ToLower());
 
-        public async Task<Tuple<string, string, DateTime>> ProcessImage(ImageProcessingOptions options)
+        public async Task<(string path, string mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options)
         {
             if (options == null)
             {
                 throw new ArgumentNullException(nameof(options));
             }
 
-            var originalImage = options.Image;
-            var item = options.Item;
+            ItemImageInfo originalImage = options.Image;
+            BaseItem item = options.Item;
 
             if (!originalImage.IsLocalFile)
             {
@@ -170,19 +165,23 @@ namespace Emby.Drawing
                 originalImage = await _libraryManager().ConvertImageToLocal(item, originalImage, options.ImageIndex).ConfigureAwait(false);
             }
 
-            var originalImagePath = originalImage.Path;
-            var dateModified = originalImage.DateModified;
-            var originalImageSize = originalImage.Width > 0 && originalImage.Height > 0 ? new ImageSize(originalImage.Width, originalImage.Height) : (ImageSize?)null;
+            string originalImagePath = originalImage.Path;
+            DateTime dateModified = originalImage.DateModified;
+            ImageSize? originalImageSize = null;
+            if (originalImage.Width > 0 && originalImage.Height > 0)
+            {
+                originalImageSize = new ImageSize(originalImage.Width, originalImage.Height);
+            }
 
             if (!_imageEncoder.SupportsImageEncoding)
             {
-                return new Tuple<string, string, DateTime>(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
+                return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
             }
 
             var supportedImageInfo = await GetSupportedImage(originalImagePath, dateModified).ConfigureAwait(false);
-            originalImagePath = supportedImageInfo.Item1;
-            dateModified = supportedImageInfo.Item2;
-            var requiresTransparency = TransparentImageTypes.Contains(Path.GetExtension(originalImagePath) ?? string.Empty);
+            originalImagePath = supportedImageInfo.path;
+            dateModified = supportedImageInfo.dateModified;
+            bool requiresTransparency = TransparentImageTypes.Contains(Path.GetExtension(originalImagePath));
 
             if (options.Enhancers.Length > 0)
             {
@@ -196,20 +195,18 @@ namespace Emby.Drawing
                     DateModified = dateModified,
                     Type = originalImage.Type,
                     Path = originalImagePath
-
                 }, requiresTransparency, item, options.ImageIndex, options.Enhancers, CancellationToken.None).ConfigureAwait(false);
 
-                originalImagePath = tuple.Item1;
-                dateModified = tuple.Item2;
-                requiresTransparency = tuple.Item3;
+                originalImagePath = tuple.path;
+                dateModified = tuple.dateModified;
+                requiresTransparency = tuple.transparent;
                 // TODO: Get this info
                 originalImageSize = null;
             }
 
-            var photo = item as Photo;
-            var autoOrient = false;
+            bool autoOrient = false;
             ImageOrientation? orientation = null;
-            if (photo != null)
+            if (item is Photo photo)
             {
                 if (photo.Orientation.HasValue)
                 {
@@ -230,7 +227,7 @@ namespace Emby.Drawing
             if (options.HasDefaultOptions(originalImagePath, originalImageSize) && (!autoOrient || !options.RequiresAutoOrientation))
             {
                 // Just spit out the original file if all the options are default
-                return new Tuple<string, string, DateTime>(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
+                return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
             }
 
             //ImageSize? originalImageSize = GetSavedImageSize(originalImagePath, dateModified);
@@ -241,15 +238,15 @@ namespace Emby.Drawing
             //    return new ValueTuple<string, string, DateTime>(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
             //}
 
-            var newSize = ImageHelper.GetNewImageSize(options, null);
-            var quality = options.Quality;
+            ImageSize newSize = ImageHelper.GetNewImageSize(options, null);
+            int quality = options.Quality;
 
-            var outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency);
-            var cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, outputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.Blur, options.BackgroundColor, options.ForegroundLayer);
+            ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency);
+            string cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, outputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.Blur, options.BackgroundColor, options.ForegroundLayer);
 
             CheckDisposed();
 
-            var lockInfo = GetLock(cacheFilePath);
+            LockInfo lockInfo = GetLock(cacheFilePath);
 
             await lockInfo.Lock.WaitAsync().ConfigureAwait(false);
 
@@ -262,17 +259,15 @@ namespace Emby.Drawing
                         options.CropWhiteSpace = false;
                     }
 
-                    var resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat);
+                    string resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat);
 
                     if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase))
                     {
-                        return new Tuple<string, string, DateTime>(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
+                        return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
                     }
-
-                    return new Tuple<string, string, DateTime>(cacheFilePath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(cacheFilePath));
                 }
 
-                return new Tuple<string, string, DateTime>(cacheFilePath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(cacheFilePath));
+                return (cacheFilePath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(cacheFilePath));
             }
             catch (ArgumentOutOfRangeException ex)
             {
@@ -281,7 +276,7 @@ namespace Emby.Drawing
                 _logger.LogError(ex, "Error encoding image");
 #endif
                 // Just spit out the original file if all the options are default
-                return new Tuple<string, string, DateTime>(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
+                return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
             }
             catch (Exception ex)
             {
@@ -289,7 +284,7 @@ namespace Emby.Drawing
                 _logger.LogError(ex, "Error encoding image");
 
                 // Just spit out the original file if all the options are default
-                return new Tuple<string, string, DateTime>(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
+                return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
             }
             finally
             {
@@ -325,42 +320,17 @@ namespace Emby.Drawing
             return ImageFormat.Jpg;
         }
 
-        private void CopyFile(string src, string destination)
-        {
-            try
-            {
-                _fileSystem.CopyFile(src, destination, true);
-            }
-            catch
-            {
-
-            }
-        }
-
         private string GetMimeType(ImageFormat format, string path)
         {
-            if (format == ImageFormat.Bmp)
-            {
-                return MimeTypes.GetMimeType("i.bmp");
-            }
-            if (format == ImageFormat.Gif)
-            {
-                return MimeTypes.GetMimeType("i.gif");
-            }
-            if (format == ImageFormat.Jpg)
-            {
-                return MimeTypes.GetMimeType("i.jpg");
-            }
-            if (format == ImageFormat.Png)
+            switch(format)
             {
-                return MimeTypes.GetMimeType("i.png");
+                case ImageFormat.Bmp:  return MimeTypes.GetMimeType("i.bmp");
+                case ImageFormat.Gif:  return MimeTypes.GetMimeType("i.gif");
+                case ImageFormat.Jpg:  return MimeTypes.GetMimeType("i.jpg");
+                case ImageFormat.Png:  return MimeTypes.GetMimeType("i.png");
+                case ImageFormat.Webp: return MimeTypes.GetMimeType("i.webp");
+                default:               return MimeTypes.GetMimeType(path);
             }
-            if (format == ImageFormat.Webp)
-            {
-                return MimeTypes.GetMimeType("i.webp");
-            }
-
-            return MimeTypes.GetMimeType(path);
         }
 
         /// <summary>
@@ -373,17 +343,12 @@ namespace Emby.Drawing
         /// </summary>
         private string GetCacheFilePath(string originalPath, ImageSize outputSize, int quality, DateTime dateModified, ImageFormat format, bool addPlayedIndicator, double percentPlayed, int? unwatchedCount, int? blur, string backgroundColor, string foregroundLayer)
         {
-            var filename = originalPath;
-
-            filename += "width=" + outputSize.Width;
-
-            filename += "height=" + outputSize.Height;
-
-            filename += "quality=" + quality;
-
-            filename += "datemodified=" + dateModified.Ticks;
-
-            filename += "f=" + format;
+            var filename = originalPath
+                + "width=" + outputSize.Width
+                + "height=" + outputSize.Height
+                + "quality=" + quality
+                + "datemodified=" + dateModified.Ticks
+                + "f=" + format;
 
             if (addPlayedIndicator)
             {
@@ -421,26 +386,20 @@ namespace Emby.Drawing
         }
 
         public ImageSize GetImageSize(BaseItem item, ItemImageInfo info)
-        {
-            return GetImageSize(item, info, true);
-        }
+            => GetImageSize(item, info, true);
 
         public ImageSize GetImageSize(BaseItem item, ItemImageInfo info, bool updateItem)
         {
-            var width = info.Width;
-            var height = info.Height;
+            int width = info.Width;
+            int height = info.Height;
 
             if (height > 0 && width > 0)
             {
-                return new ImageSize
-                {
-                    Width = width,
-                    Height = height
-                };
+                return new ImageSize(width, height);
             }
 
-            var path = info.Path;
-            _logger.LogInformation("Getting image size for item {0} {1}", item.GetType().Name, path);
+            string path = info.Path;
+            _logger.LogInformation("Getting image size for item {ItemType} {Path}", item.GetType().Name, path);
 
             var size = GetImageSize(path);
 
@@ -466,15 +425,11 @@ namespace Emby.Drawing
             }
 
             using (var s = new SKFileStream(path))
-                using (var codec = SKCodec.Create(s))
-                {
-                    var info = codec.Info;
-                    return new ImageSize
-                    {
-                        Height = info.Height,
-                        Width = info.Width
-                    };
-                }
+            using (var codec = SKCodec.Create(s))
+            {
+                var info = codec.Info;
+                return new ImageSize(info.Width, info.Height);
+            }
         }
 
         /// <summary>
@@ -518,9 +473,9 @@ namespace Emby.Drawing
         /// <exception cref="ArgumentNullException">item</exception>
         public string GetImageCacheTag(BaseItem item, ItemImageInfo image, IImageEnhancer[] imageEnhancers)
         {
-            var originalImagePath = image.Path;
-            var dateModified = image.DateModified;
-            var imageType = image.Type;
+            string originalImagePath = image.Path;
+            DateTime dateModified = image.DateModified;
+            ImageType imageType = image.Type;
 
             // Optimization
             if (imageEnhancers.Length == 0)
@@ -532,28 +487,28 @@ namespace Emby.Drawing
             var cacheKeys = imageEnhancers.Select(i => i.GetConfigurationCacheKey(item, imageType)).ToList();
             cacheKeys.Add(originalImagePath + dateModified.Ticks);
 
-            return string.Join("|", cacheKeys.ToArray()).GetMD5().ToString("N");
+            return string.Join("|", cacheKeys).GetMD5().ToString("N");
         }
 
-        private async Task<ValueTuple<string, DateTime>> GetSupportedImage(string originalImagePath, DateTime dateModified)
+        private async Task<(string path, DateTime dateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
         {
-            var inputFormat = (Path.GetExtension(originalImagePath) ?? string.Empty)
+            var inputFormat = Path.GetExtension(originalImagePath)
                 .TrimStart('.')
                 .Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase);
 
             // These are just jpg files renamed as tbn
             if (string.Equals(inputFormat, "tbn", StringComparison.OrdinalIgnoreCase))
             {
-                return new ValueTuple<string, DateTime>(originalImagePath, dateModified);
+                return (originalImagePath, dateModified);
             }
 
             if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat, StringComparer.OrdinalIgnoreCase))
             {
                 try
                 {
-                    var filename = (originalImagePath + dateModified.Ticks.ToString(UsCulture)).GetMD5().ToString("N");
+                    string filename = (originalImagePath + dateModified.Ticks.ToString(UsCulture)).GetMD5().ToString("N");
 
-                    var cacheExtension = _mediaEncoder().SupportsEncoder("libwebp") ? ".webp" : ".png";
+                    string cacheExtension = _mediaEncoder().SupportsEncoder("libwebp") ? ".webp" : ".png";
                     var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension);
 
                     var file = _fileSystem.GetFileInfo(outputPath);
@@ -571,11 +526,11 @@ namespace Emby.Drawing
                 }
                 catch (Exception ex)
                 {
-                    _logger.LogError(ex, "Image conversion failed for {originalImagePath}", originalImagePath);
+                    _logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath);
                 }
             }
 
-            return new ValueTuple<string, DateTime>(originalImagePath, dateModified);
+            return (originalImagePath, dateModified);
         }
 
         /// <summary>
@@ -589,16 +544,17 @@ namespace Emby.Drawing
         {
             var enhancers = GetSupportedEnhancers(item, imageType);
 
-            var imageInfo = item.GetImageInfo(imageType, imageIndex);
+            ItemImageInfo imageInfo = item.GetImageInfo(imageType, imageIndex);
 
-            var inputImageSupportsTransparency = SupportsTransparency(imageInfo.Path);
+            bool inputImageSupportsTransparency = SupportsTransparency(imageInfo.Path);
 
             var result = await GetEnhancedImage(imageInfo, inputImageSupportsTransparency, item, imageIndex, enhancers, CancellationToken.None);
 
-            return result.Item1;
+            return result.path;
         }
 
-        private async Task<ValueTuple<string, DateTime, bool>> GetEnhancedImage(ItemImageInfo image,
+        private async Task<(string path, DateTime dateModified, bool transparent)> GetEnhancedImage(
+            ItemImageInfo image,
             bool inputImageSupportsTransparency,
             BaseItem item,
             int imageIndex,
@@ -616,14 +572,14 @@ namespace Emby.Drawing
                 // Enhance if we have enhancers
                 var enhancedImageInfo = await GetEnhancedImageInternal(originalImagePath, item, imageType, imageIndex, enhancers, cacheGuid, cancellationToken).ConfigureAwait(false);
 
-                var enhancedImagePath = enhancedImageInfo.Item1;
+                string enhancedImagePath = enhancedImageInfo.path;
 
                 // If the path changed update dateModified
                 if (!string.Equals(enhancedImagePath, originalImagePath, StringComparison.OrdinalIgnoreCase))
                 {
-                    var treatmentRequiresTransparency = enhancedImageInfo.Item2;
+                    var treatmentRequiresTransparency = enhancedImageInfo.transparent;
 
-                    return new ValueTuple<string, DateTime, bool>(enhancedImagePath, _fileSystem.GetLastWriteTimeUtc(enhancedImagePath), treatmentRequiresTransparency);
+                    return (enhancedImagePath, _fileSystem.GetLastWriteTimeUtc(enhancedImagePath), treatmentRequiresTransparency);
                 }
             }
             catch (Exception ex)
@@ -631,7 +587,7 @@ namespace Emby.Drawing
                 _logger.LogError(ex, "Error enhancing image");
             }
 
-            return new ValueTuple<string, DateTime, bool>(originalImagePath, dateModified, inputImageSupportsTransparency);
+            return (originalImagePath, dateModified, inputImageSupportsTransparency);
         }
 
         /// <summary>
@@ -649,7 +605,8 @@ namespace Emby.Drawing
         /// or
         /// item
         /// </exception>
-        private async Task<ValueTuple<string, bool>> GetEnhancedImageInternal(string originalImagePath,
+        private async Task<(string path, bool transparent)> GetEnhancedImageInternal(
+            string originalImagePath,
             BaseItem item,
             ImageType imageType,
             int imageIndex,
@@ -677,13 +634,13 @@ namespace Emby.Drawing
             }
 
             // All enhanced images are saved as png to allow transparency
-            var cacheExtension = _imageEncoder.SupportedOutputFormats.Contains(ImageFormat.Webp) ?
+            string cacheExtension = _imageEncoder.SupportedOutputFormats.Contains(ImageFormat.Webp) ?
                 ".webp" :
                 (treatmentRequiresTransparency ? ".png" : ".jpg");
 
-            var enhancedImagePath = GetCachePath(EnhancedImageCachePath, cacheGuid + cacheExtension);
+            string enhancedImagePath = GetCachePath(EnhancedImageCachePath, cacheGuid + cacheExtension);
 
-            var lockInfo = GetLock(enhancedImagePath);
+            LockInfo lockInfo = GetLock(enhancedImagePath);
 
             await lockInfo.Lock.WaitAsync(cancellationToken).ConfigureAwait(false);
 
@@ -692,14 +649,14 @@ namespace Emby.Drawing
                 // Check again in case of contention
                 if (_fileSystem.FileExists(enhancedImagePath))
                 {
-                    return new ValueTuple<string, bool>(enhancedImagePath, treatmentRequiresTransparency);
+                    return (enhancedImagePath, treatmentRequiresTransparency);
                 }
 
                 _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(enhancedImagePath));
 
                 await ExecuteImageEnhancers(supportedEnhancers, originalImagePath, enhancedImagePath, item, imageType, imageIndex).ConfigureAwait(false);
 
-                return new ValueTuple<string, bool>(enhancedImagePath, treatmentRequiresTransparency);
+                return (enhancedImagePath, treatmentRequiresTransparency);
             }
             finally
             {
@@ -788,18 +745,16 @@ namespace Emby.Drawing
 
             var prefix = filename.Substring(0, 1);
 
-            path = Path.Combine(path, prefix);
-
-            return Path.Combine(path, filename);
+            return Path.Combine(path, prefix, filename);
         }
 
         public void CreateImageCollage(ImageCollageOptions options)
         {
-            _logger.LogInformation("Creating image collage and saving to {0}", options.OutputPath);
+            _logger.LogInformation("Creating image collage and saving to {Path}", options.OutputPath);
 
             _imageEncoder.CreateImageCollage(options);
 
-            _logger.LogInformation("Completed creation of image collage and saved to {0}", options.OutputPath);
+            _logger.LogInformation("Completed creation of image collage and saved to {Path}", options.OutputPath);
         }
 
         public IImageEnhancer[] GetSupportedEnhancers(BaseItem item, ImageType imageType)

+ 1 - 1
Emby.Server.Implementations/ApplicationHost.cs

@@ -1050,7 +1050,7 @@ namespace Emby.Server.Implementations
 
         private IImageProcessor GetImageProcessor()
         {
-            return new ImageProcessor(LoggerFactory.CreateLogger("ImageProcessor"), ServerConfigurationManager.ApplicationPaths, FileSystemManager, JsonSerializer, ImageEncoder, () => LibraryManager, TimerFactory, () => MediaEncoder);
+            return new ImageProcessor(LoggerFactory.CreateLogger("ImageProcessor"), ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder);
         }
 
         protected virtual FFMpegInstallInfo GetFfmpegInstallInfo()

+ 2 - 3
Jellyfin.Server/Program.cs

@@ -103,7 +103,7 @@ namespace Jellyfin.Server
             {
                 appHost.Init();
 
-                appHost.ImageProcessor.ImageEncoder = getImageEncoder(_logger, fileSystem, options, () => appHost.HttpClient, appPaths, environmentInfo, appHost.LocalizationManager);
+                appHost.ImageProcessor.ImageEncoder = GetImageEncoder(_logger, fileSystem, options, () => appHost.HttpClient, appPaths, environmentInfo, appHost.LocalizationManager);
 
                 _logger.LogInformation("Running startup tasks");
 
@@ -256,13 +256,12 @@ namespace Jellyfin.Server
             }
         }
 
-        public static IImageEncoder getImageEncoder(
+        public static IImageEncoder GetImageEncoder(
             ILogger logger,
             IFileSystem fileSystem,
             StartupOptions startupOptions,
             Func<IHttpClient> httpClient,
             IApplicationPaths appPaths,
-            IEnvironmentInfo environment,
             ILocalizationManager localizationManager)
         {
             try

+ 1 - 1
MediaBrowser.Controller/Drawing/IImageProcessor.cs

@@ -82,7 +82,7 @@ namespace MediaBrowser.Controller.Drawing
         /// </summary>
         /// <param name="options">The options.</param>
         /// <returns>Task.</returns>
-        Task<Tuple<string, string, DateTime>> ProcessImage(ImageProcessingOptions options);
+        Task<(string path, string mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options);
 
         /// <summary>
         /// Gets the enhanced image.