feat: Port ImageSaving to ImageSharp to fix Linux image uploads (#1131)

This commit is contained in:
Regalia
2025-02-09 18:20:53 +00:00
committed by GitHub
parent 9d7ba34edc
commit 91bc4d076e
3 changed files with 60 additions and 41 deletions

View File

@@ -1,7 +1,16 @@
using System; using System;
using System.Drawing;
using System.IO; using System.IO;
using System.Threading.Tasks; 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 namespace VRCX
{ {
@@ -17,14 +26,15 @@ namespace VRCX
return Convert.ToBase64String(ResizeImageToFitLimits(Convert.FromBase64String(base64data), false)); 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) public byte[] ResizeImageToFitLimits(byte[] imageData, bool matchingDimensions, int maxWidth = 2000,
int maxHeight = 2000, long maxSize = 10_000_000)
{ {
using var fileMemoryStream = new MemoryStream(imageData); using var fileMemoryStream = new MemoryStream(imageData);
var image = new Bitmap(fileMemoryStream); var image = Image.Load(fileMemoryStream);
// for APNG, check if image is png format and less than maxSize // for APNG, check if image is png format and less than maxSize
if ((!matchingDimensions || image.Width == image.Height) && if ((!matchingDimensions || image.Width == image.Height) &&
image.RawFormat.Equals(System.Drawing.Imaging.ImageFormat.Png) && image.Metadata.DecodedImageFormat == PngFormat.Instance &&
imageData.Length < maxSize && imageData.Length < maxSize &&
image.Width <= maxWidth && image.Width <= maxWidth &&
image.Height <= maxHeight) image.Height <= maxHeight)
@@ -32,31 +42,38 @@ namespace VRCX
return imageData; return imageData;
} }
// FIXME: I think these are aspect ratio preserving calcs, but we can ask ImageSharp nicely to do this by
// passing 0, see docs for Resize()
if (image.Width > maxWidth) if (image.Width > maxWidth)
{ {
var sizingFactor = image.Width / (double)maxWidth; var sizingFactor = image.Width / (double)maxWidth;
var newHeight = (int)Math.Round(image.Height / sizingFactor); var newHeight = (int)Math.Round(image.Height / sizingFactor);
image = new Bitmap(image, maxWidth, newHeight); image.Mutate(x => x.Resize(maxWidth, newHeight));
} }
if (image.Height > maxHeight) if (image.Height > maxHeight)
{ {
var sizingFactor = image.Height / (double)maxHeight; var sizingFactor = image.Height / (double)maxHeight;
var newWidth = (int)Math.Round(image.Width / sizingFactor); var newWidth = (int)Math.Round(image.Width / sizingFactor);
image = new Bitmap(image, newWidth, maxHeight); image.Mutate(x => x.Resize(newWidth, maxHeight));
} }
if (matchingDimensions && image.Width != image.Height) if (matchingDimensions && image.Width != image.Height)
{ {
var newSize = Math.Max(image.Width, image.Height); var newSize = Math.Max(image.Width, image.Height);
var newImage = new Bitmap(newSize, newSize); using Image<Rgba32> resizedImage = new(newSize, newSize);
using var graphics = Graphics.FromImage(newImage); // regalialong: i think the access should be safe
graphics.Clear(Color.Transparent); // ReSharper disable AccessToModifiedClosure
graphics.DrawImage(image, new Rectangle((newSize - image.Width) / 2, (newSize - image.Height) / 2, image.Width, image.Height)); // ReSharper disable AccessToDisposedClosure
resizedImage.Mutate(x => x.DrawImage(image,
new Rectangle((newSize - image.Width) / 2, (newSize - image.Height) / 2, image.Width, image.Height),
0));
// ReSharper restore AccessToDisposedClosure
// ReSharper restore AccessToModifiedClosure
image.Dispose(); image.Dispose();
image = newImage; image = resizedImage;
} }
SaveToFileToUpload(); SaveToFileToUpload();
for (int i = 0; i < 250 && imageData.Length > maxSize; i++) for (var i = 0; i < 250 && imageData.Length > maxSize; i++)
{ {
SaveToFileToUpload(); SaveToFileToUpload();
if (imageData.Length < maxSize) if (imageData.Length < maxSize)
@@ -74,7 +91,8 @@ namespace VRCX
newHeight = image.Height - 25; newHeight = image.Height - 25;
newWidth = (int)Math.Round(image.Width / (image.Height / (double)newHeight)); newWidth = (int)Math.Round(image.Width / (image.Height / (double)newHeight));
} }
image = new Bitmap(image, newWidth, newHeight);
image.Mutate(x => x.Resize(newWidth, newHeight));
} }
if (imageData.Length > maxSize) if (imageData.Length > maxSize)
@@ -82,12 +100,13 @@ namespace VRCX
throw new Exception("Failed to get image into target filesize."); throw new Exception("Failed to get image into target filesize.");
} }
image.Dispose();
return imageData; return imageData;
void SaveToFileToUpload() void SaveToFileToUpload()
{ {
using var imageSaveMemoryStream = new MemoryStream(); using var imageSaveMemoryStream = new MemoryStream();
image.Save(imageSaveMemoryStream, System.Drawing.Imaging.ImageFormat.Png); image.SaveAsPng(imageSaveMemoryStream);
imageData = imageSaveMemoryStream.ToArray(); imageData = imageSaveMemoryStream.ToArray();
} }
} }
@@ -98,10 +117,9 @@ namespace VRCX
const int desiredHeight = 1080; const int desiredHeight = 1080;
using var fileMemoryStream = new MemoryStream(imageData); using var fileMemoryStream = new MemoryStream(imageData);
var image = new Bitmap(fileMemoryStream); var image = Image.Load(fileMemoryStream);
if (image.Height > image.Width) if (image.Height > image.Width) image.Mutate(x => x.Rotate(RotateMode.Rotate90));
image.RotateFlip(RotateFlipType.Rotate90FlipNone);
// increase size to 1920x1080 // increase size to 1920x1080
if (image.Width < desiredWidth || image.Height < desiredHeight) if (image.Width < desiredWidth || image.Height < desiredHeight)
@@ -126,12 +144,16 @@ namespace VRCX
newWidth = testWidth; newWidth = testWidth;
} }
} }
var resizedImage = new Bitmap(desiredWidth, desiredHeight);
using var graphics1 = Graphics.FromImage(resizedImage);
graphics1.Clear(Color.White); using Image<Rgba32> resizedImage = new(desiredWidth, desiredHeight);
var x = (desiredWidth - newWidth) / 2; resizedImage.Mutate(x
var y = (desiredHeight - newHeight) / 2; // ReSharper disable once AccessToModifiedClosure
graphics1.DrawImage(image, new Rectangle(x, y, newWidth, newHeight)); // ReSharper disable once AccessToDisposedClosure
=> x.Fill(Color.White).DrawImage(image,
new Rectangle((desiredWidth - newWidth) / 2, (desiredHeight - newHeight) / 2, newWidth,
newHeight), 0)
);
image.Dispose(); image.Dispose();
image = resizedImage; image = resizedImage;
} }
@@ -141,31 +163,26 @@ namespace VRCX
{ {
var sizingFactor = image.Width / (double)desiredWidth; var sizingFactor = image.Width / (double)desiredWidth;
var newHeight = (int)Math.Round(image.Height / sizingFactor); var newHeight = (int)Math.Round(image.Height / sizingFactor);
image = new Bitmap(image, desiredWidth, newHeight); image.Mutate(x => x.Resize(desiredWidth, newHeight));
} }
if (image.Height > desiredHeight) if (image.Height > desiredHeight)
{ {
var sizingFactor = image.Height / (double)desiredHeight; var sizingFactor = image.Height / (double)desiredHeight;
var newWidth = (int)Math.Round(image.Width / sizingFactor); var newWidth = (int)Math.Round(image.Width / sizingFactor);
image = new Bitmap(image, newWidth, desiredHeight); image.Mutate(x => x.Resize(newWidth, desiredHeight));
} }
// add white border // add white border
// wtf are these magic numbers // wtf are these magic numbers
const int xOffset = 64; // 2048 / 32 const int xOffset = 64; // 2048 / 32
const int yOffset = 69; // 1440 / 20.869 const int yOffset = 69; // 1440 / 20.869
var newImage = new Bitmap(2048, 1440); using Image<Rgba32> newImage = new(2048, 1440);
using var graphics = Graphics.FromImage(newImage); newImage.Mutate(x => x.Fill(Color.White));
graphics.Clear(Color.White);
// graphics.DrawImage(image, new Rectangle(xOffset, yOffset, image.Width, image.Height)); // graphics.DrawImage(image, new Rectangle(xOffset, yOffset, image.Width, image.Height));
var newX = (2048 - image.Width) / 2; var newX = (2048 - image.Width) / 2;
var newY = yOffset; newImage.Mutate(x => x.DrawImage(image, new Rectangle(newX, yOffset, image.Width, image.Height), 0));
graphics.DrawImage(image, new Rectangle(newX, newY, image.Width, image.Height));
image.Dispose();
image = newImage;
using var imageSaveMemoryStream = new MemoryStream(); using var imageSaveMemoryStream = new MemoryStream();
image.Save(imageSaveMemoryStream, System.Drawing.Imaging.ImageFormat.Png); newImage.SaveAsPng(imageSaveMemoryStream);
return imageSaveMemoryStream.ToArray(); return imageSaveMemoryStream.ToArray();
} }
@@ -184,17 +201,15 @@ namespace VRCX
var tempPath = path + ".temp"; var tempPath = path + ".temp";
var bytes = await File.ReadAllBytesAsync(path); var bytes = await File.ReadAllBytesAsync(path);
var ms = new MemoryStream(bytes); var ms = new MemoryStream(bytes);
Bitmap print = new Bitmap(ms); var print = await Image.LoadAsync(ms);
// validation step to ensure image is actually a print // validation step to ensure image is actually a print
if (print.Width != 2048 || print.Height != 1440) if (print.Width != 2048 || print.Height != 1440) return false;
{
return false;
}
var point = new Point(64, 69); var point = new Point(64, 69);
var size = new Size(1920, 1080); var size = new Size(1920, 1080);
var rectangle = new Rectangle(point, size); var rectangle = new Rectangle(point, size);
Bitmap cropped = print.Clone(rectangle, print.PixelFormat); print.Mutate(x => x.Crop(rectangle));
cropped.Save(tempPath); await print.SaveAsPngAsync(tempPath);
if (ScreenshotHelper.HasTXt(path)) if (ScreenshotHelper.HasTXt(path))
{ {
var success = ScreenshotHelper.CopyTXt(path, tempPath); var success = ScreenshotHelper.CopyTXt(path, tempPath);

View File

@@ -94,6 +94,8 @@
<PackageReference Include="NLog" Version="5.4.0" /> <PackageReference Include="NLog" Version="5.4.0" />
<PackageReference Include="SharpDX.Direct3D11" Version="4.2.0" /> <PackageReference Include="SharpDX.Direct3D11" Version="4.2.0" />
<PackageReference Include="SharpDX.Mathematics" Version="4.2.0" /> <PackageReference Include="SharpDX.Mathematics" Version="4.2.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.5" />
<PackageReference Include="sqlite-net-pcl" Version="1.9.172" /> <PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
<PackageReference Include="System.Data.SQLite" Version="1.0.119" /> <PackageReference Include="System.Data.SQLite" Version="1.0.119" />
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" /> <PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />

View File

@@ -100,6 +100,8 @@
<PackageReference Include="NLog" Version="5.4.0" /> <PackageReference Include="NLog" Version="5.4.0" />
<PackageReference Include="SharpDX.Direct3D11" Version="4.2.0" /> <PackageReference Include="SharpDX.Direct3D11" Version="4.2.0" />
<PackageReference Include="SharpDX.Mathematics" Version="4.2.0" /> <PackageReference Include="SharpDX.Mathematics" Version="4.2.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.5" />
<PackageReference Include="sqlite-net-pcl" Version="1.9.172" /> <PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
<PackageReference Include="System.Data.SQLite" Version="1.0.119" /> <PackageReference Include="System.Data.SQLite" Version="1.0.119" />
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" /> <PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />