diff --git a/Iceshrimp.Backend/Core/Helpers/BlurhashHelper.cs b/Iceshrimp.Backend/Core/Helpers/BlurhashHelper.cs new file mode 100644 index 00000000..47cd2fd6 --- /dev/null +++ b/Iceshrimp.Backend/Core/Helpers/BlurhashHelper.cs @@ -0,0 +1,159 @@ +using System.Collections.Immutable; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Blurhash; +using CommunityToolkit.HighPerformance; + +namespace Iceshrimp.Backend.Core.Helpers; + +public static class BlurhashHelper +{ + private static readonly ImmutableArray PrecomputedLut = [..Enumerable.Range(0, 256).Select(SRgbToLinear)]; + + /// + /// Encodes a Span2D of raw rgb data into a Blurhash string + /// + /// The 2-dimensional array of pixels to encode + /// The number of components used on the X-Axis for the DCT + /// The number of components used on the Y-Axis for the DCT + /// The resulting Blurhash string + public static string Encode(Span2D pixels, int componentsX, int componentsY) + { + if (componentsX < 1) throw new ArgumentException("componentsX needs to be at least 1"); + if (componentsX > 9) throw new ArgumentException("componentsX needs to be at most 9"); + if (componentsY < 1) throw new ArgumentException("componentsY needs to be at least 1"); + if (componentsY > 9) throw new ArgumentException("componentsY needs to be at most 9"); + + Span factors = stackalloc Pixel[componentsX * componentsY]; + Span resultBuffer = stackalloc char[4 + 2 * componentsX * componentsY]; + Span lut = stackalloc float[256]; + PrecomputedLut.CopyTo(lut); + + var width = pixels.Width; + var height = pixels.Height; + + var xCosines = new double[width]; + var yCosines = new double[height]; + + for (var yComponent = 0; yComponent < componentsY; yComponent++) + for (var xComponent = 0; xComponent < componentsX; xComponent++) + { + double r = 0, g = 0, b = 0; + double normalization = xComponent == 0 && yComponent == 0 ? 1 : 2; + + for (var xPixel = 0; xPixel < width; xPixel++) + xCosines[xPixel] = Math.Cos(Math.PI * xComponent * xPixel / width); + for (var yPixel = 0; yPixel < height; yPixel++) + yCosines[yPixel] = Math.Cos(Math.PI * yComponent * yPixel / height); + + for (var xPixel = 0; xPixel < width; xPixel++) + for (var yPixel = 0; yPixel < height; yPixel++) + { + var basis = xCosines[xPixel] * yCosines[yPixel]; + var pixel = pixels[yPixel, xPixel]; + r += basis * lut[pixel.R]; + g += basis * lut[pixel.G]; + b += basis * lut[pixel.B]; + } + + var scale = normalization / (width * height); + factors[componentsX * yComponent + xComponent].Red = r * scale; + factors[componentsX * yComponent + xComponent].Green = g * scale; + factors[componentsX * yComponent + xComponent].Blue = b * scale; + } + + var dc = factors[0]; + var acCount = componentsX * componentsY - 1; + + var sizeFlag = (componentsX - 1) + (componentsY - 1) * 9; + sizeFlag.EncodeBase83(resultBuffer[..1]); + + float maximumValue; + if (acCount > 0) + { + // Get maximum absolute value of all AC components + var actualMaximumValue = 0.0; + for (var yComponent = 0; yComponent < componentsY; yComponent++) + for (var xComponent = 0; xComponent < componentsX; xComponent++) + { + // Ignore DC component + if (xComponent == 0 && yComponent == 0) continue; + + var factorIndex = componentsX * yComponent + xComponent; + + actualMaximumValue = Math.Max(Math.Abs(factors[factorIndex].Red), actualMaximumValue); + actualMaximumValue = Math.Max(Math.Abs(factors[factorIndex].Green), actualMaximumValue); + actualMaximumValue = Math.Max(Math.Abs(factors[factorIndex].Blue), actualMaximumValue); + } + + var quantizedMaximumValue = (int)Math.Max(0.0, Math.Min(82.0, Math.Floor(actualMaximumValue * 166 - 0.5))); + maximumValue = ((float)quantizedMaximumValue + 1) / 166; + quantizedMaximumValue.EncodeBase83(resultBuffer.Slice(1, 1)); + } + else + { + maximumValue = 1; + resultBuffer[1] = '0'; + } + + EncodeDc(dc.Red, dc.Green, dc.Blue).EncodeBase83(resultBuffer.Slice(2, 4)); + + for (var yComponent = 0; yComponent < componentsY; yComponent++) + for (var xComponent = 0; xComponent < componentsX; xComponent++) + { + // Ignore DC component + if (xComponent == 0 && yComponent == 0) continue; + + var factorIndex = componentsX * yComponent + xComponent; + + EncodeAc(factors[factorIndex].Red, factors[factorIndex].Green, factors[factorIndex].Blue, maximumValue) + .EncodeBase83(resultBuffer.Slice(6 + (factorIndex - 1) * 2, 2)); + } + + return resultBuffer.ToString(); + } + + private static int EncodeAc(double r, double g, double b, double maximumValue) + { + var quantizedR = (int)Math.Max(0, Math.Min(18, Math.Floor(MathUtils.SignPow(r / maximumValue, 0.5) * 9 + 9.5))); + var quantizedG = (int)Math.Max(0, Math.Min(18, Math.Floor(MathUtils.SignPow(g / maximumValue, 0.5) * 9 + 9.5))); + var quantizedB = (int)Math.Max(0, Math.Min(18, Math.Floor(MathUtils.SignPow(b / maximumValue, 0.5) * 9 + 9.5))); + + return quantizedR * 19 * 19 + quantizedG * 19 + quantizedB; + } + + private static int EncodeDc(double r, double g, double b) + { + var roundedR = MathUtils.LinearTosRgb(r); + var roundedG = MathUtils.LinearTosRgb(g); + var roundedB = MathUtils.LinearTosRgb(b); + return (roundedR << 16) + (roundedG << 8) + roundedB; + } + + [StructLayout(LayoutKind.Sequential)] + [method: MethodImpl(MethodImplOptions.AggressiveInlining)] + public struct RgbPixel(byte r, byte g, byte b) + { + public readonly byte R = r; + public readonly byte G = g; + public readonly byte B = b; + } + + private static void EncodeBase83(this int number, Span output) + { + var length = output.Length; + for (var index1 = 0; index1 < length; ++index1) + { + var index2 = number % 83; + number /= 83; + output[length - index1 - 1] = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~"[index2]; + } + } + + private static float SRgbToLinear(int value) + { + var num = value / (float)byte.MaxValue; + return (float)(num <= 0.04045 ? num / 12.92 : float.Pow((num + 0.055f) / 1.055f, 2.4f)); + } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Services/ImageProcessor.cs b/Iceshrimp.Backend/Core/Services/ImageProcessor.cs index cb7c8741..e3ad7889 100644 --- a/Iceshrimp.Backend/Core/Services/ImageProcessor.cs +++ b/Iceshrimp.Backend/Core/Services/ImageProcessor.cs @@ -1,6 +1,9 @@ +using System.Runtime.InteropServices; using Blurhash.ImageSharp; +using CommunityToolkit.HighPerformance; using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Database.Tables; +using Iceshrimp.Backend.Core.Helpers; using Microsoft.Extensions.Options; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats; @@ -55,9 +58,9 @@ public class ImageProcessor NetVips.NetVips.Leak = true; // We don't need the VIPS operation or file cache - NetVips.Cache.Max = 0; + NetVips.Cache.Max = 0; NetVips.Cache.MaxFiles = 0; - NetVips.Cache.MaxMem = 0; + NetVips.Cache.MaxMem = 0; NetVips.Log.SetLogHandler("VIPS", NetVips.Enums.LogLevelFlags.Warning | NetVips.Enums.LogLevelFlags.Error, VipsLogDelegate); @@ -225,30 +228,21 @@ public class ImageProcessor ) { var properties = new DriveFile.FileProperties { Width = ident.Size.Width, Height = ident.Size.Height }; - var res = new Result { Properties = properties }; + var res = new Result { Properties = properties }; - // Calculate blurhash using a x200px image for improved performance + // Calculate blurhash using a x100px image for improved performance using var blurhashImageSource = - NetVips.Image.ThumbnailBuffer(buf, width: 200, height: 200, size: NetVips.Enums.Size.Down); + NetVips.Image.ThumbnailBuffer(buf, width: 100, height: 100, size: NetVips.Enums.Size.Down); using var blurhashImage = blurhashImageSource.Interpretation == NetVips.Enums.Interpretation.Srgb ? blurhashImageSource : blurhashImageSource.Colourspace(NetVips.Enums.Interpretation.Srgb); - var blurBuf = blurhashImage.WriteToMemory(); - var blurArr = new Pixel[blurhashImage.Width, blurhashImage.Height]; + using var blurhashImageFlattened = blurhashImage.HasAlpha() ? blurhashImage.Flatten() : blurhashImage; + using var blurhashImageActual = blurhashImageFlattened.Cast(NetVips.Enums.BandFormat.Uchar); - var idx = 0; - var incr = blurhashImage.Bands - 3; - for (var i = 0; i < blurhashImage.Height; i++) - { - for (var j = 0; j < blurhashImage.Width; j++) - { - blurArr[j, i] = new Pixel(blurBuf[idx++] / 255d, blurBuf[idx++] / 255d, - blurBuf[idx++] / 255d); - idx += incr; - } - } - - res.Blurhash = Blurhash.Core.Encode(blurArr, 7, 7, new Progress()); + var blurBuf = blurhashImageActual.WriteToMemory(); + var blurPixels = MemoryMarshal.Cast(blurBuf) + .AsSpan2D(blurhashImage.Height, blurhashImage.Width); + res.Blurhash = BlurhashHelper.Encode(blurPixels, 7, 7); if (genThumb) { diff --git a/Iceshrimp.Backend/Iceshrimp.Backend.csproj b/Iceshrimp.Backend/Iceshrimp.Backend.csproj index d6e887db..85f00fe1 100644 --- a/Iceshrimp.Backend/Iceshrimp.Backend.csproj +++ b/Iceshrimp.Backend/Iceshrimp.Backend.csproj @@ -17,6 +17,7 @@ +