using System; using System.Collections.Generic; using System.IO; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using Color = SixLabors.ImageSharp.Color; using Image = SixLabors.ImageSharp.Image; using Point = SixLabors.ImageSharp.Point; using Rectangle = SixLabors.ImageSharp.Rectangle; using Size = SixLabors.ImageSharp.Size; namespace VRCX { public partial class AppApi { public void PopulateImageHosts(string json) { var hosts = JsonSerializer.Deserialize>(json); ImageCache.PopulateImageHosts(hosts); } public async Task GetImage(string url, string fileId, string version) { return await ImageCache.GetImage(url, fileId, version); } public string ResizeImageToFitLimits(string base64data) { return Convert.ToBase64String(ResizeImageToFitLimits(Convert.FromBase64String(base64data), false)); } public byte[] ResizeImageToFitLimits(byte[] imageData, bool matchingDimensions, int maxWidth = 2000, int maxHeight = 2000, long maxSize = 10_000_000) { using var fileMemoryStream = new MemoryStream(imageData); var image = Image.Load(fileMemoryStream); if (image.Width > maxWidth) { var sizingFactor = image.Width / (double)maxWidth; var newHeight = (int)Math.Round(image.Height / sizingFactor); image.Mutate(x => x.Resize(maxWidth, newHeight)); } if (image.Height > maxHeight) { var sizingFactor = image.Height / (double)maxHeight; var newWidth = (int)Math.Round(image.Width / sizingFactor); image.Mutate(x => x.Resize(newWidth, maxHeight)); } if (matchingDimensions && image.Width != image.Height) { var targetSize = Math.Max(image.Width, image.Height); var squareCanvas = new Image(targetSize, targetSize); var xOffset = (targetSize - image.Width) / 2; var yOffset = (targetSize - image.Height) / 2; squareCanvas.Mutate(x => x.DrawImage(image, new Point(xOffset, yOffset), 1f)); image = squareCanvas; } SaveToFileToUpload(); for (var i = 0; i < 250 && imageData.Length > maxSize; i++) { SaveToFileToUpload(); if (imageData.Length < maxSize) break; int newWidth; int newHeight; if (image.Width > image.Height) { newWidth = image.Width - 25; newHeight = (int)Math.Round(image.Height / (image.Width / (double)newWidth)); } else { newHeight = image.Height - 25; newWidth = (int)Math.Round(image.Width / (image.Height / (double)newHeight)); } image.Mutate(x => x.Resize(newWidth, newHeight)); } if (imageData.Length > maxSize) { throw new Exception("Failed to get image into target filesize."); } image.Dispose(); return imageData; void SaveToFileToUpload() { using var imageSaveMemoryStream = new MemoryStream(); image.SaveAsPng(imageSaveMemoryStream); imageData = imageSaveMemoryStream.ToArray(); } } public byte[] ResizePrintImage(byte[] imageData) { const int desiredWidth = 1920; const int desiredHeight = 1080; using var fileMemoryStream = new MemoryStream(imageData); var image = Image.Load(fileMemoryStream); if (image.Height > image.Width) image.Mutate(x => x.Rotate(RotateMode.Rotate270)); // increase size to 1920x1080 if (image.Width < desiredWidth || image.Height < desiredHeight) { const double expectedAspectRatio = 1920.0 / 1080.0; var target = new Image(1920, 1080); var aspectRatio = (double)image.Width / image.Height; int width, height, xOffset, yOffset; if (aspectRatio > expectedAspectRatio) { // Image is wider than 16:9 - scale based on width width = 1920; height = (int)(width / aspectRatio); xOffset = 0; yOffset = (1080 - height) / 2; } else { // Image is taller than 16:9 - scale based on height height = 1080; width = (int)(height * aspectRatio); xOffset = (1920 - width) / 2; yOffset = 0; } using var scaledImage = image.Clone(ctx => ctx.Resize(width, height)); target.Mutate(x => x.Fill(Color.White) .DrawImage(scaledImage, new Point(xOffset, yOffset), 1f)); image = target; } // limit size to 1920x1080 if (image.Width > desiredWidth) { var sizingFactor = image.Width / (double)desiredWidth; var newHeight = (int)Math.Round(image.Height / sizingFactor); image.Mutate(x => x.Resize(desiredWidth, newHeight)); } if (image.Height > desiredHeight) { var sizingFactor = image.Height / (double)desiredHeight; var newWidth = (int)Math.Round(image.Width / sizingFactor); image.Mutate(x => x.Resize(newWidth, desiredHeight)); } // add white border // wtf are these magic numbers const int xBorderOffset = 64; // 2048 / 32 const int yBorderOffset = 69; // 1440 / 20.869 using Image newImage = new(2048, 1440); newImage.Mutate(x => x.Fill(Color.White)); // graphics.DrawImage(image, new Rectangle(xOffset, yOffset, image.Width, image.Height)); var newX = (2048 - image.Width) / 2; var borderPoint = new Point(newX, yBorderOffset); newImage.Mutate(x => x.DrawImage(image, borderPoint, 1f)); using var imageSaveMemoryStream = new MemoryStream(); newImage.SaveAsPng(imageSaveMemoryStream); return imageSaveMemoryStream.ToArray(); } public async Task CropAllPrints(string ugcFolderPath) { var folder = Path.Join(GetUGCPhotoLocation(ugcFolderPath), "Prints"); if (!Directory.Exists(folder)) { return; } var files = Directory.GetFiles(folder, "*.png", SearchOption.AllDirectories); foreach (var file in files) { await CropPrintImage(file); } } public async Task CropPrintImage(string path) { var tempPath = path + ".temp"; var bytes = await File.ReadAllBytesAsync(path); var ms = new MemoryStream(bytes); var print = await Image.LoadAsync(ms); // validation step to ensure image is actually a print if (!CropPrint(ref print)) return false; await print.SaveAsPngAsync(tempPath); var oldPngFile = new PNGFile(path, false); var newPngFile = new PNGFile(tempPath, true); // Copy all iTXt chunks to new file var textChunks = oldPngFile.GetChunksOfType(PNGChunkTypeFilter.iTXt); for (var i = 0; i < textChunks.Count; i++) { newPngFile.WriteChunk(textChunks[i]); } oldPngFile.Dispose(); newPngFile.Dispose(); // check if file is in use and we have permission to write for (var i = 0; i < 10; i++) { try { await using (File.Open(path, FileMode.Append, FileAccess.Write, FileShare.None)) { break; } } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { await Task.Delay(1000); } } try { File.Move(tempPath, path, true); } catch (Exception ex) { logger.Error(ex, "Failed to replace cropped print image"); return false; } return true; } public bool CropPrint(ref Image image) { if (image.Width != 2048 || image.Height != 1440) return false; var point = new Point(64, 69); var size = new Size(1920, 1080); var rectangle = new Rectangle(point, size); image.Mutate(x => x.Crop(rectangle)); return true; } public async Task SavePrintToFile(string url, string ugcFolderPath, string monthFolder, string fileName) { var folder = Path.Join(GetUGCPhotoLocation(ugcFolderPath), "Prints", MakeValidFileName(monthFolder)); Directory.CreateDirectory(folder); var filePath = Path.Join(folder, MakeValidFileName(fileName)); if (File.Exists(filePath)) return null; try { await ImageCache.SaveImageToFile(url, filePath); } catch (Exception ex) { logger.Error(ex, "Failed to save print to file"); return null; } return filePath; } public async Task SaveStickerToFile(string url, string ugcFolderPath, string monthFolder, string fileName) { var folder = Path.Join(GetUGCPhotoLocation(ugcFolderPath), "Stickers", MakeValidFileName(monthFolder)); Directory.CreateDirectory(folder); var filePath = Path.Join(folder, MakeValidFileName(fileName)); if (File.Exists(filePath)) return null; try { await ImageCache.SaveImageToFile(url, filePath); } catch (Exception ex) { logger.Error(ex, "Failed to save print to file"); return null; } return filePath; } public async Task SaveEmojiToFile(string url, string ugcFolderPath, string monthFolder, string fileName) { var folder = Path.Join(GetUGCPhotoLocation(ugcFolderPath), "Emoji", MakeValidFileName(monthFolder)); Directory.CreateDirectory(folder); var filePath = Path.Join(folder, MakeValidFileName(fileName)); if (File.Exists(filePath)) return null; try { await ImageCache.SaveImageToFile(url, filePath); } catch (Exception ex) { logger.Error(ex, "Failed to save print to file"); return null; } return filePath; } } }