DocSpace-buildtools/web/ASC.Web.Core/Users/UserPhotoManager.cs
2019-10-31 14:28:30 +03:00

1077 lines
42 KiB
C#

/*
*
* (c) Copyright Ascensio System Limited 2010-2018
*
* This program is freeware. You can redistribute it and/or modify it under the terms of the GNU
* General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html).
* In accordance with Section 7(a) of the GNU GPL its Section 15 shall be amended to the effect that
* Ascensio System SIA expressly excludes the warranty of non-infringement of any third-party rights.
*
* THIS PROGRAM IS DISTRIBUTED WITHOUT ANY WARRANTY; WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR
* FITNESS FOR A PARTICULAR PURPOSE. For more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html
*
* You can contact Ascensio System SIA by email at sales@onlyoffice.com
*
* The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display
* Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3.
*
* Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains
* relevant author attributions when distributing the software. If the display of the logo in its graphic
* form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE"
* in every copy of the program you distribute.
* Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks.
*
*/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using ASC.Common.Caching;
using ASC.Common.Logging;
using ASC.Common.Threading.Workers;
using ASC.Core;
using ASC.Core.Tenants;
using ASC.Data.Storage;
using ASC.Web.Core.Utility.Skins;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
namespace ASC.Web.Core.Users
{
public class ResizeWorkerItem
{
public ResizeWorkerItem(Guid userId, byte[] data, long maxFileSize, Size size, IDataStore dataStore, UserPhotoThumbnailSettings settings)
{
UserId = userId;
Data = data;
MaxFileSize = maxFileSize;
Size = size;
DataStore = dataStore;
Settings = settings;
}
public Size Size { get; }
public IDataStore DataStore { get; }
public long MaxFileSize { get; }
public byte[] Data { get; }
public Guid UserId { get; }
public UserPhotoThumbnailSettings Settings { get; }
public override bool Equals(object obj)
{
if (obj is null) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != typeof(ResizeWorkerItem)) return false;
return Equals((ResizeWorkerItem)obj);
}
public bool Equals(ResizeWorkerItem other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
return other.UserId.Equals(UserId) && other.MaxFileSize == MaxFileSize && other.Size.Equals(Size);
}
public override int GetHashCode()
{
unchecked
{
var result = UserId.GetHashCode();
result = (result * 397) ^ MaxFileSize.GetHashCode();
result = (result * 397) ^ Size.GetHashCode();
return result;
}
}
}
public class UserPhotoManagerCache
{
private readonly ConcurrentDictionary<CacheSize, ConcurrentDictionary<Guid, string>> Photofiles;
private readonly ICacheNotify<UserPhotoManagerCacheItem> CacheNotify;
private readonly HashSet<int> TenantDiskCache;
public UserPhotoManagerCache(ICacheNotify<UserPhotoManagerCacheItem> notify)
{
try
{
Photofiles = new ConcurrentDictionary<CacheSize, ConcurrentDictionary<Guid, string>>();
TenantDiskCache = new HashSet<int>();
CacheNotify = notify;
CacheNotify.Subscribe((data) =>
{
var userId = new Guid(data.UserID.ToByteArray());
Photofiles.GetOrAdd(data.Size, (r) => new ConcurrentDictionary<Guid, string>())[userId] = data.FileName;
}, CacheNotifyAction.InsertOrUpdate);
CacheNotify.Subscribe((data) =>
{
var userId = new Guid(data.UserID.ToByteArray());
try
{
foreach (var s in (CacheSize[])Enum.GetValues(typeof(CacheSize)))
{
Photofiles.TryGetValue(s, out var dict);
dict?.TryRemove(userId, out _);
}
SetCacheLoadedForTenant(false, data.TenantId);
}
catch { }
}, CacheNotifyAction.Remove);
}
catch (Exception)
{
}
}
public bool IsCacheLoadedForTenant(int tenantId)
{
return TenantDiskCache.Contains(tenantId);
}
public bool SetCacheLoadedForTenant(bool isLoaded, int tenantId)
{
return isLoaded ? TenantDiskCache.Add(tenantId) : TenantDiskCache.Remove(tenantId);
}
public void ClearCache(Guid userID)
{
if (CacheNotify != null)
{
CacheNotify.Publish(new UserPhotoManagerCacheItem { UserID = Google.Protobuf.ByteString.CopyFrom(userID.ToByteArray()) }, CacheNotifyAction.Remove);
}
}
public void AddToCache(Guid userID, Size size, string fileName)
{
if (CacheNotify != null)
{
CacheNotify.Publish(new UserPhotoManagerCacheItem { UserID = Google.Protobuf.ByteString.CopyFrom(userID.ToByteArray()), Size = UserPhotoManager.ToCache(size), FileName = fileName }, CacheNotifyAction.InsertOrUpdate);
}
}
public string SearchInCache(Guid userId, Size size)
{
string fileName = null;
Photofiles.TryGetValue(UserPhotoManager.ToCache(size), out var photo);
if (size != Size.Empty)
{
photo?.TryGetValue(userId, out fileName);
}
else
{
fileName = photo?
.Select(x => x.Value)
.FirstOrDefault(x => !string.IsNullOrEmpty(x) && x.Contains("_orig_"));
}
return fileName;
}
}
public class UserPhotoManager
{
//Regex for parsing filenames into groups with id's
private static readonly Regex ParseFile =
new Regex(@"^(?'module'\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1}){0,1}" +
@"(?'user'\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1}){1}" +
@"_(?'kind'orig|size){1}_(?'size'(?'width'[0-9]{1,5})-{1}(?'height'[0-9]{1,5})){0,1}\..*", RegexOptions.Compiled);
private static readonly ConcurrentDictionary<CacheSize, ConcurrentDictionary<Guid, string>> Photofiles = new ConcurrentDictionary<CacheSize, ConcurrentDictionary<Guid, string>>();
public UserManager UserManager { get; }
public WebImageSupplier WebImageSupplier { get; }
public UserPhotoThumbnailSettings UserPhotoThumbnailSettings { get; }
public TenantManager TenantManager { get; }
public StorageFactory StorageFactory { get; }
public UserPhotoManagerCache UserPhotoManagerCache { get; }
public ILog Log { get; }
private Tenant tenant;
public Tenant Tenant { get { return tenant ?? (tenant = TenantManager.GetCurrentTenant()); } }
//note: using auto stop queue
private readonly WorkerQueue<ResizeWorkerItem> ResizeQueue;//TODO: configure
public UserPhotoManager(
UserManager userManager,
WebImageSupplier webImageSupplier,
UserPhotoThumbnailSettings userPhotoThumbnailSettings,
TenantManager tenantManager,
StorageFactory storageFactory,
UserPhotoManagerCache userPhotoManagerCache,
IOptionsMonitor<LogNLog> options,
WorkerQueueOptionsManager<ResizeWorkerItem> optionsQueue)
{
ResizeQueue = optionsQueue.Value;
UserManager = userManager;
WebImageSupplier = webImageSupplier;
UserPhotoThumbnailSettings = userPhotoThumbnailSettings;
TenantManager = tenantManager;
StorageFactory = storageFactory;
UserPhotoManagerCache = userPhotoManagerCache;
Log = options.Get("ASC.Web.Photo");
}
public string GetDefaultPhotoAbsoluteWebPath()
{
return WebImageSupplier.GetAbsoluteWebPath(_defaultAvatar);
}
public string GetRetinaPhotoURL(Guid userID)
{
return GetRetinaPhotoURL(userID, out _);
}
public string GetRetinaPhotoURL(Guid userID, out bool isdef)
{
return GetSizedPhotoAbsoluteWebPath(userID, RetinaFotoSize, out isdef);
}
public string GetMaxPhotoURL(Guid userID)
{
return GetMaxPhotoURL(userID, out _);
}
public string GetMaxPhotoURL(Guid userID, out bool isdef)
{
return GetSizedPhotoAbsoluteWebPath(userID, MaxFotoSize, out isdef);
}
public string GetBigPhotoURL(Guid userID)
{
return GetBigPhotoURL(userID, out _);
}
public string GetBigPhotoURL(Guid userID, out bool isdef)
{
return GetSizedPhotoAbsoluteWebPath(userID, BigFotoSize, out isdef);
}
public string GetMediumPhotoURL(Guid userID)
{
return GetMediumPhotoURL(userID, out _);
}
public string GetMediumPhotoURL(Guid userID, out bool isdef)
{
return GetSizedPhotoAbsoluteWebPath(userID, MediumFotoSize, out isdef);
}
public string GetSmallPhotoURL(Guid userID)
{
return GetSmallPhotoURL(userID, out _);
}
public string GetSmallPhotoURL(Guid userID, out bool isdef)
{
return GetSizedPhotoAbsoluteWebPath(userID, SmallFotoSize, out isdef);
}
public string GetSizedPhotoUrl(Guid userId, int width, int height)
{
return GetSizedPhotoAbsoluteWebPath(userId, new Size(width, height));
}
public string GetDefaultSmallPhotoURL()
{
return GetDefaultPhotoAbsoluteWebPath(SmallFotoSize);
}
public string GetDefaultMediumPhotoURL()
{
return GetDefaultPhotoAbsoluteWebPath(MediumFotoSize);
}
public string GetDefaultBigPhotoURL()
{
return GetDefaultPhotoAbsoluteWebPath(BigFotoSize);
}
public string GetDefaultMaxPhotoURL()
{
return GetDefaultPhotoAbsoluteWebPath(MaxFotoSize);
}
public string GetDefaultRetinaPhotoURL()
{
return GetDefaultPhotoAbsoluteWebPath(RetinaFotoSize);
}
public static Size OriginalFotoSize { get; } = new Size(1280, 1280);
public static Size RetinaFotoSize { get; } = new Size(360, 360);
public static Size MaxFotoSize { get; } = new Size(200, 200);
public static Size BigFotoSize { get; } = new Size(82, 82);
public static Size MediumFotoSize { get; } = new Size(48, 48);
public static Size SmallFotoSize { get; } = new Size(32, 32);
private static readonly string _defaultRetinaAvatar = "default_user_photo_size_360-360.png";
private static readonly string _defaultAvatar = "default_user_photo_size_200-200.png";
private static readonly string _defaultSmallAvatar = "default_user_photo_size_32-32.png";
private static readonly string _defaultMediumAvatar = "default_user_photo_size_48-48.png";
private static readonly string _defaultBigAvatar = "default_user_photo_size_82-82.png";
private static readonly string _tempDomainName = "temp";
public bool UserHasAvatar(Guid userID)
{
var path = GetPhotoAbsoluteWebPath(userID);
var fileName = Path.GetFileName(path);
return fileName != _defaultAvatar;
}
public string GetPhotoAbsoluteWebPath(Guid userID)
{
var path = SearchInCache(userID, Size.Empty, out _);
if (!string.IsNullOrEmpty(path)) return path;
try
{
var data = UserManager.GetUserPhoto(userID);
string photoUrl;
string fileName;
if (data == null || data.Length == 0)
{
photoUrl = GetDefaultPhotoAbsoluteWebPath();
fileName = "default";
}
else
{
photoUrl = SaveOrUpdatePhoto(userID, data, -1, new Size(-1, -1), false, out fileName);
}
UserPhotoManagerCache.AddToCache(userID, Size.Empty, fileName);
return photoUrl;
}
catch
{
}
return GetDefaultPhotoAbsoluteWebPath();
}
internal Size GetPhotoSize(Guid userID)
{
var virtualPath = GetPhotoAbsoluteWebPath(userID);
if (virtualPath == null) return Size.Empty;
try
{
var sizePart = virtualPath.Substring(virtualPath.LastIndexOf('_'));
sizePart = sizePart.Trim('_');
sizePart = sizePart.Remove(sizePart.LastIndexOf('.'));
return new Size(int.Parse(sizePart.Split('-')[0]), int.Parse(sizePart.Split('-')[1]));
}
catch
{
return Size.Empty;
}
}
private string GetSizedPhotoAbsoluteWebPath(Guid userID, Size size)
{
return GetSizedPhotoAbsoluteWebPath(userID, size, out _);
}
private string GetSizedPhotoAbsoluteWebPath(Guid userID, Size size, out bool isdef)
{
var res = SearchInCache(userID, size, out isdef);
if (!string.IsNullOrEmpty(res)) return res;
try
{
var data = UserManager.GetUserPhoto(userID);
if (data == null || data.Length == 0)
{
//empty photo. cache default
var photoUrl = GetDefaultPhotoAbsoluteWebPath(size);
UserPhotoManagerCache.AddToCache(userID, size, "default");
isdef = true;
return photoUrl;
}
//Enqueue for sizing
SizePhoto(userID, data, -1, size);
}
catch { }
isdef = false;
return GetDefaultPhotoAbsoluteWebPath(size);
}
private string GetDefaultPhotoAbsoluteWebPath(Size size) =>
size switch
{
Size(var w, var h) when w == RetinaFotoSize.Width && h == RetinaFotoSize.Height => WebImageSupplier.GetAbsoluteWebPath(_defaultRetinaAvatar),
Size(var w, var h) when w == MaxFotoSize.Width && h == MaxFotoSize.Height => WebImageSupplier.GetAbsoluteWebPath(_defaultAvatar),
Size(var w, var h) when w == BigFotoSize.Width && h == BigFotoSize.Height => WebImageSupplier.GetAbsoluteWebPath(_defaultBigAvatar),
Size(var w, var h) when w == SmallFotoSize.Width && h == SmallFotoSize.Height => WebImageSupplier.GetAbsoluteWebPath(_defaultSmallAvatar),
Size(var w, var h) when w == MediumFotoSize.Width && h == MediumFotoSize.Height => WebImageSupplier.GetAbsoluteWebPath(_defaultMediumAvatar),
_ => GetDefaultPhotoAbsoluteWebPath()
};
private static readonly HashSet<int> TenantDiskCache = new HashSet<int>();
private static readonly object DiskCacheLoaderLock = new object();
private string SearchInCache(Guid userId, Size size, out bool isDef)
{
if (!UserPhotoManagerCache.IsCacheLoadedForTenant(Tenant.TenantId))
LoadDiskCache();
isDef = false;
var fileName = UserPhotoManagerCache.SearchInCache(userId, size);
if (fileName != null && fileName.StartsWith("default"))
{
isDef = true;
return GetDefaultPhotoAbsoluteWebPath(size);
}
if (!string.IsNullOrEmpty(fileName))
{
var store = GetDataStore();
return store.GetUri(fileName).ToString();
}
return null;
}
private void LoadDiskCache()
{
lock (DiskCacheLoaderLock)
{
if (!UserPhotoManagerCache.IsCacheLoadedForTenant(Tenant.TenantId))
{
try
{
var listFileNames = GetDataStore().ListFilesRelative("", "", "*.*", false);
foreach (var fileName in listFileNames)
{
//Try parse fileName
if (fileName != null)
{
var match = ParseFile.Match(fileName);
if (match.Success && match.Groups["user"].Success)
{
var parsedUserId = new Guid(match.Groups["user"].Value);
var size = Size.Empty;
if (match.Groups["width"].Success && match.Groups["height"].Success)
{
//Parse size
size = new Size(int.Parse(match.Groups["width"].Value), int.Parse(match.Groups["height"].Value));
}
UserPhotoManagerCache.AddToCache(parsedUserId, size, fileName);
}
}
}
UserPhotoManagerCache.SetCacheLoadedForTenant(true, Tenant.TenantId);
}
catch (Exception err)
{
Log.Error(err);
}
}
}
}
public static void ResetThumbnailSettings(Guid userId)
{
var thumbSettings = new UserPhotoThumbnailSettings().GetDefault() as UserPhotoThumbnailSettings;
thumbSettings.SaveForUser(userId);
}
public string SaveOrUpdatePhoto(Guid userID, byte[] data)
{
return SaveOrUpdatePhoto(userID, data, -1, OriginalFotoSize, true, out _);
}
public string SaveOrUpdateCroppedPhoto(Guid userID, byte[] data, byte[] defaultData)
{
return SaveOrUpdateCroppedPhoto(userID, data, defaultData, -1, OriginalFotoSize, true, out _);
}
public void RemovePhoto(Guid idUser)
{
UserManager.SaveUserPhoto(idUser, null);
var storage = GetDataStore();
storage.DeleteFiles("", idUser + "*.*", false);
UserManager.SaveUserPhoto(idUser, null);
UserPhotoManagerCache.ClearCache(idUser);
}
private string SaveOrUpdatePhoto(Guid userID, byte[] data, long maxFileSize, Size size, bool saveInCoreContext, out string fileName)
{
data = TryParseImage(data, maxFileSize, size, out var imgFormat, out var width, out var height);
var widening = CommonPhotoManager.GetImgFormatName(imgFormat);
fileName = string.Format("{0}_orig_{1}-{2}.{3}", userID, width, height, widening);
if (saveInCoreContext)
{
UserManager.SaveUserPhoto(userID, data);
SetUserPhotoThumbnailSettings(userID, width, height);
UserPhotoManagerCache.ClearCache(userID);
}
var store = GetDataStore();
var photoUrl = GetDefaultPhotoAbsoluteWebPath();
if (data != null && data.Length > 0)
{
using (var stream = new MemoryStream(data))
{
photoUrl = store.Save(fileName, stream).ToString();
}
//Queue resizing
SizePhoto(userID, data, -1, SmallFotoSize, true);
SizePhoto(userID, data, -1, MediumFotoSize, true);
SizePhoto(userID, data, -1, BigFotoSize, true);
SizePhoto(userID, data, -1, MaxFotoSize, true);
SizePhoto(userID, data, -1, RetinaFotoSize, true);
}
return photoUrl;
}
private string SaveOrUpdateCroppedPhoto(Guid userID, byte[] data, byte[] defaultData, long maxFileSize, Size size, bool saveInCoreContext, out string fileName)
{
data = TryParseImage(data, maxFileSize, size, out var imgFormat, out var width, out var height);
var widening = CommonPhotoManager.GetImgFormatName(imgFormat);
fileName = string.Format("{0}_orig_{1}-{2}.{3}", userID, width, height, widening);
if (saveInCoreContext)
{
UserManager.SaveUserPhoto(userID, defaultData);
var max = Math.Max(Math.Max(width, height), SmallFotoSize.Width);
var min = Math.Max(Math.Min(width, height), SmallFotoSize.Width);
var pos = (max - min) / 2;
var settings = new UserPhotoThumbnailSettings(
width >= height ? new Point(pos, 0) : new Point(0, pos),
new Size(min, min));
settings.SaveForUser(userID);
UserPhotoManagerCache.ClearCache(userID);
}
var store = GetDataStore();
var photoUrl = GetDefaultPhotoAbsoluteWebPath();
if (data != null && data.Length > 0)
{
using (var stream = new MemoryStream(data))
{
photoUrl = store.Save(fileName, stream).ToString();
}
//Queue resizing
SizePhoto(userID, data, -1, SmallFotoSize, true);
SizePhoto(userID, data, -1, MediumFotoSize, true);
SizePhoto(userID, data, -1, BigFotoSize, true);
SizePhoto(userID, data, -1, MaxFotoSize, true);
SizePhoto(userID, data, -1, RetinaFotoSize, true);
}
return photoUrl;
}
private void SetUserPhotoThumbnailSettings(Guid userId, int width, int height)
{
var settings = UserPhotoThumbnailSettings.LoadForUser(userId);
if (!settings.IsDefault) return;
var max = Math.Max(Math.Max(width, height), SmallFotoSize.Width);
var min = Math.Max(Math.Min(width, height), SmallFotoSize.Width);
var pos = (max - min) / 2;
settings = new UserPhotoThumbnailSettings(
width >= height ? new Point(pos, 0) : new Point(0, pos),
new Size(min, min));
settings.SaveForUser(userId);
}
private byte[] TryParseImage(byte[] data, long maxFileSize, Size maxsize, out ImageFormat imgFormat, out int width, out int height)
{
if (data == null || data.Length <= 0) throw new UnknownImageFormatException();
if (maxFileSize != -1 && data.Length > maxFileSize) throw new ImageSizeLimitException();
data = ImageHelper.RotateImageByExifOrientationData(data, Log);
try
{
using var stream = new MemoryStream(data);
using var img = new Bitmap(stream);
imgFormat = img.RawFormat;
width = img.Width;
height = img.Height;
var maxWidth = maxsize.Width;
var maxHeight = maxsize.Height;
if ((maxHeight != -1 && img.Height > maxHeight) || (maxWidth != -1 && img.Width > maxWidth))
{
#region calulate height and width
if (width > maxWidth && height > maxHeight)
{
if (width > height)
{
height = (int)((double)height * (double)maxWidth / (double)width + 0.5);
width = maxWidth;
}
else
{
width = (int)((double)width * (double)maxHeight / (double)height + 0.5);
height = maxHeight;
}
}
if (width > maxWidth && height <= maxHeight)
{
height = (int)((double)height * (double)maxWidth / (double)width + 0.5);
width = maxWidth;
}
if (width <= maxWidth && height > maxHeight)
{
width = (int)((double)width * (double)maxHeight / (double)height + 0.5);
height = maxHeight;
}
#endregion
using var b = new Bitmap(width, height);
using var gTemp = Graphics.FromImage(b);
gTemp.InterpolationMode = InterpolationMode.HighQualityBicubic;
gTemp.PixelOffsetMode = PixelOffsetMode.HighQuality;
gTemp.SmoothingMode = SmoothingMode.HighQuality;
gTemp.DrawImage(img, 0, 0, width, height);
data = CommonPhotoManager.SaveToBytes(b);
}
return data;
}
catch (OutOfMemoryException)
{
throw new ImageSizeLimitException();
}
catch (ArgumentException error)
{
throw new UnknownImageFormatException(error);
}
}
private string SizePhoto(Guid userID, byte[] data, long maxFileSize, Size size)
{
return SizePhoto(userID, data, maxFileSize, size, false);
}
private string SizePhoto(Guid userID, byte[] data, long maxFileSize, Size size, bool now)
{
if (data == null || data.Length <= 0) throw new UnknownImageFormatException();
if (maxFileSize != -1 && data.Length > maxFileSize) throw new ImageWeightLimitException();
var resizeTask = new ResizeWorkerItem(userID, data, maxFileSize, size, GetDataStore(), UserPhotoThumbnailSettings.LoadForUser(userID));
if (now)
{
//Resize synchronously
ResizeImage(resizeTask);
return GetSizedPhotoAbsoluteWebPath(userID, size);
}
else
{
if (!ResizeQueue.GetItems().Contains(resizeTask))
{
//Add
ResizeQueue.Add(resizeTask);
if (!ResizeQueue.IsStarted)
{
ResizeQueue.Start(ResizeImage);
}
}
return GetDefaultPhotoAbsoluteWebPath(size);
//NOTE: return default photo here. Since task will update cache
}
}
private void ResizeImage(ResizeWorkerItem item)
{
try
{
var data = item.Data;
using var stream = new MemoryStream(data);
using var img = Image.FromStream(stream);
var imgFormat = img.RawFormat;
if (item.Size != img.Size)
{
using var img2 = item.Settings.IsDefault ?
CommonPhotoManager.DoThumbnail(img, item.Size, true, true, true) :
UserPhotoThumbnailManager.GetBitmap(img, item.Size, item.Settings);
data = CommonPhotoManager.SaveToBytes(img2);
}
else
{
data = CommonPhotoManager.SaveToBytes(img);
}
var widening = CommonPhotoManager.GetImgFormatName(imgFormat);
var fileName = string.Format("{0}_size_{1}-{2}.{3}", item.UserId, item.Size.Width, item.Size.Height, widening);
using var stream2 = new MemoryStream(data);
item.DataStore.Save(fileName, stream2).ToString();
UserPhotoManagerCache.AddToCache(item.UserId, item.Size, fileName);
}
catch (ArgumentException error)
{
throw new UnknownImageFormatException(error);
}
}
public string GetTempPhotoAbsoluteWebPath(string fileName)
{
return GetDataStore().GetUri(_tempDomainName, fileName).ToString();
}
public string SaveTempPhoto(byte[] data, long maxFileSize, int maxWidth, int maxHeight)
{
data = TryParseImage(data, maxFileSize, new Size(maxWidth, maxHeight), out var imgFormat, out var width, out var height);
var fileName = Guid.NewGuid() + "." + CommonPhotoManager.GetImgFormatName(imgFormat);
var store = GetDataStore();
using var stream = new MemoryStream(data);
return store.Save(_tempDomainName, fileName, stream).ToString();
}
public byte[] GetTempPhotoData(string fileName)
{
using var s = GetDataStore().GetReadStream(_tempDomainName, fileName);
var data = new MemoryStream();
var buffer = new byte[1024 * 10];
while (true)
{
var count = s.Read(buffer, 0, buffer.Length);
if (count == 0) break;
data.Write(buffer, 0, count);
}
return data.ToArray();
}
public string GetSizedTempPhotoAbsoluteWebPath(string fileName, int newWidth, int newHeight)
{
var store = GetDataStore();
if (store.IsFile(_tempDomainName, fileName))
{
using var s = store.GetReadStream(_tempDomainName, fileName);
using var img = Image.FromStream(s);
var imgFormat = img.RawFormat;
byte[] data;
if (img.Width != newWidth || img.Height != newHeight)
{
using var img2 = CommonPhotoManager.DoThumbnail(img, new Size(newWidth, newHeight), true, true, true);
data = CommonPhotoManager.SaveToBytes(img2);
}
else
{
data = CommonPhotoManager.SaveToBytes(img);
}
var widening = CommonPhotoManager.GetImgFormatName(imgFormat);
var index = fileName.LastIndexOf('.');
var fileNameWithoutExt = (index != -1) ? fileName.Substring(0, index) : fileName;
var trueFileName = fileNameWithoutExt + "_size_" + newWidth.ToString() + "-" + newHeight.ToString() + "." + widening;
using var stream = new MemoryStream(data);
return store.Save(_tempDomainName, trueFileName, stream).ToString();
}
return GetDefaultPhotoAbsoluteWebPath(new Size(newWidth, newHeight));
}
public void RemoveTempPhoto(string fileName)
{
var index = fileName.LastIndexOf('.');
var fileNameWithoutExt = (index != -1) ? fileName.Substring(0, index) : fileName;
try
{
var store = GetDataStore();
store.DeleteFiles(_tempDomainName, "", fileNameWithoutExt + "*.*", false);
}
catch { };
}
public Bitmap GetPhotoBitmap(Guid userID)
{
try
{
var data = UserManager.GetUserPhoto(userID);
if (data != null)
{
using var s = new MemoryStream(data);
return new Bitmap(s);
}
}
catch { }
return null;
}
public string SaveThumbnail(Guid userID, Image img, ImageFormat format)
{
var moduleID = Guid.Empty;
var widening = CommonPhotoManager.GetImgFormatName(format);
var size = img.Size;
var fileName = string.Format("{0}{1}_size_{2}-{3}.{4}", (moduleID == Guid.Empty ? "" : moduleID.ToString()), userID, img.Width, img.Height, widening);
var store = GetDataStore();
string photoUrl;
using (var s = new MemoryStream(CommonPhotoManager.SaveToBytes(img)))
{
img.Dispose();
photoUrl = store.Save(fileName, s).ToString();
}
UserPhotoManagerCache.AddToCache(userID, size, fileName);
return photoUrl;
}
public byte[] GetUserPhotoData(Guid userId, Size size)
{
try
{
var pattern = string.Format("{0}_size_{1}-{2}.*", userId, size.Width, size.Height);
var fileName = GetDataStore().ListFilesRelative("", "", pattern, false).FirstOrDefault();
if (string.IsNullOrEmpty(fileName)) return null;
using var s = GetDataStore().GetReadStream("", fileName);
var data = new MemoryStream();
var buffer = new byte[1024 * 10];
while (true)
{
var count = s.Read(buffer, 0, buffer.Length);
if (count == 0) break;
data.Write(buffer, 0, count);
}
return data.ToArray();
}
catch (Exception err)
{
Log.Error(err);
return null;
}
}
private IDataStore GetDataStore()
{
return StorageFactory.GetStorage(Tenant.TenantId.ToString(), "userPhotos");
}
public static CacheSize ToCache(Size size) =>
size switch
{
Size(var w, var h) when w == RetinaFotoSize.Width && h == RetinaFotoSize.Height => CacheSize.Retina,
Size(var w, var h) when w == MaxFotoSize.Width && h == MaxFotoSize.Height => CacheSize.Max,
Size(var w, var h) when w == BigFotoSize.Width && h == BigFotoSize.Height => CacheSize.Big,
Size(var w, var h) when w == SmallFotoSize.Width && h == SmallFotoSize.Height => CacheSize.Small,
Size(var w, var h) when w == MediumFotoSize.Width && h == MediumFotoSize.Height => CacheSize.Medium,
_ => CacheSize.Original
};
}
#region Exception Classes
public class UnknownImageFormatException : Exception
{
public UnknownImageFormatException() : base("unknown image file type") { }
public UnknownImageFormatException(Exception inner) : base("unknown image file type", inner) { }
}
public class ImageWeightLimitException : Exception
{
public ImageWeightLimitException() : base("image width is too large") { }
}
public class ImageSizeLimitException : Exception
{
public ImageSizeLimitException() : base("image size is too large") { }
}
#endregion
/// <summary>
/// Helper class for manipulating images.
/// </summary>
public static class ImageHelper
{
/// <summary>
/// Rotate the given image byte array according to Exif Orientation data
/// </summary>
/// <param name="data">source image byte array</param>
/// <param name="updateExifData">set it to TRUE to update image Exif data after rotation (default is TRUE)</param>
/// <returns>The rotated image byte array. If no rotation occurred, source data will be returned.</returns>
public static byte[] RotateImageByExifOrientationData(byte[] data, ILog Log, bool updateExifData = true)
{
try
{
using var stream = new MemoryStream(data);
using var img = new Bitmap(stream);
var fType = RotateImageByExifOrientationData(img, updateExifData);
if (fType != RotateFlipType.RotateNoneFlipNone)
{
using var tempStream = new MemoryStream();
img.Save(tempStream, System.Drawing.Imaging.ImageFormat.Png);
data = tempStream.ToArray();
}
}
catch (Exception err)
{
Log.Error(err);
}
return data;
}
/// <summary>
/// Rotate the given image file according to Exif Orientation data
/// </summary>
/// <param name="sourceFilePath">path of source file</param>
/// <param name="targetFilePath">path of target file</param>
/// <param name="targetFormat">target format</param>
/// <param name="updateExifData">set it to TRUE to update image Exif data after rotation (default is TRUE)</param>
/// <returns>The RotateFlipType value corresponding to the applied rotation. If no rotation occurred, RotateFlipType.RotateNoneFlipNone will be returned.</returns>
public static RotateFlipType RotateImageByExifOrientationData(string sourceFilePath, string targetFilePath, ImageFormat targetFormat, bool updateExifData = true)
{
// Rotate the image according to EXIF data
using var bmp = new Bitmap(sourceFilePath);
var fType = RotateImageByExifOrientationData(bmp, updateExifData);
if (fType != RotateFlipType.RotateNoneFlipNone)
{
bmp.Save(targetFilePath, targetFormat);
}
return fType;
}
/// <summary>
/// Rotate the given bitmap according to Exif Orientation data
/// </summary>
/// <param name="img">source image</param>
/// <param name="updateExifData">set it to TRUE to update image Exif data after rotation (default is TRUE)</param>
/// <returns>The RotateFlipType value corresponding to the applied rotation. If no rotation occurred, RotateFlipType.RotateNoneFlipNone will be returned.</returns>
public static RotateFlipType RotateImageByExifOrientationData(Image img, bool updateExifData = true)
{
const int orientationId = 0x0112;
var fType = RotateFlipType.RotateNoneFlipNone;
if (img.PropertyIdList.Contains(orientationId))
{
var pItem = img.GetPropertyItem(orientationId);
fType = GetRotateFlipTypeByExifOrientationData(pItem.Value[0]);
if (fType != RotateFlipType.RotateNoneFlipNone)
{
img.RotateFlip(fType);
if (updateExifData) img.RemovePropertyItem(orientationId); // Remove Exif orientation tag
}
}
return fType;
}
/// <summary>
/// Return the proper System.Drawing.RotateFlipType according to given orientation EXIF metadata
/// </summary>
/// <param name="orientation">Exif "Orientation"</param>
/// <returns>the corresponding System.Drawing.RotateFlipType enum value</returns>
public static RotateFlipType GetRotateFlipTypeByExifOrientationData(int orientation)
{
return orientation switch
{
1 => RotateFlipType.RotateNoneFlipNone,
2 => RotateFlipType.RotateNoneFlipX,
3 => RotateFlipType.Rotate180FlipNone,
4 => RotateFlipType.Rotate180FlipX,
5 => RotateFlipType.Rotate90FlipX,
6 => RotateFlipType.Rotate90FlipNone,
7 => RotateFlipType.Rotate270FlipX,
8 => RotateFlipType.Rotate270FlipNone,
_ => RotateFlipType.RotateNoneFlipNone,
};
}
}
public static class SizeExtend
{
public static void Deconstruct(this Size size, out int w, out int h) =>
(w, h) = (size.Width, size.Height);
}
public static class ResizeWorkerItemFactory
{
public static IServiceCollection AddResizeWorkerItemService(this IServiceCollection services)
{
services.TryAddSingleton<WorkerQueueOptionsManager<ResizeWorkerItem>>();
services.TryAddSingleton<WorkerQueue<ResizeWorkerItem>>();
services.AddSingleton<IConfigureOptions<WorkerQueue<ResizeWorkerItem>>, ConfigureWorkerQueue<ResizeWorkerItem>>();
services.Configure<WorkerQueue<ResizeWorkerItem>>(r =>
{
r.workerCount = 2;
r.waitInterval = (int)TimeSpan.FromSeconds(30).TotalMilliseconds;
r.errorCount = 1;
r.stopAfterFinsih = true;
});
return services;
}
}
public static class UserPhotoManagerExtension
{
public static IServiceCollection AddUserPhotoManagerService(this IServiceCollection services)
{
services.TryAddScoped<UserPhotoManager>();
services.TryAddSingleton<UserPhotoManagerCache>();
return services
.AddStorageFactoryService()
.AddUserPhotoThumbnailSettingsService()
.AddWebImageSupplierService()
.AddUserManagerService()
.AddTenantManagerService()
.AddResizeWorkerItemService();
}
}
}