Surface.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. using PixiEditor.Helpers.Extensions;
  2. using PixiEditor.Models.Position;
  3. using SkiaSharp;
  4. using System;
  5. using System.Collections;
  6. using System.Collections.Generic;
  7. using System.Diagnostics;
  8. using System.Runtime.CompilerServices;
  9. using System.Runtime.InteropServices;
  10. using System.Windows;
  11. using System.Windows.Media;
  12. using System.Windows.Media.Imaging;
  13. namespace PixiEditor.Models.DataHolders
  14. {
  15. public class Surface : IDisposable
  16. {
  17. public static SKPaint ReplacingPaint { get; } = new() { BlendMode = SKBlendMode.Src };
  18. public static SKPaint BlendingPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.SrcOver };
  19. public static SKPaint MaskingPaint { get; } = new() { BlendMode = SKBlendMode.DstIn };
  20. public static SKPaint InverseMaskingPaint { get; } = new() { BlendMode = SKBlendMode.DstOut };
  21. private static readonly SKPaint nearestNeighborReplacingPaint = new SKPaint() { BlendMode = SKBlendMode.Src, FilterQuality = SKFilterQuality.None };
  22. public SKSurface SkiaSurface { get; private set; }
  23. public int Width { get; }
  24. public int Height { get; }
  25. public bool Disposed { get; private set; } = false;
  26. private SKPaint drawingPaint = new SKPaint() { BlendMode = SKBlendMode.Src };
  27. private IntPtr surfaceBuffer;
  28. public Surface(int w, int h)
  29. {
  30. if (w <= 0 || h <= 0)
  31. throw new ArgumentException("Surface dimensions must be non-zero");
  32. InitSurface(w, h);
  33. Width = w;
  34. Height = h;
  35. }
  36. public Surface(Surface original)
  37. {
  38. Width = original.Width;
  39. Height = original.Height;
  40. InitSurface(Width, Height);
  41. original.SkiaSurface.Draw(SkiaSurface.Canvas, 0, 0, ReplacingPaint);
  42. }
  43. public Surface(int w, int h, byte[] pbgra32Bytes)
  44. {
  45. if (w <= 0 || h <= 0)
  46. throw new ArgumentException("Surface dimensions must be non-zero");
  47. Width = w;
  48. Height = h;
  49. InitSurface(w, h);
  50. DrawBytes(w, h, pbgra32Bytes, SKColorType.Bgra8888, SKAlphaType.Premul);
  51. }
  52. public Surface(BitmapSource original)
  53. {
  54. SKColorType color = original.Format.ToSkia(out SKAlphaType alpha);
  55. if (original.PixelWidth <= 0 || original.PixelHeight <= 0)
  56. throw new ArgumentException("Surface dimensions must be non-zero");
  57. int stride = (original.PixelWidth * original.Format.BitsPerPixel + 7) / 8;
  58. byte[] pixels = new byte[stride * original.PixelHeight];
  59. original.CopyPixels(pixels, stride, 0);
  60. Width = original.PixelWidth;
  61. Height = original.PixelHeight;
  62. InitSurface(Width, Height);
  63. DrawBytes(Width, Height, pixels, color, alpha);
  64. }
  65. public Surface(SKImage image)
  66. {
  67. Width = image.Width;
  68. Height = image.Height;
  69. InitSurface(Width, Height);
  70. SkiaSurface.Canvas.DrawImage(image, 0, 0);
  71. }
  72. /// <summary>
  73. /// Combines the <paramref name="images"/> into a <see cref="Surface"/>
  74. /// </summary>
  75. /// <param name="width">The width of the <see cref="Surface"/></param>
  76. /// <param name="height">The height of the <see cref="Surface"/></param>
  77. /// <returns>A surface that has the <paramref name="images"/> drawn on it</returns>
  78. public static Surface Combine(int width, int height, IEnumerable<(SKImage image, Coordinates offset)> images)
  79. {
  80. Surface surface = new Surface(width, height);
  81. foreach (var image in images)
  82. {
  83. surface.SkiaSurface.Canvas.DrawImage(image.image, (SKPoint)image.offset);
  84. }
  85. return surface;
  86. }
  87. public Surface ResizeNearestNeighbor(int newW, int newH)
  88. {
  89. SKImage image = SkiaSurface.Snapshot();
  90. Surface newSurface = new(newW, newH);
  91. newSurface.SkiaSurface.Canvas.DrawImage(image, new SKRect(0, 0, newW, newH), nearestNeighborReplacingPaint);
  92. return newSurface;
  93. }
  94. public Surface Crop(int x, int y, int width, int height)
  95. {
  96. Surface result = new Surface(width, height);
  97. SkiaSurface.Draw(result.SkiaSurface.Canvas, x, y, ReplacingPaint);
  98. return result;
  99. }
  100. public unsafe SKColor GetSRGBPixel(int x, int y)
  101. {
  102. Half* ptr = (Half*)(surfaceBuffer + (x + y * Width) * 8);
  103. float a = (float)ptr[3];
  104. return (SKColor)new SKColorF((float)ptr[0] / a, (float)ptr[1] / a, (float)ptr[2] / a, (float)ptr[3]);
  105. }
  106. public void SetSRGBPixel(int x, int y, SKColor color)
  107. {
  108. // It's possible that this function can be sped up by writing into surfaceBuffer, not sure if skia will like it though
  109. drawingPaint.Color = color;
  110. SkiaSurface.Canvas.DrawPoint(x, y, drawingPaint);
  111. }
  112. public unsafe byte[] ToByteArray(SKColorType colorType = SKColorType.Bgra8888, SKAlphaType alphaType = SKAlphaType.Premul)
  113. {
  114. var imageInfo = new SKImageInfo(Width, Height, colorType, alphaType, SKColorSpace.CreateSrgb());
  115. byte[] buffer = new byte[Width * Height * imageInfo.BytesPerPixel];
  116. fixed (void* pointer = buffer)
  117. {
  118. if (!SkiaSurface.ReadPixels(imageInfo, new IntPtr(pointer), imageInfo.RowBytes, 0, 0))
  119. {
  120. throw new InvalidOperationException("Could not read surface into buffer");
  121. }
  122. }
  123. return buffer;
  124. }
  125. public WriteableBitmap ToWriteableBitmap()
  126. {
  127. WriteableBitmap result = new WriteableBitmap(Width, Height, 96, 96, PixelFormats.Pbgra32, null);
  128. result.Lock();
  129. var dirty = new Int32Rect(0, 0, Width, Height);
  130. result.WritePixels(dirty, ToByteArray(), Width * 4, 0);
  131. result.AddDirtyRect(dirty);
  132. result.Unlock();
  133. return result;
  134. }
  135. public void Dispose()
  136. {
  137. if (Disposed)
  138. return;
  139. Disposed = true;
  140. SkiaSurface.Dispose();
  141. drawingPaint.Dispose();
  142. Marshal.FreeHGlobal(surfaceBuffer);
  143. GC.SuppressFinalize(this);
  144. }
  145. ~Surface()
  146. {
  147. Marshal.FreeHGlobal(surfaceBuffer);
  148. }
  149. private static SKSurface CreateSurface(int w, int h, IntPtr buffer)
  150. {
  151. var surface = SKSurface.Create(new SKImageInfo(w, h, SKColorType.RgbaF16, SKAlphaType.Premul, SKColorSpace.CreateSrgb()), buffer);
  152. if (surface == null)
  153. throw new Exception("Could not create surface");
  154. return surface;
  155. }
  156. private static SKSurface CreateSurface(int w, int h)
  157. {
  158. var surface = SKSurface.Create(new SKImageInfo(w, h, SKColorType.RgbaF16, SKAlphaType.Premul, SKColorSpace.CreateSrgb()));
  159. if (surface == null)
  160. throw new Exception("Could not create surface");
  161. return surface;
  162. }
  163. private unsafe void InitSurface(int w, int h)
  164. {
  165. int byteC = w * h * 8;
  166. surfaceBuffer = Marshal.AllocHGlobal(byteC);
  167. Unsafe.InitBlockUnaligned((byte*)surfaceBuffer, 0, (uint)byteC);
  168. SkiaSurface = CreateSurface(w, h, surfaceBuffer);
  169. }
  170. private unsafe void DrawBytes(int w, int h, byte[] bytes, SKColorType colorType, SKAlphaType alphaType)
  171. {
  172. SKImageInfo info = new SKImageInfo(w, h, colorType, alphaType);
  173. fixed (void* pointer = bytes)
  174. {
  175. using SKPixmap map = new(info, new IntPtr(pointer));
  176. using SKSurface surface = SKSurface.Create(map);
  177. surface.Draw(SkiaSurface.Canvas, 0, 0, ReplacingPaint);
  178. }
  179. }
  180. #if DEBUG
  181. // Used to iterate the surface's pixels during development
  182. [Obsolete("Only meant for use in a debugger like Visual Studio", true)]
  183. private SurfaceDebugger Debugger => new SurfaceDebugger(this);
  184. [Obsolete("Only meant for use in a debugger like Visual Studio", true)]
  185. private class SurfaceDebugger : IEnumerable
  186. {
  187. private readonly Surface _surface;
  188. public SurfaceDebugger(Surface surface)
  189. {
  190. _surface = surface;
  191. }
  192. IEnumerator IEnumerable.GetEnumerator()
  193. {
  194. var pixmap = _surface.SkiaSurface.PeekPixels();
  195. for (int y = 0; y < pixmap.Width; y++)
  196. {
  197. yield return new DebugPixel(y);
  198. for (int x = 0; x < pixmap.Height; x++)
  199. {
  200. yield return new DebugPixel(x, y, pixmap.GetPixelColor(x, y).ToString());
  201. }
  202. }
  203. }
  204. [DebuggerDisplay("{DebuggerDisplay,nq}")]
  205. private struct DebugPixel
  206. {
  207. [DebuggerBrowsable(DebuggerBrowsableState.Never)]
  208. private string DebuggerDisplay
  209. {
  210. get
  211. {
  212. if (isPixel)
  213. {
  214. return $"X: {x}; Y: {y} - {hex}";
  215. }
  216. else
  217. {
  218. return $"|- Y: {y} -|";
  219. }
  220. }
  221. }
  222. [DebuggerBrowsable(DebuggerBrowsableState.Never)]
  223. private readonly int x;
  224. [DebuggerBrowsable(DebuggerBrowsableState.Never)]
  225. private readonly int y;
  226. [DebuggerBrowsable(DebuggerBrowsableState.Never)]
  227. private readonly string hex;
  228. [DebuggerBrowsable(DebuggerBrowsableState.Never)]
  229. private readonly bool isPixel;
  230. public DebugPixel(int y)
  231. {
  232. x = 0;
  233. this.y = y;
  234. hex = null;
  235. isPixel = false;
  236. }
  237. public DebugPixel(int x, int y, string hex)
  238. {
  239. this.x = x;
  240. this.y = y;
  241. this.hex = hex;
  242. isPixel = true;
  243. }
  244. }
  245. }
  246. #endif
  247. }
  248. }