Webiant Logo Webiant Logo
  1. No results found.

    Try your search with a different keyword or use * as a wildcard.

PictureService.cs

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.StaticFiles;
using Nop.Core;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Media;
using Nop.Core.Infrastructure;
using Nop.Data;
using Nop.Services.Catalog;
using Nop.Services.Configuration;
using Nop.Services.Logging;
using Nop.Services.Seo;
using SkiaSharp;
using Svg.Skia;

namespace Nop.Services.Media;

/// 
/// Picture service
/// 
public partial class PictureService : IPictureService
{
    #region Fields

    protected readonly IDownloadService _downloadService;
    protected readonly IHttpContextAccessor _httpContextAccessor;
    protected readonly ILogger _logger;
    protected readonly INopFileProvider _fileProvider;
    protected readonly IProductAttributeParser _productAttributeParser;
    protected readonly IProductAttributeService _productAttributeService;
    protected readonly IRepository _pictureRepository;
    protected readonly IRepository _pictureBinaryRepository;
    protected readonly IRepository _productPictureRepository;
    protected readonly ISettingService _settingService;
    protected readonly IUrlRecordService _urlRecordService;
    protected readonly IWebHelper _webHelper;
    protected readonly MediaSettings _mediaSettings;

    #endregion

    #region Ctor

    public PictureService(IDownloadService downloadService,
        IHttpContextAccessor httpContextAccessor,
        ILogger logger,
        INopFileProvider fileProvider,
        IProductAttributeParser productAttributeParser,
        IProductAttributeService productAttributeService,
        IRepository pictureRepository,
        IRepository pictureBinaryRepository,
        IRepository productPictureRepository,
        ISettingService settingService,
        IUrlRecordService urlRecordService,
        IWebHelper webHelper,
        MediaSettings mediaSettings)
    {
        _downloadService = downloadService;
        _httpContextAccessor = httpContextAccessor;
        _logger = logger;
        _fileProvider = fileProvider;
        _productAttributeParser = productAttributeParser;
        _productAttributeService = productAttributeService;
        _pictureRepository = pictureRepository;
        _pictureBinaryRepository = pictureBinaryRepository;
        _productPictureRepository = productPictureRepository;
        _settingService = settingService;
        _urlRecordService = urlRecordService;
        _webHelper = webHelper;
        _mediaSettings = mediaSettings;
    }

    #endregion

    #region Utilities

    /// 
    /// Loads a picture from file
    /// 
    /// Picture identifier
    /// MIME type
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the picture binary
    /// 
    protected virtual async Task LoadPictureFromFileAsync(int pictureId, string mimeType)
    {
        var lastPart = await GetFileExtensionFromMimeTypeAsync(mimeType);
        var fileName = $"{pictureId:0000000}_0.{lastPart}";
        var filePath = await GetPictureLocalPathAsync(fileName);

        return await _fileProvider.ReadAllBytesAsync(filePath);
    }

    /// 
    /// Save picture on file system
    /// 
    /// Picture identifier
    /// Picture binary
    /// MIME type
    /// A task that represents the asynchronous operation
    protected virtual async Task SavePictureInFileAsync(int pictureId, byte[] pictureBinary, string mimeType)
    {
        var lastPart = await GetFileExtensionFromMimeTypeAsync(mimeType);
        var fileName = $"{pictureId:0000000}_0.{lastPart}";
        await _fileProvider.WriteAllBytesAsync(await GetPictureLocalPathAsync(fileName), pictureBinary);
    }

    /// 
    /// Delete a picture on file system
    /// 
    /// Picture
    /// A task that represents the asynchronous operation
    protected virtual async Task DeletePictureOnFileSystemAsync(Picture picture)
    {
        ArgumentNullException.ThrowIfNull(picture);

        var lastPart = await GetFileExtensionFromMimeTypeAsync(picture.MimeType);
        var fileName = $"{picture.Id:0000000}_0.{lastPart}";
        var filePath = await GetPictureLocalPathAsync(fileName);
        _fileProvider.DeleteFile(filePath);
    }

    /// 
    /// Delete picture thumbs
    /// 
    /// Picture
    /// A task that represents the asynchronous operation
    protected virtual async Task DeletePictureThumbsAsync(Picture picture)
    {
        var filter = $"{picture.Id:0000000}*.*";
        var currentFiles = _fileProvider.GetFiles(_fileProvider.GetAbsolutePath(NopMediaDefaults.ImageThumbsPath), filter, false);
        foreach (var currentFileName in currentFiles)
        {
            var thumbFilePath = await GetThumbLocalPathAsync(currentFileName);
            _fileProvider.DeleteFile(thumbFilePath);
        }
    }

    /// 
    /// Get picture (thumb) local path
    /// 
    /// Filename
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the local picture thumb path
    /// 
    protected virtual Task GetThumbLocalPathAsync(string thumbFileName)
    {
        var thumbsDirectoryPath = _fileProvider.GetAbsolutePath(NopMediaDefaults.ImageThumbsPath);

        if (_mediaSettings.MultipleThumbDirectories)
        {
            //get the first two letters of the file name
            var fileNameWithoutExtension = _fileProvider.GetFileNameWithoutExtension(thumbFileName);
            if (fileNameWithoutExtension != null && fileNameWithoutExtension.Length > NopMediaDefaults.MultipleThumbDirectoriesLength)
            {
                var subDirectoryName = fileNameWithoutExtension[0..NopMediaDefaults.MultipleThumbDirectoriesLength];
                thumbsDirectoryPath = _fileProvider.GetAbsolutePath(NopMediaDefaults.ImageThumbsPath, subDirectoryName);
                _fileProvider.CreateDirectory(thumbsDirectoryPath);
            }
        }

        var thumbFilePath = _fileProvider.Combine(thumbsDirectoryPath, thumbFileName);
        return Task.FromResult(thumbFilePath);
    }

    /// 
    /// Get images path URL 
    /// 
    /// Store location URL; null to use determine the current store location automatically
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the 
    /// 
    protected virtual Task GetImagesPathUrlAsync(string storeLocation = null)
    {
        var pathBase = _httpContextAccessor.HttpContext.Request?.PathBase.Value ?? string.Empty;
        var imagesPathUrl = _mediaSettings.UseAbsoluteImagePath ? storeLocation : $"{pathBase}/";
        imagesPathUrl = string.IsNullOrEmpty(imagesPathUrl) ? _webHelper.GetStoreLocation() : imagesPathUrl;
        imagesPathUrl += "images/";

        return Task.FromResult(imagesPathUrl);
    }

    /// 
    /// Get picture (thumb) URL 
    /// 
    /// Filename
    /// Store location URL; null to use determine the current store location automatically
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the local picture thumb path
    /// 
    protected virtual async Task GetThumbUrlAsync(string thumbFileName, string storeLocation = null)
    {
        var url = await GetImagesPathUrlAsync(storeLocation) + "thumbs/";

        if (_mediaSettings.MultipleThumbDirectories)
        {
            //get the first two letters of the file name
            var fileNameWithoutExtension = _fileProvider.GetFileNameWithoutExtension(thumbFileName);
            if (fileNameWithoutExtension != null && fileNameWithoutExtension.Length > NopMediaDefaults.MultipleThumbDirectoriesLength)
            {
                var subDirectoryName = fileNameWithoutExtension[0..NopMediaDefaults.MultipleThumbDirectoriesLength];
                url = url + subDirectoryName + "/";
            }
        }

        url += thumbFileName;
        return url;
    }

    /// 
    /// Get picture local path. Used when images stored on file system (not in the database)
    /// 
    /// Filename
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the local picture path
    /// 
    protected virtual Task GetPictureLocalPathAsync(string fileName)
    {
        return Task.FromResult(_fileProvider.GetAbsolutePath("images", fileName));
    }

    /// 
    /// Gets the loaded picture binary depending on picture storage settings
    /// 
    /// Picture
    /// Load from database; otherwise, from file system
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the picture binary
    /// 
    protected virtual async Task LoadPictureBinaryAsync(Picture picture, bool fromDb)
    {
        ArgumentNullException.ThrowIfNull(picture);

        var result = fromDb
            ? (await GetPictureBinaryByPictureIdAsync(picture.Id))?.BinaryData ?? Array.Empty()
            : await LoadPictureFromFileAsync(picture.Id, picture.MimeType);

        return result;
    }

    /// 
    /// Get a value indicating whether some file (thumb) already exists
    /// 
    /// Thumb file path
    /// Thumb file name
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// 
    protected virtual Task GeneratedThumbExistsAsync(string thumbFilePath, string thumbFileName)
    {
        return Task.FromResult(_fileProvider.FileExists(thumbFilePath));
    }

    /// 
    /// Save a value indicating whether some file (thumb) already exists
    /// 
    /// Thumb file path
    /// Thumb file name
    /// MIME type
    /// Picture binary
    /// A task that represents the asynchronous operation
    protected virtual async Task SaveThumbAsync(string thumbFilePath, string thumbFileName, string mimeType, byte[] binary)
    {
        //ensure \thumb directory exists
        var thumbsDirectoryPath = _fileProvider.GetAbsolutePath(NopMediaDefaults.ImageThumbsPath);
        _fileProvider.CreateDirectory(thumbsDirectoryPath);

        //save
        await _fileProvider.WriteAllBytesAsync(thumbFilePath, binary);
    }

    /// 
    /// Updates the picture binary data
    /// 
    /// The picture object
    /// The picture binary data
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the picture binary
    /// 
    protected virtual async Task UpdatePictureBinaryAsync(Picture picture, byte[] binaryData)
    {
        ArgumentNullException.ThrowIfNull(picture);

        var pictureBinary = await GetPictureBinaryByPictureIdAsync(picture.Id);

        var isNew = pictureBinary == null;

        if (isNew)
            pictureBinary = new PictureBinary
            {
                PictureId = picture.Id
            };

        pictureBinary.BinaryData = binaryData;

        if (isNew)
            await _pictureBinaryRepository.InsertAsync(pictureBinary);
        else
            await _pictureBinaryRepository.UpdateAsync(pictureBinary);

        return pictureBinary;
    }

    /// 
    /// Get image format by mime type
    /// 
    /// Mime type
    /// SKEncodedImageFormat
    protected virtual SKEncodedImageFormat GetImageFormatByMimeType(string mimeType)
    {
        var format = SKEncodedImageFormat.Jpeg;
        if (string.IsNullOrEmpty(mimeType))
            return format;

        var parts = mimeType.ToLowerInvariant().Split('/');
        var lastPart = parts[^1];

        switch (lastPart)
        {
            case "webp":
                format = SKEncodedImageFormat.Webp;
                break;
            case "png":
            case "gif":
            case "bmp":
            case "x-icon":
                format = SKEncodedImageFormat.Png;
                break;
            default:
                break;
        }

        return format;
    }

    /// 
    /// Gets the MIME type from the file name
    /// 
    /// 
    /// 
    protected virtual string GetMimeTypeFromFileName(string fileName)
    {
        var provider = new FileExtensionContentTypeProvider();
        if (!provider.TryGetContentType(fileName, out var contentType))
        {
            contentType = "application/octet-stream";
        }
        return contentType;
    }

    /// 
    /// Resize image by targetSize
    /// 
    /// Source image
    /// Destination format
    /// Target size
    /// Image as array of byte[]
    protected virtual byte[] ImageResize(SKBitmap image, SKEncodedImageFormat format, int targetSize)
    {
        ArgumentNullException.ThrowIfNull(image);

        float width, height;
        if (image.Height > image.Width)
        {
            // portrait
            width = image.Width * (targetSize / (float)image.Height);
            height = targetSize;
        }
        else
        {
            // landscape or square
            width = targetSize;
            height = image.Height * (targetSize / (float)image.Width);
        }

        if ((int)width == 0 || (int)height == 0)
        {
            width = image.Width;
            height = image.Height;
        }
        try
        {
            using var resizedBitmap = image.Resize(new SKImageInfo((int)width, (int)height), SKFilterQuality.Medium);
            using var cropImage = SKImage.FromBitmap(resizedBitmap);

            //In order to exclude saving pictures in low quality at the time of installation, we will set the value of this parameter to 80 (as by default)
            return cropImage.Encode(format, _mediaSettings.DefaultImageQuality > 0 ? _mediaSettings.DefaultImageQuality : 80).ToArray();
        }
        catch
        {
            return image.Bytes;
        }

    }

    /// 
    /// Gets pictures
    /// 
    /// Picture identifiers
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the list of pictures
    /// 
    protected virtual async Task> GetPicturesByIdsAsync(int[] pictureIds)
    {
        return await _pictureRepository.GetByIdsAsync(pictureIds, cache => default);
    }

    #endregion

    #region Getting picture local path/URL methods

    /// 
    /// Returns the file extension from mime type.
    /// 
    /// Mime type
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the file extension
    /// 
    public virtual Task GetFileExtensionFromMimeTypeAsync(string mimeType)
    {
        if (mimeType == null)
            return Task.FromResult(null);

        var parts = mimeType.Split('/');
        var lastPart = parts[^1];
        lastPart = lastPart switch
        {
            "pjpeg" => "jpg",
            "jpeg" => "jpeg",
            "bmp" => "bmp",
            "gif" => "gif",
            "x-png" or "png" => "png",
            "tiff" => "tiff",
            "x-icon" => "ico",
            "webp" => "webp",
            "svg+xml" => "svg",
            _ => "",
        };
        return Task.FromResult(lastPart);
    }

    /// 
    /// Gets the loaded picture binary depending on picture storage settings
    /// 
    /// Picture
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the picture binary
    /// 
    public virtual async Task LoadPictureBinaryAsync(Picture picture)
    {
        return await LoadPictureBinaryAsync(picture, await IsStoreInDbAsync());
    }

    /// 
    /// Get picture SEO friendly name
    /// 
    /// Name
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// 
    public virtual async Task GetPictureSeNameAsync(string name)
    {
        return await _urlRecordService.GetSeNameAsync(name, true, false);
    }

    /// 
    /// Gets the default picture URL
    /// 
    /// The target picture size (longest side)
    /// Default picture type
    /// Store location URL; null to use determine the current store location automatically
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the picture URL
    /// 
    public virtual async Task GetDefaultPictureUrlAsync(int targetSize = 0,
        PictureType defaultPictureType = PictureType.Entity,
        string storeLocation = null)
    {
        //get overridden default image if exists
        if (defaultPictureType == PictureType.Entity && _mediaSettings.ProductDefaultImageId > 0)
            return await GetPictureUrlAsync(_mediaSettings.ProductDefaultImageId, targetSize, false, storeLocation, PictureType.Entity);

        var defaultImageFileName = defaultPictureType switch
        {
            PictureType.Avatar => await _settingService.GetSettingByKeyAsync("Media.Customer.DefaultAvatarImageName", NopMediaDefaults.DefaultAvatarFileName),
            _ => await _settingService.GetSettingByKeyAsync("Media.DefaultImageName", NopMediaDefaults.DefaultImageFileName),
        };
        var filePath = await GetPictureLocalPathAsync(defaultImageFileName);
        if (!_fileProvider.FileExists(filePath))
        {
            return string.Empty;
        }

        if (targetSize == 0)
            return await GetImagesPathUrlAsync(storeLocation) + defaultImageFileName;

        var fileExtension = _fileProvider.GetFileExtension(filePath);
        var thumbFileName = $"{_fileProvider.GetFileNameWithoutExtension(filePath)}_{targetSize}{fileExtension}";
        var thumbFilePath = await GetThumbLocalPathAsync(thumbFileName);
        if (!await GeneratedThumbExistsAsync(thumbFilePath, thumbFileName))
        {
            //the named mutex helps to avoid creating the same files in different threads,
            //and does not decrease performance significantly, because the code is blocked only for the specific file.
            //you should be very careful, mutexes cannot be used in with the await operation
            //we can't use semaphore here, because it produces PlatformNotSupportedException exception on UNIX based systems
            using var mutex = new Mutex(false, thumbFileName);
            mutex.WaitOne();
            try
            {
                using var image = SKBitmap.Decode(filePath);
                var codec = SKCodec.Create(filePath);
                var format = codec.EncodedFormat;
                var pictureBinary = ImageResize(image, format, targetSize);
                var mimeType = GetMimeTypeFromFileName(thumbFileName);
                SaveThumbAsync(thumbFilePath, thumbFileName, mimeType, pictureBinary).Wait();
            }
            finally
            {
                mutex.ReleaseMutex();
            }
        }

        return await GetThumbUrlAsync(thumbFileName, storeLocation);
    }

    /// 
    /// Get a picture URL
    /// 
    /// Picture identifier
    /// The target picture size (longest side)
    /// A value indicating whether the default picture is shown
    /// Store location URL; null to use determine the current store location automatically
    /// Default picture type
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the picture URL
    /// 
    public virtual async Task GetPictureUrlAsync(int pictureId,
        int targetSize = 0,
        bool showDefaultPicture = true,
        string storeLocation = null,
        PictureType defaultPictureType = PictureType.Entity)
    {
        var picture = await GetPictureByIdAsync(pictureId);
        return (await GetPictureUrlAsync(picture, targetSize, showDefaultPicture, storeLocation, defaultPictureType)).Url;
    }

    /// 
    /// Get a picture URL
    /// 
    /// Reference instance of Picture
    /// The target picture size (longest side)
    /// A value indicating whether the default picture is shown
    /// Store location URL; null to use determine the current store location automatically
    /// Default picture type
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the picture URL
    /// 
    public virtual async Task<(string Url, Picture Picture)> GetPictureUrlAsync(Picture picture,
        int targetSize = 0,
        bool showDefaultPicture = true,
        string storeLocation = null,
        PictureType defaultPictureType = PictureType.Entity)
    {
        if (picture == null)
            return showDefaultPicture ? (await GetDefaultPictureUrlAsync(targetSize, defaultPictureType, storeLocation), null) : (string.Empty, (Picture)null);

        byte[] pictureBinary = null;
        if (picture.IsNew)
        {
            await DeletePictureThumbsAsync(picture);
            pictureBinary = await LoadPictureBinaryAsync(picture);

            if ((pictureBinary?.Length ?? 0) == 0)
                return showDefaultPicture ? (await GetDefaultPictureUrlAsync(targetSize, defaultPictureType, storeLocation), picture) : (string.Empty, picture);

            //we do not validate picture binary here to ensure that no exception ("Parameter is not valid") will be thrown
            picture = await UpdatePictureAsync(picture.Id,
                pictureBinary,
                picture.MimeType,
                picture.SeoFilename,
                picture.AltAttribute,
                picture.TitleAttribute,
                false,
                false);
        }

        var seoFileName = picture.SeoFilename; // = GetPictureSeName(picture.SeoFilename); //just for sure

        var lastPart = await GetFileExtensionFromMimeTypeAsync(picture.MimeType);

        var thumbFileName = !string.IsNullOrEmpty(seoFileName)
            ? $"{picture.Id:0000000}_{seoFileName}.{lastPart}"
            : $"{picture.Id:0000000}.{lastPart}";

        //there is no need to resize the svg image as the browser will take care of it
        if (targetSize == 0 || picture.MimeType == MimeTypes.ImageSvg)
        {
            var thumbFilePath = await GetThumbLocalPathAsync(thumbFileName);
            if (await GeneratedThumbExistsAsync(thumbFilePath, thumbFileName))
                return (await GetThumbUrlAsync(thumbFileName, storeLocation), picture);

            pictureBinary ??= await LoadPictureBinaryAsync(picture);

            //the named mutex helps to avoid creating the same files in different threads,
            //and does not decrease performance significantly, because the code is blocked only for the specific file.
            //you should be very careful, mutexes cannot be used in with the await operation
            //we can't use semaphore here, because it produces PlatformNotSupportedException exception on UNIX based systems
            using var mutex = new Mutex(false, thumbFileName);
            mutex.WaitOne();
            try
            {
                SaveThumbAsync(thumbFilePath, thumbFileName, picture.MimeType, pictureBinary).Wait();
            }
            finally
            {
                mutex.ReleaseMutex();
            }
        }
        else
        {
            thumbFileName = !string.IsNullOrEmpty(seoFileName)
                ? $"{picture.Id:0000000}_{seoFileName}_{targetSize}.{lastPart}"
                : $"{picture.Id:0000000}_{targetSize}.{lastPart}";

            var thumbFilePath = await GetThumbLocalPathAsync(thumbFileName);
            if (await GeneratedThumbExistsAsync(thumbFilePath, thumbFileName))
                return (await GetThumbUrlAsync(thumbFileName, storeLocation), picture);

            pictureBinary ??= await LoadPictureBinaryAsync(picture);

            //the named mutex helps to avoid creating the same files in different threads,
            //and does not decrease performance significantly, because the code is blocked only for the specific file.
            //you should be very careful, mutexes cannot be used in with the await operation
            //we can't use semaphore here, because it produces PlatformNotSupportedException exception on UNIX based systems
            using var mutex = new Mutex(false, thumbFileName);
            mutex.WaitOne();
            try
            {
                if (pictureBinary != null)
                    try
                    {
                        using var image = SKBitmap.Decode(pictureBinary);
                        var format = GetImageFormatByMimeType(picture.MimeType);
                        pictureBinary = ImageResize(image, format, targetSize);
                        SaveThumbAsync(thumbFilePath, thumbFileName, picture.MimeType, pictureBinary).Wait();
                    }
                    catch
                    {
                        // ignored
                    }
            }
            finally
            {
                mutex.ReleaseMutex();
            }
        }

        return (await GetThumbUrlAsync(thumbFileName, storeLocation), picture);
    }

    /// 
    /// Get a picture local path
    /// 
    /// Picture instance
    /// The target picture size (longest side)
    /// A value indicating whether the default picture is shown
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the 
    /// 
    public virtual async Task GetThumbLocalPathAsync(Picture picture, int targetSize = 0, bool showDefaultPicture = true)
    {
        var (url, _) = await GetPictureUrlAsync(picture, targetSize, showDefaultPicture);
        if (string.IsNullOrEmpty(url))
            return string.Empty;

        return await GetThumbLocalPathAsync(_fileProvider.GetFileName(url));
    }

    #endregion

    #region Convertation methods

    /// 
    /// Convert image from SVG format to PNG
    /// 
    /// Stream for SVG file
    /// A task that represents the asynchronous operation
    /// The task result contains the byte array
    public virtual Task ConvertSvgToPngAsync(Stream stream)
    {
        try
        {
            using var svg = new SKSvg();
            svg.Load(stream);

            using var bitmap = new SKBitmap((int)svg.Picture.CullRect.Width, (int)svg.Picture.CullRect.Height);
            var canvas = new SKCanvas(bitmap);
            canvas.DrawPicture(svg.Picture);
            canvas.Flush();
            canvas.Save();

            using var image = SKImage.FromBitmap(bitmap);
            using var data = image.Encode(SKEncodedImageFormat.Png, 100);

            // save the data to a stream
            using var memStream = new MemoryStream();
            data.SaveTo(memStream);
            memStream.Seek(0, SeekOrigin.Begin);

            return Task.FromResult(memStream.ToArray());
        }
        catch
        {
        }

        return null;
    }

    #endregion

    #region CRUD methods

    /// 
    /// Gets a picture
    /// 
    /// Picture identifier
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the picture
    /// 
    public virtual async Task GetPictureByIdAsync(int pictureId)
    {
        return await _pictureRepository.GetByIdAsync(pictureId, cache => default);
    }

    /// 
    /// Deletes a picture
    /// 
    /// Picture
    /// A task that represents the asynchronous operation
    public virtual async Task DeletePictureAsync(Picture picture)
    {
        ArgumentNullException.ThrowIfNull(picture);

        //delete thumbs
        await DeletePictureThumbsAsync(picture);

        //delete from file system
        if (!await IsStoreInDbAsync())
            await DeletePictureOnFileSystemAsync(picture);

        //delete from database
        await _pictureRepository.DeleteAsync(picture);
    }

    /// 
    /// Gets a collection of pictures
    /// 
    /// Virtual path
    /// Current page
    /// Items on each page
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the paged list of pictures
    /// 
    public virtual async Task> GetPicturesAsync(string virtualPath = "", int pageIndex = 0, int pageSize = int.MaxValue)
    {
        var query = _pictureRepository.Table;

        if (!string.IsNullOrEmpty(virtualPath))
            query = virtualPath.EndsWith('/') ? query.Where(p => p.VirtualPath.StartsWith(virtualPath) || p.VirtualPath == virtualPath.TrimEnd('/')) : query.Where(p => p.VirtualPath == virtualPath);

        query = query.OrderByDescending(p => p.Id);

        return await query.ToPagedListAsync(pageIndex, pageSize);
    }

    /// 
    /// Gets pictures by product identifier
    /// 
    /// Product identifier
    /// Number of records to return. 0 if you want to get all items
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the pictures
    /// 
    public virtual async Task> GetPicturesByProductIdAsync(int productId, int recordsToReturn = 0)
    {
        if (productId == 0)
            return new List();

        var query = from p in _pictureRepository.Table
            join pp in _productPictureRepository.Table on p.Id equals pp.PictureId
            orderby pp.DisplayOrder, pp.Id
            where pp.ProductId == productId
            select p;

        if (recordsToReturn > 0)
            query = query.Take(recordsToReturn);

        var pics = await query.ToListAsync();

        return pics;
    }

    /// 
    /// Inserts a picture
    /// 
    /// The picture binary
    /// The picture MIME type
    /// The SEO filename
    /// "alt" attribute for "img" HTML element
    /// "title" attribute for "img" HTML element
    /// A value indicating whether the picture is new
    /// A value indicating whether to validated provided picture binary
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the picture
    /// 
    public virtual async Task InsertPictureAsync(byte[] pictureBinary, string mimeType, string seoFilename,
        string altAttribute = null, string titleAttribute = null,
        bool isNew = true, bool validateBinary = true)
    {
        mimeType = CommonHelper.EnsureNotNull(mimeType);
        mimeType = CommonHelper.EnsureMaximumLength(mimeType, 20);

        seoFilename = CommonHelper.EnsureMaximumLength(seoFilename, 100);

        if (validateBinary)
            pictureBinary = await ValidatePictureAsync(pictureBinary, mimeType, seoFilename);

        var picture = new Picture
        {
            MimeType = mimeType,
            SeoFilename = seoFilename,
            AltAttribute = altAttribute,
            TitleAttribute = titleAttribute,
            IsNew = isNew
        };
        await _pictureRepository.InsertAsync(picture);
        await UpdatePictureBinaryAsync(picture, await IsStoreInDbAsync() ? pictureBinary : Array.Empty());

        if (!await IsStoreInDbAsync())
            await SavePictureInFileAsync(picture.Id, pictureBinary, mimeType);

        return picture;
    }

    /// 
    /// Inserts a picture
    /// 
    /// Form file
    /// File name which will be use if IFormFile.FileName not present
    /// Virtual path
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the picture
    /// 
    public virtual async Task InsertPictureAsync(IFormFile formFile, string defaultFileName = "", string virtualPath = "")
    {
        var imgExt = new List
        {
            ".bmp",
            ".gif",
            ".webp",
            ".jpeg",
            ".jpg",
            ".jpe",
            ".jfif",
            ".pjpeg",
            ".pjp",
            ".png",
            ".tiff",
            ".tif",
            ".svg"
        } as IReadOnlyCollection;

        var fileName = formFile.FileName;
        if (string.IsNullOrEmpty(fileName) && !string.IsNullOrEmpty(defaultFileName))
            fileName = defaultFileName;

        //remove path (passed in IE)
        fileName = _fileProvider.GetFileName(fileName);

        var contentType = formFile.ContentType;

        var fileExtension = _fileProvider.GetFileExtension(fileName);
        if (!string.IsNullOrEmpty(fileExtension))
            fileExtension = fileExtension.ToLowerInvariant();

        if (imgExt.All(ext => !ext.Equals(fileExtension, StringComparison.CurrentCultureIgnoreCase)))
            return null;

        //contentType is not always available 
        //that's why we manually update it here
        //https://mimetype.io/all-types/
        if (string.IsNullOrEmpty(contentType))
            contentType = GetPictureContentTypeByFileExtension(fileExtension);

        if (contentType == MimeTypes.ImageSvg && !_mediaSettings.AllowSVGUploads)
            return null;

        var picture = await InsertPictureAsync(await _downloadService.GetDownloadBitsAsync(formFile),
            contentType,
            _fileProvider.GetFileNameWithoutExtension(fileName),
            validateBinary: contentType != MimeTypes.ImageSvg);

        if (string.IsNullOrEmpty(virtualPath))
            return picture;

        picture.VirtualPath = _fileProvider.GetVirtualPath(virtualPath);
        await UpdatePictureAsync(picture);

        return picture;
    }

    /// 
    /// Updates the picture
    /// 
    /// The picture identifier
    /// The picture binary
    /// The picture MIME type
    /// The SEO filename
    /// "alt" attribute for "img" HTML element
    /// "title" attribute for "img" HTML element
    /// A value indicating whether the picture is new
    /// A value indicating whether to validated provided picture binary
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the picture
    /// 
    public virtual async Task UpdatePictureAsync(int pictureId, byte[] pictureBinary, string mimeType,
        string seoFilename, string altAttribute = null, string titleAttribute = null,
        bool isNew = true, bool validateBinary = true)
    {
        mimeType = CommonHelper.EnsureNotNull(mimeType);
        mimeType = CommonHelper.EnsureMaximumLength(mimeType, 20);

        seoFilename = CommonHelper.EnsureMaximumLength(seoFilename, 100);

        if (validateBinary)
            pictureBinary = await ValidatePictureAsync(pictureBinary, mimeType, seoFilename);

        var picture = await GetPictureByIdAsync(pictureId);
        if (picture == null)
            return null;

        //delete old thumbs if a picture has been changed
        if (seoFilename != picture.SeoFilename)
            await DeletePictureThumbsAsync(picture);

        picture.MimeType = mimeType;
        picture.SeoFilename = seoFilename;
        picture.AltAttribute = altAttribute;
        picture.TitleAttribute = titleAttribute;
        picture.IsNew = isNew;

        await _pictureRepository.UpdateAsync(picture);
        await UpdatePictureBinaryAsync(picture, await IsStoreInDbAsync() ? pictureBinary : Array.Empty());

        if (!await IsStoreInDbAsync())
            await SavePictureInFileAsync(picture.Id, pictureBinary, mimeType);

        return picture;
    }

    /// 
    /// Updates the picture
    /// 
    /// The picture to update
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the picture
    /// 
    public virtual async Task UpdatePictureAsync(Picture picture)
    {
        if (picture == null)
            return null;

        var seoFilename = CommonHelper.EnsureMaximumLength(picture.SeoFilename, 100);

        //delete old thumbs if exists
        await DeletePictureThumbsAsync(picture);

        picture.SeoFilename = seoFilename;

        await _pictureRepository.UpdateAsync(picture);
        await UpdatePictureBinaryAsync(picture, await IsStoreInDbAsync() ? (await GetPictureBinaryByPictureIdAsync(picture.Id)).BinaryData : Array.Empty());

        if (!await IsStoreInDbAsync())
            await SavePictureInFileAsync(picture.Id, (await GetPictureBinaryByPictureIdAsync(picture.Id)).BinaryData, picture.MimeType);

        return picture;
    }

    /// 
    /// Get product picture binary by picture identifier
    /// 
    /// The picture identifier
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the picture binary
    /// 
    public virtual async Task GetPictureBinaryByPictureIdAsync(int pictureId)
    {
        return await _pictureBinaryRepository.Table
            .FirstOrDefaultAsync(pb => pb.PictureId == pictureId);
    }

    /// 
    /// Updates a SEO filename of a picture
    /// 
    /// The picture identifier
    /// The SEO filename
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the picture
    /// 
    public virtual async Task SetSeoFilenameAsync(int pictureId, string seoFilename)
    {
        var picture = await GetPictureByIdAsync(pictureId) ?? throw new ArgumentException("No picture found with the specified id");

        //update if it has been changed
        if (seoFilename != picture.SeoFilename)
        {
            //update picture
            picture = await UpdatePictureAsync(picture.Id,
                await LoadPictureBinaryAsync(picture),
                picture.MimeType,
                seoFilename,
                picture.AltAttribute,
                picture.TitleAttribute,
                true,
                false);
        }

        return picture;
    }

    /// 
    /// Validates input picture dimensions
    /// 
    /// Picture binary
    /// MIME type
    /// Name of file
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the picture binary or throws an exception
    /// 
    public virtual async Task ValidatePictureAsync(byte[] pictureBinary, string mimeType, string fileName)
    {
        try
        {
            using var image = SKBitmap.Decode(pictureBinary);

            //resize the image in accordance with the maximum size
            if (Math.Max(image.Height, image.Width) > _mediaSettings.MaximumImageSize)
            {
                var format = GetImageFormatByMimeType(mimeType);
                pictureBinary = ImageResize(image, format, _mediaSettings.MaximumImageSize);
            }
            return pictureBinary;
        }
        catch (Exception exc)
        {
            await _logger.ErrorAsync($"Cannot decode picture binary (file name: {fileName})", exc);
            return pictureBinary;
        }
    }

    /// 
    /// Get product picture (for shopping cart and order details pages)
    /// 
    /// Product
    /// Attributes (in XML format)
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the picture
    /// 
    public virtual async Task GetProductPictureAsync(Product product, string attributesXml)
    {
        ArgumentNullException.ThrowIfNull(product);

        //first, try to get product attribute combination picture
        var combination = await _productAttributeParser.FindProductAttributeCombinationAsync(product, attributesXml);
        if (combination != null)
        {
            var combinationPicture = (await _productAttributeService.GetProductAttributeCombinationPicturesAsync(combination.Id)).FirstOrDefault();
            if (await GetPictureByIdAsync(combinationPicture?.PictureId ?? 0) is Picture picture)
                return picture;
        }

        //then, let's see whether we have attribute values with pictures
        var values = await _productAttributeParser.ParseProductAttributeValuesAsync(attributesXml);
        foreach (var attributeValue in values)
        {
            var valuePictures = await _productAttributeService.GetProductAttributeValuePicturesAsync(attributeValue.Id);
            var attributePicture = (await GetPicturesByIdsAsync(valuePictures.Select(vp => vp.PictureId).ToArray())).FirstOrDefault();

            if (attributePicture != null)
                return attributePicture;
        }

        //now let's load the default product picture
        var productPicture = (await GetPicturesByProductIdAsync(product.Id, 1)).FirstOrDefault();
        if (productPicture != null)
            return productPicture;

        //finally, let's check whether this product has some parent "grouped" product
        if (product.VisibleIndividually || product.ParentGroupedProductId <= 0)
            return null;

        var parentGroupedProductPicture = (await GetPicturesByProductIdAsync(product.ParentGroupedProductId, 1)).FirstOrDefault();
        return parentGroupedProductPicture;
    }

    /// 
    /// Gets a value indicating whether the images should be stored in data base.
    /// 
    /// A task that represents the asynchronous operation
    public virtual async Task IsStoreInDbAsync()
    {
        return await _settingService.GetSettingByKeyAsync("Media.Images.StoreInDB", true);
    }

    /// 
    /// Sets a value indicating whether the images should be stored in data base
    /// 
    /// A value indicating whether the images should be stored in data base
    /// A task that represents the asynchronous operation
    public virtual async Task SetIsStoreInDbAsync(bool isStoreInDb)
    {
        //check whether it's a new value
        if (await IsStoreInDbAsync() == isStoreInDb)
            return;

        //save the new setting value
        await _settingService.SetSettingAsync("Media.Images.StoreInDB", isStoreInDb);

        var pageIndex = 0;
        const int pageSize = 400;
        try
        {
            while (true)
            {
                var pictures = await GetPicturesAsync(pageIndex: pageIndex, pageSize: pageSize);
                pageIndex++;

                //all pictures converted?
                if (!pictures.Any())
                    break;

                foreach (var picture in pictures)
                {
                    if (!string.IsNullOrEmpty(picture.VirtualPath))
                        continue;

                    var pictureBinary = await LoadPictureBinaryAsync(picture, !isStoreInDb);

                    //we used the code below before. but it's too slow
                    //let's do it manually (uncommented code) - copy some logic from "UpdatePicture" method
                    /*just update a picture (all required logic is in "UpdatePicture" method)
                    we do not validate picture binary here to ensure that no exception ("Parameter is not valid") will be thrown when "moving" pictures
                    UpdatePicture(picture.Id,
                                  pictureBinary,
                                  picture.MimeType,
                                  picture.SeoFilename,
                                  true,
                                  false);*/
                    if (isStoreInDb)
                        //delete from file system. now it's in the database
                        await DeletePictureOnFileSystemAsync(picture);
                    else
                        //now on file system
                        await SavePictureInFileAsync(picture.Id, pictureBinary, picture.MimeType);
                    //update appropriate properties
                    await UpdatePictureBinaryAsync(picture, isStoreInDb ? pictureBinary : Array.Empty());
                    picture.IsNew = true;
                }

                //save all at once
                await _pictureRepository.UpdateAsync(pictures, false);
            }
        }
        catch
        {
            // ignored
        }
    }

    #endregion

    #region Common methods

    /// 
    /// Get content type for picture by file extension
    /// 
    /// The file extension
    /// Picture's content type
    public string GetPictureContentTypeByFileExtension(string fileExtension)
    {
        string contentType = null;

        switch (fileExtension.ToLower())
        {
            case ".bmp":
                contentType = MimeTypes.ImageBmp;
                break;
            case ".gif":
                contentType = MimeTypes.ImageGif;
                break;
            case ".jpeg":
            case ".jpg":
            case ".jpe":
            case ".jfif":
            case ".pjpeg":
            case ".pjp":
                contentType = MimeTypes.ImageJpeg;
                break;
            case ".webp":
                contentType = MimeTypes.ImageWebp;
                break;
            case ".png":
                contentType = MimeTypes.ImagePng;
                break;
            case ".svg":
                contentType = MimeTypes.ImageSvg;
                break;
            case ".tiff":
            case ".tif":
                contentType = MimeTypes.ImageTiff;
                break;
            default:
                break;
        }

        return contentType;
    }

    #endregion
}