/* * * (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; namespace ASC.Web.Core.Users { internal 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 UserPhotoManager { private static readonly ConcurrentDictionary> Photofiles = new ConcurrentDictionary>(); private static readonly ICacheNotify CacheNotify; static UserPhotoManager() { try { CacheNotify = new KafkaCache(); CacheNotify.Subscribe((data) => { var userId = new Guid(data.UserID.ToByteArray()); Photofiles.GetOrAdd(data.Size, (r) => new ConcurrentDictionary())[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 UserManager UserManager { get; } public WebImageSupplier WebImageSupplier { get; } public UserPhotoThumbnailSettings UserPhotoThumbnailSettings { get; } public TenantManager TenantManager { get; } public StorageFactory StorageFactory { get; } private Tenant tenant; public Tenant Tenant { get { return tenant ?? (tenant = TenantManager.GetCurrentTenant()); } } public UserPhotoManager( UserManager userManager, WebImageSupplier webImageSupplier, UserPhotoThumbnailSettings userPhotoThumbnailSettings, TenantManager tenantManager, StorageFactory storageFactory) { UserManager = userManager; WebImageSupplier = webImageSupplier; UserPhotoThumbnailSettings = userPhotoThumbnailSettings; TenantManager = tenantManager; StorageFactory = storageFactory; } 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); } 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); 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() }; //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 TenantDiskCache = new HashSet(); private static readonly object DiskCacheLoaderLock = new object(); private bool IsCacheLoadedForTenant() { // return TenantDiskCache.Contains(Tenant.TenantId); } private static bool SetCacheLoadedForTenant(bool isLoaded, int tenantId) { return isLoaded ? TenantDiskCache.Add(tenantId) : TenantDiskCache.Remove(tenantId); } private string SearchInCache(Guid userId, Size size, out bool isDef) { if (!IsCacheLoadedForTenant()) LoadDiskCache(); isDef = false; string fileName = null; Photofiles.TryGetValue(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_")); } 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 (!IsCacheLoadedForTenant()) { 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)); } AddToCache(parsedUserId, size, fileName); } } } SetCacheLoadedForTenant(true, Tenant.TenantId); } 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); } } private static void AddToCache(Guid userID, Size size, string fileName) { if (CacheNotify != null) { CacheNotify.Publish(new UserPhotoManagerCacheItem { UserID = Google.Protobuf.ByteString.CopyFrom(userID.ToByteArray()), Size = ToCache(size), FileName = fileName }, CacheNotifyAction.InsertOrUpdate); } } 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); 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); 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); 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 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 { 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); } } //note: using auto stop queue private static readonly WorkerQueue ResizeQueue = new WorkerQueue(2, TimeSpan.FromSeconds(30), 1, true);//TODO: configure 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 static 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(); 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(); } 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) { LogManager.GetLogger("ASC.Web.Photo").Error(err); return null; } } private IDataStore GetDataStore() { return StorageFactory.GetStorage(Tenant.TenantId.ToString(), "userPhotos"); } 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 }; } #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 /// /// Helper class for manipulating images. /// public static class ImageHelper { /// /// Rotate the given image byte array according to Exif Orientation data /// /// source image byte array /// set it to TRUE to update image Exif data after rotation (default is TRUE) /// The rotated image byte array. If no rotation occurred, source data will be returned. 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) { LogManager.GetLogger("ASC.Web.Photo").Error(err); } return data; } /// /// Rotate the given image file according to Exif Orientation data /// /// path of source file /// path of target file /// target format /// set it to TRUE to update image Exif data after rotation (default is TRUE) /// The RotateFlipType value corresponding to the applied rotation. If no rotation occurred, RotateFlipType.RotateNoneFlipNone will be returned. 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; } /// /// Rotate the given bitmap according to Exif Orientation data /// /// source image /// set it to TRUE to update image Exif data after rotation (default is TRUE) /// The RotateFlipType value corresponding to the applied rotation. If no rotation occurred, RotateFlipType.RotateNoneFlipNone will be returned. 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; } /// /// Return the proper System.Drawing.RotateFlipType according to given orientation EXIF metadata /// /// Exif "Orientation" /// the corresponding System.Drawing.RotateFlipType enum value 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); } }