DocSpace-client/web/ASC.Web.Core/Users/UserPhotoManager.cs

955 lines
38 KiB
C#
Raw Normal View History

2019-06-07 08:59:07 +00:00
/*
*
* (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;
2019-08-08 09:26:58 +00:00
using ASC.Core.Tenants;
2019-06-07 08:59:07 +00:00
using ASC.Data.Storage;
using ASC.Web.Core.Utility.Skins;
using ASC.Web.Studio.Utility;
namespace ASC.Web.Core.Users
{
internal class ResizeWorkerItem
{
public ResizeWorkerItem(Guid userId, byte[] data, long maxFileSize, Size size, IDataStore dataStore, UserPhotoThumbnailSettings settings)
{
2019-08-15 13:56:54 +00:00
UserId = userId;
Data = data;
MaxFileSize = maxFileSize;
Size = size;
DataStore = dataStore;
Settings = settings;
2019-06-07 08:59:07 +00:00
}
2019-08-15 13:56:54 +00:00
public Size Size { get; }
2019-06-07 08:59:07 +00:00
2019-08-15 13:56:54 +00:00
public IDataStore DataStore { get; }
2019-06-07 08:59:07 +00:00
2019-08-15 13:56:54 +00:00
public long MaxFileSize { get; }
2019-06-07 08:59:07 +00:00
2019-08-15 13:56:54 +00:00
public byte[] Data { get; }
2019-06-07 08:59:07 +00:00
2019-08-15 13:56:54 +00:00
public Guid UserId { get; }
2019-06-07 08:59:07 +00:00
2019-08-15 13:56:54 +00:00
public UserPhotoThumbnailSettings Settings { get; }
2019-06-07 08:59:07 +00:00
public override bool Equals(object obj)
{
2019-08-15 14:36:08 +00:00
if (obj is null) return false;
2019-06-07 08:59:07 +00:00
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != typeof(ResizeWorkerItem)) return false;
return Equals((ResizeWorkerItem)obj);
}
public bool Equals(ResizeWorkerItem other)
{
2019-08-15 14:36:08 +00:00
if (other is null) return false;
2019-06-07 08:59:07 +00:00
if (ReferenceEquals(this, other)) return true;
return other.UserId.Equals(UserId) && other.MaxFileSize == MaxFileSize && other.Size.Equals(Size);
}
public override int GetHashCode()
{
unchecked
{
2019-08-15 13:03:57 +00:00
var result = UserId.GetHashCode();
2019-06-07 08:59:07 +00:00
result = (result * 397) ^ MaxFileSize.GetHashCode();
result = (result * 397) ^ Size.GetHashCode();
return result;
}
}
}
public class UserPhotoManager
{
2019-08-28 09:15:33 +00:00
private static readonly ConcurrentDictionary<CacheSize, ConcurrentDictionary<Guid, string>> Photofiles = new ConcurrentDictionary<CacheSize, ConcurrentDictionary<Guid, string>>();
private static readonly ICacheNotify<UserPhotoManagerCacheItem> CacheNotify;
2019-06-07 08:59:07 +00:00
static UserPhotoManager()
{
try
{
CacheNotify = new KafkaCache<UserPhotoManagerCacheItem>();
2019-06-07 08:59:07 +00:00
CacheNotify.Subscribe((data) =>
2019-06-07 08:59:07 +00:00
{
var userId = new Guid(data.UserID.ToByteArray());
2019-08-28 09:15:33 +00:00
Photofiles.GetOrAdd(data.Size, (r) => new ConcurrentDictionary<Guid, string>())[userId] = data.FileName;
}, CacheNotifyAction.InsertOrUpdate);
CacheNotify.Subscribe((data) =>
{
var userId = new Guid(data.UserID.ToByteArray());
var size = FromCahe(data.Size);
try
2019-06-07 08:59:07 +00:00
{
2019-08-28 09:15:33 +00:00
Photofiles.TryGetValue(CacheSize.Big, out var dict);
dict?.TryRemove(userId, out _);
//var storage = GetDataStore();
//storage.DeleteFiles("", data.UserID + "*.*", false);
//SetCacheLoadedForTenant(false);
2019-06-07 08:59:07 +00:00
}
catch { }
}, CacheNotifyAction.Remove);
2019-06-07 08:59:07 +00:00
}
catch (Exception)
{
2019-08-15 12:04:42 +00:00
2019-06-07 08:59:07 +00:00
}
}
public static string GetDefaultPhotoAbsoluteWebPath()
{
return WebImageSupplier.GetAbsoluteWebPath(_defaultAvatar);
}
2019-08-08 09:26:58 +00:00
public static string GetRetinaPhotoURL(int tenantId, Guid userID)
2019-06-07 08:59:07 +00:00
{
2019-08-08 09:26:58 +00:00
return GetRetinaPhotoURL(tenantId, userID, out _);
2019-07-24 12:34:23 +00:00
}
2019-08-08 09:26:58 +00:00
public static string GetRetinaPhotoURL(int tenantId, Guid userID, out bool isdef)
2019-07-24 12:34:23 +00:00
{
2019-08-08 09:26:58 +00:00
return GetSizedPhotoAbsoluteWebPath(tenantId, userID, RetinaFotoSize, out isdef);
2019-06-07 08:59:07 +00:00
}
2019-08-08 09:26:58 +00:00
public static string GetMaxPhotoURL(int tenantId, Guid userID)
2019-06-07 08:59:07 +00:00
{
2019-08-08 09:26:58 +00:00
return GetMaxPhotoURL(tenantId, userID, out _);
2019-07-24 12:34:23 +00:00
}
2019-08-08 09:26:58 +00:00
public static string GetMaxPhotoURL(int tenantId, Guid userID, out bool isdef)
2019-07-24 12:34:23 +00:00
{
2019-08-08 09:26:58 +00:00
return GetSizedPhotoAbsoluteWebPath(tenantId, userID, MaxFotoSize, out isdef);
2019-06-07 08:59:07 +00:00
}
2019-08-08 09:26:58 +00:00
public static string GetBigPhotoURL(int tenantId, Guid userID)
2019-06-07 08:59:07 +00:00
{
2019-08-08 09:26:58 +00:00
return GetBigPhotoURL(tenantId, userID, out _);
2019-07-24 12:34:23 +00:00
}
2019-08-08 09:26:58 +00:00
public static string GetBigPhotoURL(int tenantId, Guid userID, out bool isdef)
2019-07-24 12:34:23 +00:00
{
2019-08-08 09:26:58 +00:00
return GetSizedPhotoAbsoluteWebPath(tenantId, userID, BigFotoSize, out isdef);
2019-06-07 08:59:07 +00:00
}
2019-08-08 09:26:58 +00:00
public static string GetMediumPhotoURL(int tenantId, Guid userID)
2019-06-07 08:59:07 +00:00
{
2019-08-08 09:26:58 +00:00
return GetMediumPhotoURL(tenantId, userID, out _);
2019-07-24 12:34:23 +00:00
}
2019-08-08 09:26:58 +00:00
public static string GetMediumPhotoURL(int tenantId, Guid userID, out bool isdef)
2019-07-24 12:34:23 +00:00
{
2019-08-08 09:26:58 +00:00
return GetSizedPhotoAbsoluteWebPath(tenantId, userID, MediumFotoSize, out isdef);
2019-06-07 08:59:07 +00:00
}
2019-08-08 09:26:58 +00:00
public static string GetSmallPhotoURL(int tenantId, Guid userID)
2019-06-07 08:59:07 +00:00
{
2019-08-08 09:26:58 +00:00
return GetSmallPhotoURL(tenantId, userID, out _);
2019-07-24 12:34:23 +00:00
}
2019-08-08 09:26:58 +00:00
public static string GetSmallPhotoURL(int tenantId, Guid userID, out bool isdef)
2019-07-24 12:34:23 +00:00
{
2019-08-08 09:26:58 +00:00
return GetSizedPhotoAbsoluteWebPath(tenantId, userID, SmallFotoSize, out isdef);
2019-06-07 08:59:07 +00:00
}
2019-08-08 09:26:58 +00:00
public static string GetSizedPhotoUrl(int tenantId, Guid userId, int width, int height)
2019-06-07 08:59:07 +00:00
{
2019-08-08 09:26:58 +00:00
return GetSizedPhotoAbsoluteWebPath(tenantId, userId, new Size(width, height));
2019-06-07 08:59:07 +00:00
}
public static string GetDefaultSmallPhotoURL()
{
return GetDefaultPhotoAbsoluteWebPath(SmallFotoSize);
}
public static string GetDefaultMediumPhotoURL()
{
return GetDefaultPhotoAbsoluteWebPath(MediumFotoSize);
}
public static string GetDefaultBigPhotoURL()
{
return GetDefaultPhotoAbsoluteWebPath(BigFotoSize);
}
public static string GetDefaultMaxPhotoURL()
{
return GetDefaultPhotoAbsoluteWebPath(MaxFotoSize);
}
public static string GetDefaultRetinaPhotoURL()
{
return GetDefaultPhotoAbsoluteWebPath(RetinaFotoSize);
}
public static Size OriginalFotoSize { get; } = new Size(1280, 1280);
2019-06-07 08:59:07 +00:00
public static Size RetinaFotoSize { get; } = new Size(360, 360);
2019-06-07 08:59:07 +00:00
public static Size MaxFotoSize { get; } = new Size(200, 200);
2019-06-07 08:59:07 +00:00
public static Size BigFotoSize { get; } = new Size(82, 82);
2019-06-07 08:59:07 +00:00
public static Size MediumFotoSize { get; } = new Size(48, 48);
2019-06-07 08:59:07 +00:00
public static Size SmallFotoSize { get; } = new Size(32, 32);
2019-06-07 08:59:07 +00:00
2019-08-15 14:36:08 +00:00
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";
2019-06-07 08:59:07 +00:00
2019-08-08 09:26:58 +00:00
public static bool UserHasAvatar(Tenant tenant, Guid userID)
2019-06-07 08:59:07 +00:00
{
2019-08-08 09:26:58 +00:00
var path = GetPhotoAbsoluteWebPath(tenant, userID);
2019-06-07 08:59:07 +00:00
var fileName = Path.GetFileName(path);
return fileName != _defaultAvatar;
}
2019-08-15 12:04:42 +00:00
2019-08-08 09:26:58 +00:00
public static string GetPhotoAbsoluteWebPath(Tenant tenant, Guid userID)
2019-06-07 08:59:07 +00:00
{
var path = SearchInCache(tenant.TenantId, userID, Size.Empty, out _);
2019-06-07 08:59:07 +00:00
if (!string.IsNullOrEmpty(path)) return path;
try
{
2019-08-08 09:26:58 +00:00
var data = CoreContext.UserManager.GetUserPhoto(tenant.TenantId, userID);
2019-06-07 08:59:07 +00:00
string photoUrl;
string fileName;
if (data == null || data.Length == 0)
{
photoUrl = GetDefaultPhotoAbsoluteWebPath();
fileName = "default";
}
else
{
2019-08-08 09:26:58 +00:00
photoUrl = SaveOrUpdatePhoto(tenant, userID, data, -1, new Size(-1, -1), false, out fileName);
2019-06-07 08:59:07 +00:00
}
AddToCache(userID, Size.Empty, fileName);
return photoUrl;
}
catch
{
}
return GetDefaultPhotoAbsoluteWebPath();
}
2019-08-08 09:26:58 +00:00
internal static Size GetPhotoSize(Tenant tenant, Guid userID)
2019-06-07 08:59:07 +00:00
{
2019-08-08 09:26:58 +00:00
var virtualPath = GetPhotoAbsoluteWebPath(tenant, userID);
2019-06-07 08:59:07 +00:00
if (virtualPath == null) return Size.Empty;
try
{
var sizePart = virtualPath.Substring(virtualPath.LastIndexOf('_'));
sizePart = sizePart.Trim('_');
sizePart = sizePart.Remove(sizePart.LastIndexOf('.'));
2019-08-15 13:16:39 +00:00
return new Size(int.Parse(sizePart.Split('-')[0]), int.Parse(sizePart.Split('-')[1]));
2019-06-07 08:59:07 +00:00
}
catch
{
return Size.Empty;
}
}
2019-08-08 09:26:58 +00:00
private static string GetSizedPhotoAbsoluteWebPath(int tenantId, Guid userID, Size size)
2019-07-24 12:34:23 +00:00
{
2019-08-08 09:26:58 +00:00
return GetSizedPhotoAbsoluteWebPath(tenantId, userID, size, out _);
2019-07-24 12:34:23 +00:00
}
2019-08-08 09:26:58 +00:00
private static string GetSizedPhotoAbsoluteWebPath(int tenantId, Guid userID, Size size, out bool isdef)
2019-06-07 08:59:07 +00:00
{
var res = SearchInCache(tenantId, userID, size, out isdef);
2019-08-05 13:29:58 +00:00
if (!string.IsNullOrEmpty(res)) return res;
2019-06-07 08:59:07 +00:00
try
{
2019-08-08 09:26:58 +00:00
var data = CoreContext.UserManager.GetUserPhoto(tenantId, userID);
2019-06-07 08:59:07 +00:00
if (data == null || data.Length == 0)
{
//empty photo. cache default
var photoUrl = GetDefaultPhotoAbsoluteWebPath(size);
AddToCache(userID, size, "default");
2019-07-24 12:34:23 +00:00
isdef = true;
2019-06-07 08:59:07 +00:00
return photoUrl;
}
//Enqueue for sizing
2019-08-08 09:26:58 +00:00
SizePhoto(tenantId, userID, data, -1, size);
2019-06-07 08:59:07 +00:00
}
catch { }
2019-07-24 12:34:23 +00:00
isdef = false;
2019-06-07 08:59:07 +00:00
return GetDefaultPhotoAbsoluteWebPath(size);
}
private static 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()
};
2019-06-07 08:59:07 +00:00
//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 HashSet<int> TenantDiskCache = new HashSet<int>();
private static readonly object DiskCacheLoaderLock = new object();
private static bool IsCacheLoadedForTenant(int tenantId)
2019-06-07 08:59:07 +00:00
{
//
return TenantDiskCache.Contains(tenantId);
2019-06-07 08:59:07 +00:00
}
private static bool SetCacheLoadedForTenant(bool isLoaded)
{
return isLoaded ? TenantDiskCache.Add(TenantProvider.CurrentTenantID) : TenantDiskCache.Remove(TenantProvider.CurrentTenantID);
}
private static string SearchInCache(int tenantId, Guid userId, Size size, out bool isDef)
2019-06-07 08:59:07 +00:00
{
if (!IsCacheLoadedForTenant(tenantId))
LoadDiskCache(tenantId);
2019-06-07 08:59:07 +00:00
2019-08-05 13:29:58 +00:00
isDef = false;
string fileName = null;
2019-08-28 09:15:33 +00:00
Photofiles.TryGetValue(ToCache(size), out var photo);
2019-06-07 08:59:07 +00:00
if (size != Size.Empty)
{
2019-08-28 09:15:33 +00:00
photo?.TryGetValue(userId, out fileName);
}
else
{
fileName = photo?
.Select(x => x.Value)
.FirstOrDefault(x => !string.IsNullOrEmpty(x) && x.Contains("_orig_"));
2019-06-07 08:59:07 +00:00
}
2019-08-05 13:29:58 +00:00
if (fileName != null && fileName.StartsWith("default"))
{
isDef = true;
return GetDefaultPhotoAbsoluteWebPath(size);
}
2019-06-07 08:59:07 +00:00
if (!string.IsNullOrEmpty(fileName))
{
var store = GetDataStore(tenantId);
2019-06-07 08:59:07 +00:00
return store.GetUri(fileName).ToString();
}
return null;
}
private static void LoadDiskCache(int tenantId)
2019-06-07 08:59:07 +00:00
{
lock (DiskCacheLoaderLock)
{
if (!IsCacheLoadedForTenant(tenantId))
2019-06-07 08:59:07 +00:00
{
try
{
var listFileNames = GetDataStore(tenantId).ListFilesRelative("", "", "*.*", false);
2019-06-07 08:59:07 +00:00
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));
}
AddToCache(parsedUserId, size, fileName);
}
}
}
SetCacheLoadedForTenant(true);
}
catch (Exception err)
{
LogManager.GetLogger("ASC.Web.Photo").Error(err);
}
}
}
}
private static void ClearCache(Guid userID)
{
if (CacheNotify != null)
{
CacheNotify.Publish(new UserPhotoManagerCacheItem { UserID = Google.Protobuf.ByteString.CopyFrom(userID.ToByteArray()) }, CacheNotifyAction.Remove);
2019-06-07 08:59:07 +00:00
}
}
private static void AddToCache(Guid userID, Size size, string fileName)
2019-06-07 08:59:07 +00:00
{
if (CacheNotify != null)
{
CacheNotify.Publish(new UserPhotoManagerCacheItem { UserID = Google.Protobuf.ByteString.CopyFrom(userID.ToByteArray()), Size = ToCache(size), FileName = fileName }, CacheNotifyAction.InsertOrUpdate);
2019-06-07 08:59:07 +00:00
}
}
public static void ResetThumbnailSettings(Guid userId)
{
var thumbSettings = new UserPhotoThumbnailSettings().GetDefault() as UserPhotoThumbnailSettings;
thumbSettings.SaveForUser(userId);
}
2019-08-08 09:26:58 +00:00
public static string SaveOrUpdatePhoto(Tenant tenant, Guid userID, byte[] data)
2019-06-07 08:59:07 +00:00
{
2019-08-16 09:08:46 +00:00
return SaveOrUpdatePhoto(tenant, userID, data, -1, OriginalFotoSize, true, out _);
2019-06-07 08:59:07 +00:00
}
2019-08-08 09:26:58 +00:00
public static void RemovePhoto(Tenant tenant, Guid idUser)
2019-06-07 08:59:07 +00:00
{
2019-08-08 09:26:58 +00:00
CoreContext.UserManager.SaveUserPhoto(tenant, idUser, null);
2019-06-07 08:59:07 +00:00
ClearCache(idUser);
}
2019-08-08 09:26:58 +00:00
private static string SaveOrUpdatePhoto(Tenant tenant, Guid userID, byte[] data, long maxFileSize, Size size, bool saveInCoreContext, out string fileName)
2019-06-07 08:59:07 +00:00
{
2019-08-15 13:05:50 +00:00
data = TryParseImage(data, maxFileSize, size, out var imgFormat, out var width, out var height);
2019-06-07 08:59:07 +00:00
var widening = CommonPhotoManager.GetImgFormatName(imgFormat);
fileName = string.Format("{0}_orig_{1}-{2}.{3}", userID, width, height, widening);
if (saveInCoreContext)
{
2019-08-08 09:26:58 +00:00
CoreContext.UserManager.SaveUserPhoto(tenant, userID, data);
2019-06-07 08:59:07 +00:00
SetUserPhotoThumbnailSettings(userID, width, height);
ClearCache(userID);
}
var store = GetDataStore(tenant.TenantId);
2019-06-07 08:59:07 +00:00
var photoUrl = GetDefaultPhotoAbsoluteWebPath();
if (data != null && data.Length > 0)
{
using (var stream = new MemoryStream(data))
{
photoUrl = store.Save(fileName, stream).ToString();
}
//Queue resizing
2019-08-08 09:26:58 +00:00
SizePhoto(tenant.TenantId, userID, data, -1, SmallFotoSize, true);
SizePhoto(tenant.TenantId, userID, data, -1, MediumFotoSize, true);
SizePhoto(tenant.TenantId, userID, data, -1, BigFotoSize, true);
SizePhoto(tenant.TenantId, userID, data, -1, MaxFotoSize, true);
SizePhoto(tenant.TenantId, userID, data, -1, RetinaFotoSize, true);
2019-06-07 08:59:07 +00:00
}
return photoUrl;
}
private static 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 static 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);
try
{
2019-08-15 15:08:40 +00:00
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))
2019-06-07 08:59:07 +00:00
{
2019-08-15 15:08:40 +00:00
#region calulate height and width
2019-06-07 08:59:07 +00:00
2019-08-15 15:08:40 +00:00
if (width > maxWidth && height > maxHeight)
2019-06-07 08:59:07 +00:00
{
2019-08-15 15:08:40 +00:00
if (width > height)
2019-06-07 08:59:07 +00:00
{
height = (int)((double)height * (double)maxWidth / (double)width + 0.5);
width = maxWidth;
}
2019-08-15 15:08:40 +00:00
else
2019-06-07 08:59:07 +00:00
{
width = (int)((double)width * (double)maxHeight / (double)height + 0.5);
height = maxHeight;
}
2019-08-15 15:08:40 +00:00
}
2019-06-07 08:59:07 +00:00
2019-08-15 15:08:40 +00:00
if (width > maxWidth && height <= maxHeight)
{
height = (int)((double)height * (double)maxWidth / (double)width + 0.5);
width = maxWidth;
}
2019-06-07 08:59:07 +00:00
2019-08-15 15:08:40 +00:00
if (width <= maxWidth && height > maxHeight)
{
width = (int)((double)width * (double)maxHeight / (double)height + 0.5);
height = maxHeight;
2019-06-07 08:59:07 +00:00
}
2019-08-15 15:08:40 +00:00
#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);
2019-06-07 08:59:07 +00:00
}
2019-08-15 15:08:40 +00:00
return data;
2019-06-07 08:59:07 +00:00
}
catch (OutOfMemoryException)
{
throw new ImageSizeLimitException();
}
catch (ArgumentException error)
{
throw new UnknownImageFormatException(error);
}
}
//note: using auto stop queue
private static readonly WorkerQueue<ResizeWorkerItem> ResizeQueue = new WorkerQueue<ResizeWorkerItem>(2, TimeSpan.FromSeconds(30), 1, true);//TODO: configure
2019-08-08 09:26:58 +00:00
private static string SizePhoto(int tenantId, Guid userID, byte[] data, long maxFileSize, Size size)
2019-06-07 08:59:07 +00:00
{
2019-08-08 09:26:58 +00:00
return SizePhoto(tenantId, userID, data, maxFileSize, size, false);
2019-06-07 08:59:07 +00:00
}
2019-08-08 09:26:58 +00:00
private static string SizePhoto(int tenantId, Guid userID, byte[] data, long maxFileSize, Size size, bool now)
2019-06-07 08:59:07 +00:00
{
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(tenantId), UserPhotoThumbnailSettings.LoadForUser(userID));
2019-06-07 08:59:07 +00:00
if (now)
{
//Resize synchronously
ResizeImage(resizeTask);
2019-08-08 09:26:58 +00:00
return GetSizedPhotoAbsoluteWebPath(tenantId, userID, size);
2019-06-07 08:59:07 +00:00
}
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 static void ResizeImage(ResizeWorkerItem item)
{
try
{
var data = item.Data;
2019-08-15 15:08:40 +00:00
using var stream = new MemoryStream(data);
using var img = Image.FromStream(stream);
var imgFormat = img.RawFormat;
if (item.Size != img.Size)
2019-06-07 08:59:07 +00:00
{
2019-08-15 15:08:40 +00:00
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);
}
2019-06-07 08:59:07 +00:00
2019-08-15 15:08:40 +00:00
var widening = CommonPhotoManager.GetImgFormatName(imgFormat);
var fileName = string.Format("{0}_size_{1}-{2}.{3}", item.UserId, item.Size.Width, item.Size.Height, widening);
2019-06-07 08:59:07 +00:00
2019-08-15 15:08:40 +00:00
using var stream2 = new MemoryStream(data);
item.DataStore.Save(fileName, stream2).ToString();
2019-06-07 08:59:07 +00:00
2019-08-15 15:08:40 +00:00
AddToCache(item.UserId, item.Size, fileName);
2019-06-07 08:59:07 +00:00
}
catch (ArgumentException error)
{
throw new UnknownImageFormatException(error);
}
}
public static string GetTempPhotoAbsoluteWebPath(int tenantId, string fileName)
2019-06-07 08:59:07 +00:00
{
return GetDataStore(tenantId).GetUri(_tempDomainName, fileName).ToString();
2019-06-07 08:59:07 +00:00
}
public static string SaveTempPhoto(int tenantId, byte[] data, long maxFileSize, int maxWidth, int maxHeight)
2019-06-07 08:59:07 +00:00
{
2019-08-15 13:05:50 +00:00
data = TryParseImage(data, maxFileSize, new Size(maxWidth, maxHeight), out var imgFormat, out var width, out var height);
2019-06-07 08:59:07 +00:00
var fileName = Guid.NewGuid() + "." + CommonPhotoManager.GetImgFormatName(imgFormat);
var store = GetDataStore(tenantId);
2019-08-15 15:08:40 +00:00
using var stream = new MemoryStream(data);
return store.Save(_tempDomainName, fileName, stream).ToString();
2019-06-07 08:59:07 +00:00
}
public static byte[] GetTempPhotoData(int tenantId, string fileName)
2019-06-07 08:59:07 +00:00
{
using var s = GetDataStore(tenantId).GetReadStream(_tempDomainName, fileName);
2019-08-15 15:08:40 +00:00
var data = new MemoryStream();
var buffer = new byte[1024 * 10];
while (true)
2019-06-07 08:59:07 +00:00
{
2019-08-15 15:08:40 +00:00
var count = s.Read(buffer, 0, buffer.Length);
if (count == 0) break;
data.Write(buffer, 0, count);
2019-06-07 08:59:07 +00:00
}
2019-08-15 15:08:40 +00:00
return data.ToArray();
2019-06-07 08:59:07 +00:00
}
public static string GetSizedTempPhotoAbsoluteWebPath(int tenantId, string fileName, int newWidth, int newHeight)
2019-06-07 08:59:07 +00:00
{
var store = GetDataStore(tenantId);
2019-06-07 08:59:07 +00:00
if (store.IsFile(_tempDomainName, fileName))
{
2019-08-15 15:08:40 +00:00
using var s = store.GetReadStream(_tempDomainName, fileName);
using var img = Image.FromStream(s);
var imgFormat = img.RawFormat;
byte[] data;
2019-06-07 08:59:07 +00:00
2019-08-15 15:08:40 +00:00
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);
2019-06-07 08:59:07 +00:00
}
2019-08-15 15:08:40 +00:00
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();
2019-06-07 08:59:07 +00:00
}
return GetDefaultPhotoAbsoluteWebPath(new Size(newWidth, newHeight));
}
public static void RemoveTempPhoto(int tenantId, string fileName)
2019-06-07 08:59:07 +00:00
{
var index = fileName.LastIndexOf('.');
var fileNameWithoutExt = (index != -1) ? fileName.Substring(0, index) : fileName;
try
{
var store = GetDataStore(tenantId);
2019-06-07 08:59:07 +00:00
store.DeleteFiles(_tempDomainName, "", fileNameWithoutExt + "*.*", false);
}
catch { };
}
2019-08-08 09:26:58 +00:00
public static Bitmap GetPhotoBitmap(int tenantId, Guid userID)
2019-06-07 08:59:07 +00:00
{
try
{
2019-08-08 09:26:58 +00:00
var data = CoreContext.UserManager.GetUserPhoto(tenantId, userID);
2019-06-07 08:59:07 +00:00
if (data != null)
{
2019-08-15 15:08:40 +00:00
using var s = new MemoryStream(data);
return new Bitmap(s);
2019-06-07 08:59:07 +00:00
}
}
catch { }
return null;
}
public static string SaveThumbnail(int tenantId, Guid userID, Image img, ImageFormat format)
2019-06-07 08:59:07 +00:00
{
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(tenantId);
2019-06-07 08:59:07 +00:00
string photoUrl;
using (var s = new MemoryStream(CommonPhotoManager.SaveToBytes(img)))
{
img.Dispose();
photoUrl = store.Save(fileName, s).ToString();
}
AddToCache(userID, size, fileName);
return photoUrl;
}
public static byte[] GetUserPhotoData(int tenantId, Guid userId, Size size)
2019-06-07 08:59:07 +00:00
{
try
{
var pattern = string.Format("{0}_size_{1}-{2}.*", userId, size.Width, size.Height);
var fileName = GetDataStore(tenantId).ListFilesRelative("", "", pattern, false).FirstOrDefault();
2019-06-07 08:59:07 +00:00
if (string.IsNullOrEmpty(fileName)) return null;
using var s = GetDataStore(tenantId).GetReadStream("", fileName);
2019-08-15 15:08:40 +00:00
var data = new MemoryStream();
var buffer = new byte[1024 * 10];
while (true)
2019-06-07 08:59:07 +00:00
{
2019-08-15 15:08:40 +00:00
var count = s.Read(buffer, 0, buffer.Length);
if (count == 0) break;
data.Write(buffer, 0, count);
2019-06-07 08:59:07 +00:00
}
2019-08-15 15:08:40 +00:00
return data.ToArray();
2019-06-07 08:59:07 +00:00
}
catch (Exception err)
{
LogManager.GetLogger("ASC.Web.Photo").Error(err);
return null;
}
}
private static IDataStore GetDataStore(int tenantId)
2019-06-07 08:59:07 +00:00
{
return StorageFactory.GetStorage(tenantId.ToString(), "userPhotos");
2019-06-07 08:59:07 +00:00
}
private static Size FromCahe(CacheSize cacheSize) =>
cacheSize switch
{
CacheSize.Big => BigFotoSize,
CacheSize.Max => MaxFotoSize,
CacheSize.Medium => MediumFotoSize,
CacheSize.Original => OriginalFotoSize,
CacheSize.Retina => RetinaFotoSize,
CacheSize.Small => SmallFotoSize,
_ => OriginalFotoSize,
};
private 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
};
2019-06-07 08:59:07 +00:00
}
#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, 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)
{
2019-08-15 12:04:42 +00:00
LogManager.GetLogger("ASC.Web.Photo").Error(err);
2019-06-07 08:59:07 +00:00
}
return data;
}
2019-08-15 12:04:42 +00:00
2019-06-07 08:59:07 +00:00
/// <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
2019-08-16 08:44:03 +00:00
using var bmp = new Bitmap(sourceFilePath);
2019-06-07 08:59:07 +00:00
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)
{
2019-08-15 15:13:25 +00:00
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,
};
2019-06-07 08:59:07 +00:00
}
}
public static class SizeExtend
{
public static void Deconstruct(this Size size, out int w, out int h) =>
(w, h) = (size.Width, size.Height);
}
2019-06-07 08:59:07 +00:00
}