ChunkyImage.cs 57 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520
  1. using System.ComponentModel.DataAnnotations;
  2. using System.Runtime.CompilerServices;
  3. using ChunkyImageLib.DataHolders;
  4. using ChunkyImageLib.Operations;
  5. using OneOf;
  6. using OneOf.Types;
  7. using PixiEditor.Common;
  8. using Drawie.Backend.Core;
  9. using Drawie.Backend.Core.ColorsImpl;
  10. using Drawie.Backend.Core.ColorsImpl.Paintables;
  11. using Drawie.Backend.Core.Numerics;
  12. using Drawie.Backend.Core.Surfaces;
  13. using Drawie.Backend.Core.Surfaces.ImageData;
  14. using Drawie.Backend.Core.Surfaces.PaintImpl;
  15. using Drawie.Backend.Core.Vector;
  16. using Drawie.Numerics;
  17. [assembly: InternalsVisibleTo("ChunkyImageLibTest")]
  18. namespace ChunkyImageLib;
  19. /// <summary>
  20. /// This class is thread-safe only for reading! Only the functions from IReadOnlyChunkyImage can be called from any thread.
  21. /// ChunkyImage can be in two general states:
  22. /// 1. a state with all chunks committed and no queued operations
  23. /// - latestChunks and latestChunksData are empty
  24. /// - queuedOperations are empty
  25. /// - committedChunks[ChunkResolution.Full] contains the current versions of all stored chunks
  26. /// - committedChunks[*any other resolution*] may contain the current low res versions of some of the chunks (or all of them, or none)
  27. /// - LatestSize == CommittedSize == current image size (px)
  28. /// 2. and a state with some queued operations
  29. /// - queuedOperations contains all requested operations (drawing, raster clips, clear, etc.)
  30. /// - committedChunks[ChunkResolution.Full] contains the last versions before any operations of all stored chunks
  31. /// - committedChunks[*any other resolution*] may contain the last low res versions before any operations of some of the chunks (or all of them, or none)
  32. /// - latestChunks stores chunks with some (or none, or all) queued operations applied
  33. /// - latestChunksData stores the data for some or all of the latest chunks (not necessarily synced with latestChunks).
  34. /// The data includes how many operations from the queue have already been applied to the chunk, as well as chunk deleted state (the clear operation deletes chunks)
  35. /// - LatestSize contains the new size if any resize operations were requested, otherwise the committed size
  36. /// You can check the current state via queuedOperations.Count == 0
  37. ///
  38. /// Depending on the chosen blend mode the latest chunks contain different things:
  39. /// - BlendMode.Src: default mode, the latest chunks are the same as committed ones but with some or all queued operations applied.
  40. /// This means that operations can work with the existing pixels.
  41. /// - Any other blend mode: the latest chunks contain only the things drawn by the queued operations.
  42. /// They need to be drawn over the committed chunks to obtain the final image. In this case, operations won't have access to the existing pixels.
  43. /// </summary>
  44. public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICacheable
  45. {
  46. private struct LatestChunkData
  47. {
  48. public LatestChunkData()
  49. {
  50. QueueProgress = 0;
  51. IsDeleted = false;
  52. }
  53. public int QueueProgress { get; set; }
  54. public bool IsDeleted { get; set; }
  55. }
  56. private bool disposed = false;
  57. private readonly object lockObject = new();
  58. private int commitCounter = 0;
  59. private RectI cachedPreciseBounds = RectI.Empty;
  60. private int lastBoundsCacheHash = -1;
  61. public const int FullChunkSize = ChunkPool.FullChunkSize;
  62. private static Paint ClippingPaint { get; } = new Paint() { BlendMode = BlendMode.DstIn };
  63. private static Paint InverseClippingPaint { get; } = new Paint() { BlendMode = BlendMode.DstOut };
  64. private static Paint ReplacingPaint { get; } = new Paint() { BlendMode = BlendMode.Src };
  65. private static Paint SmoothReplacingPaint { get; } =
  66. new Paint() { BlendMode = BlendMode.Src, FilterQuality = FilterQuality.Medium };
  67. private static Paint AddingPaint { get; } = new Paint() { BlendMode = BlendMode.Plus };
  68. private readonly Paint blendModePaint = new Paint() { BlendMode = BlendMode.Src };
  69. public ColorSpace ProcessingColorSpace { get; set; }
  70. public int CommitCounter => commitCounter;
  71. public VecI CommittedSize { get; private set; }
  72. public VecI LatestSize { get; private set; }
  73. public int QueueLength
  74. {
  75. get
  76. {
  77. lock (lockObject)
  78. return queuedOperations.Count;
  79. }
  80. }
  81. private readonly List<(IOperation operation, AffectedArea affectedArea)> queuedOperations = new();
  82. private readonly List<ChunkyImage> activeClips = new();
  83. private BlendMode blendMode = BlendMode.Src;
  84. private bool lockTransparency = false;
  85. private VectorPath? clippingPath;
  86. private double? horizontalSymmetryAxis = null;
  87. private double? verticalSymmetryAxis = null;
  88. private int operationCounter = 0;
  89. private readonly Dictionary<ChunkResolution, Dictionary<VecI, Chunk>> committedChunks;
  90. private readonly Dictionary<ChunkResolution, Dictionary<VecI, Chunk>> latestChunks;
  91. private readonly Dictionary<ChunkResolution, Dictionary<VecI, LatestChunkData>> latestChunksData;
  92. public ChunkyImage(VecI size, ColorSpace colorSpace)
  93. {
  94. CommittedSize = size;
  95. LatestSize = size;
  96. committedChunks = new()
  97. {
  98. [ChunkResolution.Full] = new(),
  99. [ChunkResolution.Half] = new(),
  100. [ChunkResolution.Quarter] = new(),
  101. [ChunkResolution.Eighth] = new(),
  102. };
  103. latestChunks = new()
  104. {
  105. [ChunkResolution.Full] = new(),
  106. [ChunkResolution.Half] = new(),
  107. [ChunkResolution.Quarter] = new(),
  108. [ChunkResolution.Eighth] = new(),
  109. };
  110. latestChunksData = new()
  111. {
  112. [ChunkResolution.Full] = new(),
  113. [ChunkResolution.Half] = new(),
  114. [ChunkResolution.Quarter] = new(),
  115. [ChunkResolution.Eighth] = new(),
  116. };
  117. ProcessingColorSpace = colorSpace;
  118. }
  119. public ChunkyImage(Surface image, ColorSpace colorSpace) : this(image.Size, colorSpace)
  120. {
  121. EnqueueDrawImage(VecI.Zero, image);
  122. CommitChanges();
  123. }
  124. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  125. public RectI? FindChunkAlignedMostUpToDateBounds()
  126. {
  127. lock (lockObject)
  128. {
  129. ThrowIfDisposed();
  130. RectI? rect = null;
  131. foreach (var (pos, _) in committedChunks[ChunkResolution.Full])
  132. {
  133. RectI chunkBounds = new RectI(pos * FullChunkSize, new VecI(FullChunkSize));
  134. rect ??= chunkBounds;
  135. rect = rect.Value.Union(chunkBounds);
  136. }
  137. foreach (var operation in queuedOperations)
  138. {
  139. foreach (var pos in operation.affectedArea.Chunks)
  140. {
  141. RectI chunkBounds = new RectI(pos * FullChunkSize, new VecI(FullChunkSize));
  142. rect ??= chunkBounds;
  143. rect = rect.Value.Union(chunkBounds);
  144. }
  145. }
  146. return rect;
  147. }
  148. }
  149. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  150. public RectI? FindChunkAlignedCommittedBounds()
  151. {
  152. lock (lockObject)
  153. {
  154. ThrowIfDisposed();
  155. RectI? rect = null;
  156. foreach (var (pos, _) in committedChunks[ChunkResolution.Full])
  157. {
  158. RectI chunkBounds = new RectI(pos * FullChunkSize, new VecI(FullChunkSize));
  159. rect ??= chunkBounds;
  160. rect = rect.Value.Union(chunkBounds);
  161. }
  162. return rect;
  163. }
  164. }
  165. /// <summary>
  166. /// Finds the precise bounds in <paramref name="suggestedResolution"/>. If there are no chunks rendered for that resolution, full res chunks are used instead.
  167. /// </summary>
  168. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  169. public RectI? FindTightCommittedBounds(ChunkResolution suggestedResolution = ChunkResolution.Full)
  170. {
  171. lock (lockObject)
  172. {
  173. ThrowIfDisposed();
  174. if (lastBoundsCacheHash == GetCacheHash())
  175. {
  176. return cachedPreciseBounds;
  177. }
  178. var chunkSize = suggestedResolution.PixelSize();
  179. var multiplier = suggestedResolution.Multiplier();
  180. RectI scaledCommittedSize = (RectI)(new RectD(VecI.Zero, CommittedSize * multiplier)).RoundOutwards();
  181. RectI? preciseBounds = null;
  182. foreach (var (chunkPos, fullResChunk) in committedChunks[ChunkResolution.Full])
  183. {
  184. if (committedChunks[suggestedResolution].TryGetValue(chunkPos, out Chunk? requestedResChunk))
  185. {
  186. RectI visibleArea = new RectI(chunkPos * chunkSize, new VecI(chunkSize))
  187. .Intersect(scaledCommittedSize).Translate(-chunkPos * chunkSize);
  188. RectI? chunkPreciseBounds = requestedResChunk.FindPreciseBounds(visibleArea);
  189. if (chunkPreciseBounds is null)
  190. continue;
  191. RectI globalChunkBounds = chunkPreciseBounds.Value.Offset(chunkPos * chunkSize);
  192. preciseBounds ??= globalChunkBounds;
  193. preciseBounds = preciseBounds.Value.Union(globalChunkBounds);
  194. }
  195. else
  196. {
  197. RectI visibleArea = new RectI(chunkPos * FullChunkSize, new VecI(FullChunkSize))
  198. .Intersect(new RectI(VecI.Zero, CommittedSize)).Translate(-chunkPos * FullChunkSize);
  199. RectI? chunkPreciseBounds = fullResChunk.FindPreciseBounds(visibleArea);
  200. if (chunkPreciseBounds is null)
  201. continue;
  202. RectI globalChunkBounds = (RectI)chunkPreciseBounds.Value.Scale(multiplier)
  203. .Offset(chunkPos * chunkSize).RoundOutwards();
  204. preciseBounds ??= globalChunkBounds;
  205. preciseBounds = preciseBounds.Value.Union(globalChunkBounds);
  206. }
  207. }
  208. preciseBounds = (RectI?)preciseBounds?.Scale(suggestedResolution.InvertedMultiplier()).RoundOutwards();
  209. preciseBounds = preciseBounds?.Intersect(new RectI(preciseBounds.Value.Pos, CommittedSize));
  210. cachedPreciseBounds = preciseBounds.GetValueOrDefault();
  211. lastBoundsCacheHash = GetCacheHash();
  212. return preciseBounds;
  213. }
  214. }
  215. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  216. public ChunkyImage CloneFromCommitted()
  217. {
  218. lock (lockObject)
  219. {
  220. ThrowIfDisposed();
  221. ChunkyImage output = new(LatestSize, ProcessingColorSpace);
  222. var chunks = FindCommittedChunks();
  223. foreach (var chunk in chunks)
  224. {
  225. var image = GetCommittedChunk(chunk, ChunkResolution.Full);
  226. if (image is null)
  227. continue;
  228. output.EnqueueDrawImage(chunk * FullChunkSize, image.Surface);
  229. }
  230. output.CommitChanges();
  231. return output;
  232. }
  233. }
  234. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  235. public Color GetCommittedPixel(VecI posOnImage)
  236. {
  237. lock (lockObject)
  238. {
  239. ThrowIfDisposed();
  240. var chunkPos = OperationHelper.GetChunkPos(posOnImage, FullChunkSize);
  241. var posInChunk = posOnImage - chunkPos * FullChunkSize;
  242. return MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) switch
  243. {
  244. null => Colors.Transparent,
  245. var chunk => chunk.Surface.GetSrgbPixel(posInChunk)
  246. };
  247. }
  248. }
  249. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  250. public Color GetCommittedPixelRaw(VecI posOnImage)
  251. {
  252. lock (lockObject)
  253. {
  254. ThrowIfDisposed();
  255. var chunkPos = OperationHelper.GetChunkPos(posOnImage, FullChunkSize);
  256. var posInChunk = posOnImage - chunkPos * FullChunkSize;
  257. return MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) switch
  258. {
  259. null => Colors.Transparent,
  260. var chunk => chunk.Surface.GetRawPixel(posInChunk)
  261. };
  262. }
  263. }
  264. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  265. public Color GetMostUpToDatePixel(VecI posOnImage)
  266. {
  267. lock (lockObject)
  268. {
  269. ThrowIfDisposed();
  270. var chunkPos = OperationHelper.GetChunkPos(posOnImage, FullChunkSize);
  271. var posInChunk = posOnImage - chunkPos * FullChunkSize;
  272. // nothing queued, return committed
  273. if (queuedOperations.Count == 0)
  274. {
  275. Chunk? committedChunk = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
  276. return committedChunk switch
  277. {
  278. null => Colors.Transparent,
  279. _ => committedChunk.Surface.GetSrgbPixel(posInChunk)
  280. };
  281. }
  282. // something is queued, blend mode is Src so no merging needed
  283. if (blendMode == BlendMode.Src)
  284. {
  285. Chunk? latestChunk = GetLatestChunk(chunkPos, ChunkResolution.Full);
  286. return latestChunk switch
  287. {
  288. null => Colors.Transparent,
  289. _ => latestChunk.Surface.GetSrgbPixel(posInChunk)
  290. };
  291. }
  292. // something is queued, blend mode is not Src so we have to do merging
  293. {
  294. Chunk? committedChunk = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
  295. Chunk? latestChunk = GetLatestChunk(chunkPos, ChunkResolution.Full);
  296. Color committedColor = committedChunk is null
  297. ? Colors.Transparent
  298. : committedChunk.Surface.GetSrgbPixel(posInChunk);
  299. Color latestColor = latestChunk is null
  300. ? Colors.Transparent
  301. : latestChunk.Surface.GetSrgbPixel(posInChunk);
  302. // using a whole chunk just to draw 1 pixel is kinda dumb,
  303. // but this should be faster than any approach that requires allocations
  304. using Chunk tempChunk = Chunk.Create(ProcessingColorSpace, ChunkResolution.Eighth);
  305. using Paint committedPaint = new Paint() { Color = committedColor, BlendMode = BlendMode.Src };
  306. using Paint latestPaint = new Paint() { Color = latestColor, BlendMode = this.blendMode };
  307. tempChunk.Surface.DrawingSurface.Canvas.DrawPixel(VecI.Zero, committedPaint);
  308. tempChunk.Surface.DrawingSurface.Canvas.DrawPixel(VecI.Zero, latestPaint);
  309. return tempChunk.Surface.GetSrgbPixel(VecI.Zero);
  310. }
  311. }
  312. }
  313. /// <returns>
  314. /// True if the chunk existed and was drawn, otherwise false
  315. /// </returns>
  316. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  317. public bool DrawMostUpToDateChunkOn(VecI chunkPos, ChunkResolution resolution, DrawingSurface surface, VecI pos,
  318. Paint? paint = null)
  319. {
  320. lock (lockObject)
  321. {
  322. ThrowIfDisposed();
  323. OneOf<None, EmptyChunk, Chunk> latestChunk;
  324. {
  325. var chunk = GetLatestChunk(chunkPos, resolution);
  326. if (latestChunksData[resolution].TryGetValue(chunkPos, out var chunkData) && chunkData.IsDeleted)
  327. {
  328. latestChunk = new EmptyChunk();
  329. }
  330. else
  331. {
  332. latestChunk = chunk is null ? new None() : chunk;
  333. }
  334. }
  335. var committedChunk = GetCommittedChunk(chunkPos, resolution);
  336. // draw committed directly
  337. if (latestChunk.IsT0 || latestChunk.IsT1 && committedChunk is not null && blendMode != BlendMode.Src)
  338. {
  339. if (committedChunk is null)
  340. return false;
  341. committedChunk.DrawChunkOn(surface, pos, paint);
  342. return true;
  343. }
  344. // no need to combine with committed, draw directly
  345. if (blendMode == BlendMode.Src || committedChunk is null)
  346. {
  347. if (latestChunk.IsT2)
  348. {
  349. latestChunk.AsT2.DrawChunkOn(surface, pos, paint);
  350. return true;
  351. }
  352. return false;
  353. }
  354. // combine with committed and then draw
  355. using var tempChunk = Chunk.Create(ProcessingColorSpace, resolution);
  356. tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(committedChunk.Surface.DrawingSurface, 0, 0,
  357. ReplacingPaint);
  358. blendModePaint.BlendMode = blendMode;
  359. tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(latestChunk.AsT2.Surface.DrawingSurface, 0, 0,
  360. blendModePaint);
  361. if (lockTransparency)
  362. OperationHelper.ClampAlpha(tempChunk.Surface.DrawingSurface, committedChunk.Surface.DrawingSurface);
  363. tempChunk.DrawChunkOn(surface, pos, paint);
  364. return true;
  365. }
  366. }
  367. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  368. public bool LatestOrCommittedChunkExists(VecI chunkPos)
  369. {
  370. lock (lockObject)
  371. {
  372. ThrowIfDisposed();
  373. if (MaybeGetLatestChunk(chunkPos, ChunkResolution.Full) is not null ||
  374. MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is not null)
  375. return true;
  376. foreach (var operation in queuedOperations)
  377. {
  378. if (operation.affectedArea.Chunks.Contains(chunkPos))
  379. return true;
  380. }
  381. return false;
  382. }
  383. }
  384. public bool LatestOrCommittedChunkExists()
  385. {
  386. lock (lockObject)
  387. {
  388. ThrowIfDisposed();
  389. var chunks = FindAllChunks();
  390. foreach (var chunk in chunks)
  391. {
  392. if (LatestOrCommittedChunkExists(chunk))
  393. return true;
  394. }
  395. }
  396. return false;
  397. }
  398. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  399. public bool DrawCommittedChunkOn(VecI chunkPos, ChunkResolution resolution, DrawingSurface surface, VecI pos,
  400. Paint? paint = null)
  401. {
  402. lock (lockObject)
  403. {
  404. ThrowIfDisposed();
  405. var chunk = GetCommittedChunk(chunkPos, resolution);
  406. if (chunk is null)
  407. return false;
  408. chunk.DrawChunkOn(surface, pos, paint);
  409. return true;
  410. }
  411. }
  412. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  413. internal bool CommittedChunkExists(VecI chunkPos)
  414. {
  415. lock (lockObject)
  416. {
  417. ThrowIfDisposed();
  418. return MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is not null;
  419. }
  420. }
  421. /// <summary>
  422. /// Returns the latest version of the chunk if it exists or should exist based on queued operation. The returned chunk is fully up to date.
  423. /// </summary>
  424. private Chunk? GetLatestChunk(VecI pos, ChunkResolution resolution)
  425. {
  426. if (queuedOperations.Count == 0)
  427. return null;
  428. MaybeCreateAndProcessQueueForChunk(pos, resolution);
  429. var maybeNewlyProcessedChunk = MaybeGetLatestChunk(pos, resolution);
  430. return maybeNewlyProcessedChunk;
  431. }
  432. /// <summary>
  433. /// Tries it's best to return a committed chunk, either if it exists or if it can be created from it's high res version. Returns null if it can't.
  434. private Chunk? GetCommittedChunk(VecI pos, ChunkResolution resolution)
  435. {
  436. var maybeSameRes = MaybeGetCommittedChunk(pos, resolution);
  437. if (maybeSameRes is not null)
  438. return maybeSameRes;
  439. var maybeFullRes = MaybeGetCommittedChunk(pos, ChunkResolution.Full);
  440. if (maybeFullRes is not null)
  441. return GetOrCreateCommittedChunk(pos, resolution);
  442. return null;
  443. }
  444. private Chunk? MaybeGetLatestChunk(VecI pos, ChunkResolution resolution)
  445. => latestChunks[resolution].TryGetValue(pos, out Chunk? value) ? value : null;
  446. private Chunk? MaybeGetCommittedChunk(VecI pos, ChunkResolution resolution)
  447. => committedChunks[resolution].TryGetValue(pos, out Chunk? value) ? value : null;
  448. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  449. public void AddRasterClip(ChunkyImage clippingMask)
  450. {
  451. lock (lockObject)
  452. {
  453. ThrowIfDisposed();
  454. if (queuedOperations.Count > 0)
  455. throw new InvalidOperationException(
  456. "This function can only be executed when there are no queued operations");
  457. activeClips.Add(clippingMask);
  458. }
  459. }
  460. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  461. public void SetClippingPath(VectorPath clippingPath)
  462. {
  463. lock (lockObject)
  464. {
  465. ThrowIfDisposed();
  466. if (queuedOperations.Count > 0)
  467. throw new InvalidOperationException(
  468. "This function can only be executed when there are no queued operations");
  469. this.clippingPath = clippingPath;
  470. }
  471. }
  472. /// <summary>
  473. /// Porter duff compositing operators (apart from SrcOver) likely won't have the intended effect.
  474. /// </summary>
  475. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  476. public void SetBlendMode(BlendMode mode)
  477. {
  478. lock (lockObject)
  479. {
  480. ThrowIfDisposed();
  481. if (queuedOperations.Count > 0)
  482. throw new InvalidOperationException(
  483. "This function can only be executed when there are no queued operations");
  484. blendMode = mode;
  485. }
  486. }
  487. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  488. public void SetHorizontalAxisOfSymmetry(double position)
  489. {
  490. lock (lockObject)
  491. {
  492. ThrowIfDisposed();
  493. if (queuedOperations.Count > 0)
  494. throw new InvalidOperationException(
  495. "This function can only be executed when there are no queued operations");
  496. horizontalSymmetryAxis = position;
  497. }
  498. }
  499. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  500. public void SetVerticalAxisOfSymmetry(double position)
  501. {
  502. lock (lockObject)
  503. {
  504. ThrowIfDisposed();
  505. if (queuedOperations.Count > 0)
  506. throw new InvalidOperationException(
  507. "This function can only be executed when there are no queued operations");
  508. verticalSymmetryAxis = position;
  509. }
  510. }
  511. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  512. public void EnableLockTransparency()
  513. {
  514. lock (lockObject)
  515. {
  516. ThrowIfDisposed();
  517. lockTransparency = true;
  518. }
  519. }
  520. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  521. public void EnqueueReplaceColor(Color oldColor, Color newColor)
  522. {
  523. lock (lockObject)
  524. {
  525. ThrowIfDisposed();
  526. ReplaceColorOperation operation = new(oldColor, newColor);
  527. EnqueueOperation(operation);
  528. }
  529. }
  530. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  531. public void EnqueueDrawRectangle(ShapeData rect)
  532. {
  533. lock (lockObject)
  534. {
  535. ThrowIfDisposed();
  536. RectangleOperation operation = new(rect);
  537. EnqueueOperation(operation);
  538. }
  539. }
  540. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  541. public void EnqueueDrawEllipse(RectD location, Paintable? strokeColor, Paintable? fillColor, float strokeWidth,
  542. double rotationRad = 0, bool antiAliased = false,
  543. Paint? paint = null)
  544. {
  545. lock (lockObject)
  546. {
  547. ThrowIfDisposed();
  548. EllipseOperation operation = new(location, strokeColor, fillColor, strokeWidth, rotationRad, antiAliased,
  549. paint);
  550. EnqueueOperation(operation);
  551. }
  552. }
  553. /// <summary>
  554. /// Be careful about the copyImage argument. The default is true, and this is a thread safe version without any side effects.
  555. /// It will however copy the surface right away which can be slow (in updateable changes especially).
  556. /// If copyImage is set to false, the image won't be copied and instead a reference will be stored.
  557. /// Surface is NOT THREAD SAFE, so if you pass a Surface here with copyImage == false you must not do anything with that surface anywhere (not even read) until CommitChanges/CancelChanges is called.
  558. /// </summary>
  559. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  560. public void EnqueueDrawImage(Matrix3X3 transformMatrix, Surface image, Paint? paint = null, bool copyImage = true)
  561. {
  562. lock (lockObject)
  563. {
  564. ThrowIfDisposed();
  565. ImageOperation operation = new(transformMatrix, image, paint, copyImage);
  566. EnqueueOperation(operation);
  567. }
  568. }
  569. /// <summary>
  570. /// Be careful about the copyImage argument, see other overload for details
  571. /// </summary>
  572. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  573. public void EnqueueDrawImage(ShapeCorners corners, Surface image, Paint? paint = null, bool copyImage = true)
  574. {
  575. lock (lockObject)
  576. {
  577. ThrowIfDisposed();
  578. ImageOperation operation = new(corners, image, paint, copyImage);
  579. EnqueueOperation(operation);
  580. }
  581. }
  582. /// <summary>
  583. /// Be careful about the copyImage argument, see other overload for details
  584. /// </summary>
  585. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  586. public void EnqueueDrawImage(VecI pos, Surface image, Paint? paint = null, bool copyImage = true)
  587. {
  588. lock (lockObject)
  589. {
  590. ThrowIfDisposed();
  591. ImageOperation operation = new(pos, image, paint, copyImage);
  592. EnqueueOperation(operation);
  593. }
  594. }
  595. /// <summary>
  596. /// Be careful about the copyImage argument. The default is true, and this is a thread safe version without any side effects.
  597. /// It will however copy the surface right away which can be slow (in updateable changes especially).
  598. /// If copyImage is set to false, the image won't be copied and instead a reference will be stored.
  599. /// Texture is NOT THREAD SAFE, so if you pass a Texture here with copyImage == false you must not do anything with that texture anywhere (not even read) until CommitChanges/CancelChanges is called.
  600. /// </summary>
  601. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  602. public void EnqueueDrawTexture(Matrix3X3 transformMatrix, Texture image, Paint? paint = null, bool copyImage = true)
  603. {
  604. lock (lockObject)
  605. {
  606. ThrowIfDisposed();
  607. TextureOperation operation = new(transformMatrix, image, paint, copyImage);
  608. EnqueueOperation(operation);
  609. }
  610. }
  611. /// <summary>
  612. /// Be careful about the copyImage argument, see other overload for details
  613. /// </summary>
  614. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  615. public void EnqueueDrawTexture(ShapeCorners corners, Texture image, Paint? paint = null, bool copyImage = true)
  616. {
  617. lock (lockObject)
  618. {
  619. ThrowIfDisposed();
  620. TextureOperation operation = new(corners, image, paint, copyImage);
  621. EnqueueOperation(operation);
  622. }
  623. }
  624. /// <summary>
  625. /// Be careful about the copyImage argument, see other overload for details
  626. /// </summary>
  627. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  628. public void EnqueueDrawTexture(VecI pos, Texture image, Paint? paint = null, bool copyImage = true)
  629. {
  630. lock (lockObject)
  631. {
  632. ThrowIfDisposed();
  633. TextureOperation operation = new(pos, image, paint, copyImage);
  634. EnqueueOperation(operation);
  635. }
  636. }
  637. public void EnqueueApplyMask(ChunkyImage mask)
  638. {
  639. lock (lockObject)
  640. {
  641. ThrowIfDisposed();
  642. ApplyMaskOperation operation = new(mask);
  643. EnqueueOperation(operation);
  644. }
  645. }
  646. /// <param name="customBounds">Bounds used for affected chunks, will be computed from path in O(n) if null is passed</param>
  647. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  648. public void EnqueueDrawPath(VectorPath path, Color color, float strokeWidth, StrokeCap strokeCap,
  649. BlendMode blendMode, RectI? customBounds = null)
  650. {
  651. lock (lockObject)
  652. {
  653. ThrowIfDisposed();
  654. PathOperation operation = new(path, color, strokeWidth, strokeCap, blendMode, customBounds);
  655. EnqueueOperation(operation);
  656. }
  657. }
  658. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  659. public void EnqueueDrawBresenhamLine(VecI from, VecI to, Paintable paintable, BlendMode blendMode)
  660. {
  661. lock (lockObject)
  662. {
  663. ThrowIfDisposed();
  664. BresenhamLineOperation operation = new(from, to, paintable, blendMode);
  665. EnqueueOperation(operation);
  666. }
  667. }
  668. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  669. public void EnqueueDrawSkiaLine(VecD from, VecD to, StrokeCap strokeCap, float strokeWidth, Color color,
  670. BlendMode blendMode)
  671. {
  672. lock (lockObject)
  673. {
  674. ThrowIfDisposed();
  675. DrawingSurfaceLineOperation operation = new(from, to, strokeCap, strokeWidth, color, blendMode);
  676. EnqueueOperation(operation);
  677. }
  678. }
  679. public void EnqueueDrawSkiaLine(VecD from, VecD to, Paint paint)
  680. {
  681. lock (lockObject)
  682. {
  683. ThrowIfDisposed();
  684. DrawingSurfaceLineOperation operation = new(from, to, paint);
  685. EnqueueOperation(operation);
  686. }
  687. }
  688. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  689. public void EnqueueDrawPixels(IEnumerable<VecI> pixels, Color color, BlendMode blendMode)
  690. {
  691. lock (lockObject)
  692. {
  693. ThrowIfDisposed();
  694. PixelsOperation operation = new(pixels, color, blendMode);
  695. EnqueueOperation(operation);
  696. }
  697. }
  698. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  699. public void EnqueueDrawPixel(VecI pos, Color color, BlendMode blendMode)
  700. {
  701. lock (lockObject)
  702. {
  703. ThrowIfDisposed();
  704. PixelOperation operation = new(pos, color, blendMode);
  705. EnqueueOperation(operation);
  706. }
  707. }
  708. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  709. public void EnqueueDrawPixel(VecI pos, PixelProcessor pixelProcessor, BlendMode blendMode)
  710. {
  711. lock (lockObject)
  712. {
  713. ThrowIfDisposed();
  714. PixelOperation operation = new(pos, pixelProcessor, GetCommittedPixel, blendMode);
  715. EnqueueOperation(operation);
  716. }
  717. }
  718. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  719. public void EnqueueDrawCommitedChunkyImage(VecI pos, ChunkyImage image, bool flipHor = false, bool flipVer = false)
  720. {
  721. lock (lockObject)
  722. {
  723. ThrowIfDisposed();
  724. ChunkyImageOperation operation = new(image, pos, flipHor, flipVer, false);
  725. EnqueueOperation(operation);
  726. }
  727. }
  728. public void EnqueueDrawUpToDateChunkyImage(VecI pos, ChunkyImage image, bool flipHor = false, bool flipVer = false)
  729. {
  730. ThrowIfDisposed();
  731. ChunkyImageOperation operation = new(image, pos, flipHor, flipVer, true);
  732. EnqueueOperation(operation);
  733. }
  734. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  735. public void EnqueueClearRegion(RectI region)
  736. {
  737. lock (lockObject)
  738. {
  739. ThrowIfDisposed();
  740. ClearRegionOperation operation = new(region);
  741. EnqueueOperation(operation);
  742. }
  743. }
  744. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  745. public void EnqueueClearPath(VectorPath path, RectI? pathTightBounds = null)
  746. {
  747. lock (lockObject)
  748. {
  749. ThrowIfDisposed();
  750. ClearPathOperation operation = new(path, pathTightBounds);
  751. EnqueueOperation(operation);
  752. }
  753. }
  754. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  755. public void EnqueueClear()
  756. {
  757. lock (lockObject)
  758. {
  759. ThrowIfDisposed();
  760. ClearOperation operation = new();
  761. EnqueueOperation(operation, new(FindAllChunks()));
  762. }
  763. }
  764. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  765. public void EnqueueResize(VecI newSize)
  766. {
  767. lock (lockObject)
  768. {
  769. ThrowIfDisposed();
  770. ResizeOperation operation = new(newSize);
  771. LatestSize = newSize;
  772. EnqueueOperation(operation, new(FindAllChunksOutsideBounds(newSize)));
  773. }
  774. }
  775. public void EnqueueDrawPaint(Paint paint)
  776. {
  777. lock (lockObject)
  778. {
  779. ThrowIfDisposed();
  780. PaintOperation operation = new(paint);
  781. EnqueueOperation(operation);
  782. }
  783. }
  784. private void EnqueueOperation(IDrawOperation operation)
  785. {
  786. List<IDrawOperation> operations = new(4) { operation };
  787. if (operation is IMirroredDrawOperation mirroredOperation)
  788. {
  789. if (horizontalSymmetryAxis is not null && verticalSymmetryAxis is not null)
  790. operations.Add(mirroredOperation.AsMirrored(verticalSymmetryAxis, horizontalSymmetryAxis));
  791. if (horizontalSymmetryAxis is not null)
  792. operations.Add(mirroredOperation.AsMirrored(null, horizontalSymmetryAxis));
  793. if (verticalSymmetryAxis is not null)
  794. operations.Add(mirroredOperation.AsMirrored(verticalSymmetryAxis, null));
  795. }
  796. foreach (var op in operations)
  797. {
  798. var area = op.FindAffectedArea(LatestSize);
  799. area.Chunks.RemoveWhere(pos => IsOutsideBounds(pos, LatestSize));
  800. area.GlobalArea = area.GlobalArea?.Intersect(new RectI(VecI.Zero, LatestSize));
  801. if (operation.IgnoreEmptyChunks)
  802. area.Chunks.IntersectWith(FindAllChunks());
  803. EnqueueOperation(op, area);
  804. operationCounter++;
  805. }
  806. }
  807. private void EnqueueOperation(IOperation operation, AffectedArea area)
  808. {
  809. queuedOperations.Add((operation, area));
  810. }
  811. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  812. public void CancelChanges()
  813. {
  814. lock (lockObject)
  815. {
  816. ThrowIfDisposed();
  817. //clear queued operations
  818. foreach (var operation in queuedOperations)
  819. operation.operation.Dispose();
  820. queuedOperations.Clear();
  821. //clear additional state
  822. activeClips.Clear();
  823. blendMode = BlendMode.Src;
  824. lockTransparency = false;
  825. horizontalSymmetryAxis = null;
  826. verticalSymmetryAxis = null;
  827. clippingPath = null;
  828. //clear latest chunks
  829. foreach (var (_, chunksOfRes) in latestChunks)
  830. {
  831. foreach (var (_, chunk) in chunksOfRes)
  832. {
  833. chunk.Dispose();
  834. }
  835. }
  836. LatestSize = CommittedSize;
  837. foreach (var (res, chunks) in latestChunks)
  838. {
  839. chunks.Clear();
  840. latestChunksData[res].Clear();
  841. }
  842. }
  843. }
  844. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  845. public void CommitChanges()
  846. {
  847. lock (lockObject)
  848. {
  849. ThrowIfDisposed();
  850. var affectedArea = FindAffectedArea();
  851. foreach (var chunk in affectedArea.Chunks)
  852. {
  853. MaybeCreateAndProcessQueueForChunk(chunk, ChunkResolution.Full);
  854. }
  855. foreach (var (operation, _) in queuedOperations)
  856. {
  857. operation.Dispose();
  858. }
  859. CommitLatestChunks();
  860. CommittedSize = LatestSize;
  861. queuedOperations.Clear();
  862. activeClips.Clear();
  863. blendMode = BlendMode.Src;
  864. lockTransparency = false;
  865. horizontalSymmetryAxis = null;
  866. verticalSymmetryAxis = null;
  867. clippingPath = null;
  868. commitCounter++;
  869. if (commitCounter % 30 == 0)
  870. FindAndDeleteEmptyCommittedChunks();
  871. }
  872. }
  873. /// <summary>
  874. /// Does all necessary steps to convert latest chunks into committed ones. The latest chunk dictionary become empty after this function is called.
  875. /// </summary>
  876. private void CommitLatestChunks()
  877. {
  878. // move/draw fully processed latest chunks to/on committed
  879. foreach (var (resolution, chunks) in latestChunks)
  880. {
  881. foreach (var (pos, chunk) in chunks)
  882. {
  883. // get chunk if exists
  884. LatestChunkData data = latestChunksData[resolution][pos];
  885. if (data.QueueProgress != queuedOperations.Count)
  886. {
  887. if (resolution == ChunkResolution.Full)
  888. {
  889. throw new InvalidOperationException(
  890. "Trying to commit a full res chunk that wasn't fully processed");
  891. }
  892. else
  893. {
  894. chunk.Dispose();
  895. continue;
  896. }
  897. }
  898. // do a swap
  899. if (blendMode == BlendMode.Src)
  900. {
  901. // delete committed version
  902. if (committedChunks[resolution].ContainsKey(pos))
  903. {
  904. var oldChunk = committedChunks[resolution][pos];
  905. committedChunks[resolution].Remove(pos);
  906. oldChunk.Dispose();
  907. }
  908. // put the latest version in place of the committed one
  909. if (!data.IsDeleted)
  910. committedChunks[resolution].Add(pos, chunk);
  911. else
  912. chunk.Dispose();
  913. }
  914. // do blending
  915. else
  916. {
  917. // nothing to blend, continue
  918. if (data.IsDeleted)
  919. {
  920. chunk.Dispose();
  921. continue;
  922. }
  923. // nothing to blend with, swap
  924. var maybeCommitted = MaybeGetCommittedChunk(pos, resolution);
  925. if (maybeCommitted is null)
  926. {
  927. committedChunks[resolution].Add(pos, chunk);
  928. continue;
  929. }
  930. //blend
  931. blendModePaint.BlendMode = blendMode;
  932. if (lockTransparency)
  933. {
  934. using Chunk tempChunk = Chunk.Create(ProcessingColorSpace, resolution);
  935. tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(maybeCommitted.Surface.DrawingSurface, 0, 0,
  936. ReplacingPaint);
  937. maybeCommitted.Surface.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, 0, 0,
  938. blendModePaint);
  939. OperationHelper.ClampAlpha(maybeCommitted.Surface.DrawingSurface,
  940. tempChunk.Surface.DrawingSurface);
  941. }
  942. else
  943. {
  944. maybeCommitted.Surface.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, 0, 0,
  945. blendModePaint);
  946. }
  947. chunk.Dispose();
  948. }
  949. }
  950. }
  951. // delete committed low res chunks that weren't updated
  952. foreach (var (pos, _) in latestChunks[ChunkResolution.Full])
  953. {
  954. foreach (var (resolution, _) in latestChunks)
  955. {
  956. if (resolution == ChunkResolution.Full)
  957. continue;
  958. if (!latestChunksData[resolution].TryGetValue(pos, out var halfChunk) ||
  959. halfChunk.QueueProgress != queuedOperations.Count)
  960. {
  961. if (committedChunks[resolution].TryGetValue(pos, out var committedLowResChunk))
  962. {
  963. committedChunks[resolution].Remove(pos);
  964. committedLowResChunk.Dispose();
  965. }
  966. }
  967. }
  968. }
  969. // clear latest chunks
  970. foreach (var (resolution, chunks) in latestChunks)
  971. {
  972. chunks.Clear();
  973. latestChunksData[resolution].Clear();
  974. }
  975. }
  976. /// <returns>
  977. /// All chunks that have something in them, including latest (uncommitted) ones
  978. /// </returns>
  979. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  980. public HashSet<VecI> FindAllChunks()
  981. {
  982. lock (lockObject)
  983. {
  984. ThrowIfDisposed();
  985. var allChunks = committedChunks[ChunkResolution.Full].Select(chunk => chunk.Key).ToHashSet();
  986. foreach (var (_, affArea) in queuedOperations)
  987. {
  988. allChunks.UnionWith(affArea.Chunks);
  989. }
  990. return allChunks;
  991. }
  992. }
  993. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  994. public HashSet<VecI> FindCommittedChunks()
  995. {
  996. lock (lockObject)
  997. {
  998. ThrowIfDisposed();
  999. return committedChunks[ChunkResolution.Full].Select(chunk => chunk.Key).ToHashSet();
  1000. }
  1001. }
  1002. /// <returns>
  1003. /// Chunks affected by operations that haven't been committed yet
  1004. /// </returns>
  1005. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  1006. public AffectedArea FindAffectedArea(int fromOperationIndex = 0)
  1007. {
  1008. lock (lockObject)
  1009. {
  1010. ThrowIfDisposed();
  1011. var chunks = new HashSet<VecI>();
  1012. RectI? rect = null;
  1013. for (int i = fromOperationIndex; i < queuedOperations.Count; i++)
  1014. {
  1015. var (_, area) = queuedOperations[i];
  1016. chunks.UnionWith(area.Chunks);
  1017. rect ??= area.GlobalArea;
  1018. if (area.GlobalArea is not null && rect is not null)
  1019. rect = rect.Value.Union(area.GlobalArea.Value);
  1020. }
  1021. return new AffectedArea(chunks, rect);
  1022. }
  1023. }
  1024. public void SetCommitedChunk(Chunk chunk, VecI pos, ChunkResolution resolution)
  1025. {
  1026. lock (lockObject)
  1027. {
  1028. ThrowIfDisposed();
  1029. committedChunks[resolution][pos] = chunk;
  1030. }
  1031. }
  1032. /// <summary>
  1033. /// Applies all operations queued for a specific (latest) chunk. If the latest chunk doesn't exist yet, creates it. If none of the existing operations affect the chunk does nothing.
  1034. /// </summary>
  1035. private void MaybeCreateAndProcessQueueForChunk(VecI chunkPos, ChunkResolution resolution)
  1036. {
  1037. if (!latestChunksData[resolution].TryGetValue(chunkPos, out LatestChunkData chunkData))
  1038. chunkData = new()
  1039. {
  1040. QueueProgress = 0, IsDeleted = !committedChunks[ChunkResolution.Full].ContainsKey(chunkPos)
  1041. };
  1042. if (chunkData.QueueProgress == queuedOperations.Count)
  1043. return;
  1044. Chunk? targetChunk = null;
  1045. OneOf<FilledChunk, EmptyChunk, Chunk> combinedRasterClips = new FilledChunk();
  1046. bool initialized = false;
  1047. for (int i = 0; i < queuedOperations.Count; i++)
  1048. {
  1049. var (operation, affArea) = queuedOperations[i];
  1050. if (!affArea.Chunks.Contains(chunkPos))
  1051. continue;
  1052. if (!initialized)
  1053. {
  1054. initialized = true;
  1055. targetChunk = GetOrCreateLatestChunk(chunkPos, resolution);
  1056. combinedRasterClips = CombineRasterClipsForChunk(chunkPos, resolution);
  1057. }
  1058. if (chunkData.QueueProgress <= i)
  1059. chunkData.IsDeleted = ApplyOperationToChunk(operation, affArea, combinedRasterClips, targetChunk!,
  1060. chunkPos, resolution, chunkData);
  1061. }
  1062. if (initialized)
  1063. {
  1064. if (lockTransparency && !chunkData.IsDeleted &&
  1065. MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is not null)
  1066. {
  1067. var committed = GetCommittedChunk(chunkPos, resolution);
  1068. OperationHelper.ClampAlpha(targetChunk!.Surface.DrawingSurface, committed!.Surface.DrawingSurface);
  1069. }
  1070. chunkData.QueueProgress = queuedOperations.Count;
  1071. latestChunksData[resolution][chunkPos] = chunkData;
  1072. }
  1073. if (combinedRasterClips.TryPickT2(out Chunk value, out var _))
  1074. value.Dispose();
  1075. }
  1076. private OneOf<FilledChunk, EmptyChunk, Chunk> CombineRasterClipsForChunk(VecI chunkPos, ChunkResolution resolution)
  1077. {
  1078. if (lockTransparency && MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is null)
  1079. {
  1080. return new EmptyChunk();
  1081. }
  1082. if (activeClips.Count == 0)
  1083. {
  1084. return new FilledChunk();
  1085. }
  1086. var intersection = Chunk.Create(ProcessingColorSpace, resolution);
  1087. intersection.Surface.DrawingSurface.Canvas.Clear(Colors.White);
  1088. foreach (var mask in activeClips)
  1089. {
  1090. if (mask.CommittedChunkExists(chunkPos))
  1091. {
  1092. mask.DrawCommittedChunkOn(chunkPos, resolution, intersection.Surface.DrawingSurface, VecI.Zero,
  1093. ClippingPaint);
  1094. }
  1095. else
  1096. {
  1097. intersection.Dispose();
  1098. return new EmptyChunk();
  1099. }
  1100. }
  1101. return intersection;
  1102. }
  1103. /// <returns>
  1104. /// True if the chunk was fully cleared (and should be deleted).
  1105. /// </returns>
  1106. private bool ApplyOperationToChunk(
  1107. IOperation operation,
  1108. AffectedArea operationAffectedArea,
  1109. OneOf<FilledChunk, EmptyChunk, Chunk> combinedRasterClips,
  1110. Chunk targetChunk,
  1111. VecI chunkPos,
  1112. ChunkResolution resolution,
  1113. LatestChunkData chunkData)
  1114. {
  1115. if (operation is ClearOperation)
  1116. return true;
  1117. if (operation is IDrawOperation chunkOperation)
  1118. {
  1119. if (combinedRasterClips.IsT1) // Nothing is visible
  1120. return chunkData.IsDeleted;
  1121. if (chunkData.IsDeleted)
  1122. targetChunk.Surface.DrawingSurface.Canvas.Clear();
  1123. // just regular drawing
  1124. if (combinedRasterClips.IsT0) // Everything is visible as far as the raster clips are concerned
  1125. {
  1126. CallDrawWithClip(chunkOperation, operationAffectedArea.GlobalArea, targetChunk, resolution, chunkPos);
  1127. return false;
  1128. }
  1129. // drawing with raster clipping
  1130. var clip = combinedRasterClips.AsT2;
  1131. using var tempChunk = Chunk.Create(ProcessingColorSpace, targetChunk.Resolution);
  1132. targetChunk.DrawChunkOn(tempChunk.Surface.DrawingSurface, VecI.Zero, ReplacingPaint);
  1133. CallDrawWithClip(chunkOperation, operationAffectedArea.GlobalArea, tempChunk, resolution, chunkPos);
  1134. clip.DrawChunkOn(tempChunk.Surface.DrawingSurface, VecI.Zero, ClippingPaint);
  1135. clip.DrawChunkOn(targetChunk.Surface.DrawingSurface, VecI.Zero, InverseClippingPaint);
  1136. tempChunk.DrawChunkOn(targetChunk.Surface.DrawingSurface, VecI.Zero, AddingPaint);
  1137. return false;
  1138. }
  1139. if (operation is ResizeOperation resizeOperation)
  1140. {
  1141. return IsOutsideBounds(chunkPos, resizeOperation.Size);
  1142. }
  1143. return chunkData.IsDeleted;
  1144. }
  1145. private void CallDrawWithClip(IDrawOperation operation, RectI? operationAffectedArea, Chunk targetChunk,
  1146. ChunkResolution resolution, VecI chunkPos)
  1147. {
  1148. if (operationAffectedArea is null)
  1149. return;
  1150. int count = targetChunk.Surface.DrawingSurface.Canvas.Save();
  1151. float scale = (float)resolution.Multiplier();
  1152. if (clippingPath is not null && !clippingPath.IsEmpty)
  1153. {
  1154. using VectorPath transformedPath = new(clippingPath);
  1155. VecD trans = -chunkPos * FullChunkSize * scale;
  1156. transformedPath.Transform(Matrix3X3.CreateScaleTranslation(scale, scale, (float)trans.X, (float)trans.Y));
  1157. targetChunk.Surface.DrawingSurface.Canvas.ClipPath(transformedPath);
  1158. }
  1159. VecD affectedAreaPos = operationAffectedArea.Value.TopLeft;
  1160. VecD affectedAreaSize = operationAffectedArea.Value.Size;
  1161. affectedAreaPos = (affectedAreaPos - chunkPos * FullChunkSize) * scale;
  1162. affectedAreaSize = affectedAreaSize * scale;
  1163. targetChunk.Surface.DrawingSurface.Canvas.ClipRect(new RectD(affectedAreaPos, affectedAreaSize));
  1164. operation.DrawOnChunk(targetChunk, chunkPos);
  1165. targetChunk.Surface.DrawingSurface.Canvas.RestoreToCount(count);
  1166. }
  1167. /// <summary>
  1168. /// Finds and deletes empty committed chunks. Returns true if all existing chunks were deleted.
  1169. /// Note: this function modifies the internal state, it is not thread safe! Use it only in changes (same as all the other functions that change the image in some way).
  1170. /// </summary>
  1171. /// <exception cref="ObjectDisposedException">This image is disposed</exception>
  1172. public bool CheckIfCommittedIsEmpty()
  1173. {
  1174. lock (lockObject)
  1175. {
  1176. ThrowIfDisposed();
  1177. if (queuedOperations.Count > 0)
  1178. throw new InvalidOperationException(
  1179. "This function can only be used when there are no queued operations");
  1180. FindAndDeleteEmptyCommittedChunks();
  1181. return committedChunks[ChunkResolution.Full].Count == 0;
  1182. }
  1183. }
  1184. private HashSet<VecI> FindAllChunksOutsideBounds(VecI size)
  1185. {
  1186. var chunks = FindAllChunks();
  1187. chunks.RemoveWhere(pos => !IsOutsideBounds(pos, size));
  1188. return chunks;
  1189. }
  1190. private static bool IsOutsideBounds(VecI chunkPos, VecI imageSize)
  1191. {
  1192. return chunkPos.X < 0 || chunkPos.Y < 0 || chunkPos.X * FullChunkSize >= imageSize.X ||
  1193. chunkPos.Y * FullChunkSize >= imageSize.Y;
  1194. }
  1195. private void FindAndDeleteEmptyCommittedChunks()
  1196. {
  1197. if (queuedOperations.Count != 0)
  1198. throw new InvalidOperationException("This method cannot be used while any operations are queued");
  1199. HashSet<VecI> toRemove = new();
  1200. foreach (var (pos, chunk) in committedChunks[ChunkResolution.Full])
  1201. {
  1202. if (chunk.Surface.IsFullyTransparent())
  1203. {
  1204. toRemove.Add(pos);
  1205. chunk.Dispose();
  1206. }
  1207. }
  1208. foreach (var pos in toRemove)
  1209. {
  1210. committedChunks[ChunkResolution.Full].Remove(pos);
  1211. committedChunks[ChunkResolution.Half].Remove(pos);
  1212. committedChunks[ChunkResolution.Quarter].Remove(pos);
  1213. committedChunks[ChunkResolution.Eighth].Remove(pos);
  1214. }
  1215. }
  1216. /// <summary>
  1217. /// Gets existing committed chunk or creates a new one. Doesn't apply any operations to the chunk, returns it as it is.
  1218. /// </summary>
  1219. private Chunk GetOrCreateCommittedChunk(VecI chunkPos, ChunkResolution resolution)
  1220. {
  1221. // committed chunk of the same resolution exists
  1222. Chunk? targetChunk = MaybeGetCommittedChunk(chunkPos, resolution);
  1223. if (targetChunk is not null)
  1224. return targetChunk;
  1225. // for full res chunks: nothing exists, create brand new chunk
  1226. if (resolution == ChunkResolution.Full)
  1227. {
  1228. var newChunk = Chunk.Create(ProcessingColorSpace, resolution);
  1229. committedChunks[resolution][chunkPos] = newChunk;
  1230. return newChunk;
  1231. }
  1232. // for low res chunks: full res version exists
  1233. Chunk? existingFullResChunk = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
  1234. if (existingFullResChunk is not null)
  1235. {
  1236. var newChunk = Chunk.Create(ProcessingColorSpace, resolution);
  1237. newChunk.Surface.DrawingSurface.Canvas.Save();
  1238. newChunk.Surface.DrawingSurface.Canvas.Scale((float)resolution.Multiplier());
  1239. newChunk.Surface.DrawingSurface.Canvas.DrawSurface(existingFullResChunk.Surface.DrawingSurface, 0, 0,
  1240. SmoothReplacingPaint);
  1241. newChunk.Surface.DrawingSurface.Canvas.Restore();
  1242. committedChunks[resolution][chunkPos] = newChunk;
  1243. return newChunk;
  1244. }
  1245. // for low res chunks: full res version doesn't exist
  1246. {
  1247. GetOrCreateCommittedChunk(chunkPos, ChunkResolution.Full);
  1248. var newChunk = Chunk.Create(ProcessingColorSpace, resolution);
  1249. committedChunks[resolution][chunkPos] = newChunk;
  1250. return newChunk;
  1251. }
  1252. }
  1253. /// <summary>
  1254. /// Gets existing latest chunk or creates a new one, based on a committed one if it exists. Doesn't do any operations to the chunk.
  1255. /// </summary>
  1256. private Chunk GetOrCreateLatestChunk(VecI chunkPos, ChunkResolution resolution)
  1257. {
  1258. // latest chunk exists
  1259. Chunk? targetChunk = MaybeGetLatestChunk(chunkPos, resolution);
  1260. if (targetChunk is not null)
  1261. return targetChunk;
  1262. // committed chunk of the same resolution exists
  1263. var maybeCommittedAnyRes = MaybeGetCommittedChunk(chunkPos, resolution);
  1264. if (maybeCommittedAnyRes is not null)
  1265. {
  1266. Chunk newChunk = Chunk.Create(ProcessingColorSpace, resolution);
  1267. if (blendMode == BlendMode.Src)
  1268. maybeCommittedAnyRes.Surface.CopyTo(newChunk.Surface);
  1269. else
  1270. newChunk.Surface.DrawingSurface.Canvas.Clear();
  1271. latestChunks[resolution][chunkPos] = newChunk;
  1272. return newChunk;
  1273. }
  1274. // committed chunk of full resolution exists
  1275. var maybeCommittedFullRes = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
  1276. if (maybeCommittedFullRes is not null)
  1277. {
  1278. //create low res committed chunk
  1279. var committedChunkLowRes = GetOrCreateCommittedChunk(chunkPos, resolution);
  1280. //create latest based on it
  1281. Chunk newChunk = Chunk.Create(ProcessingColorSpace, resolution);
  1282. committedChunkLowRes.Surface.CopyTo(newChunk.Surface);
  1283. latestChunks[resolution][chunkPos] = newChunk;
  1284. return newChunk;
  1285. }
  1286. // no previous chunks exist
  1287. var newLatestChunk = Chunk.Create(ProcessingColorSpace, resolution);
  1288. newLatestChunk.Surface.DrawingSurface.Canvas.Clear();
  1289. latestChunks[resolution][chunkPos] = newLatestChunk;
  1290. return newLatestChunk;
  1291. }
  1292. private void ThrowIfDisposed()
  1293. {
  1294. if (disposed)
  1295. throw new ObjectDisposedException(nameof(ChunkyImage));
  1296. }
  1297. public void Dispose()
  1298. {
  1299. lock (lockObject)
  1300. {
  1301. if (disposed)
  1302. return;
  1303. CancelChanges();
  1304. DisposeAll();
  1305. blendModePaint.Dispose();
  1306. GC.SuppressFinalize(this);
  1307. }
  1308. }
  1309. private void DisposeAll()
  1310. {
  1311. foreach (var (_, chunks) in committedChunks)
  1312. {
  1313. foreach (var (_, chunk) in chunks)
  1314. {
  1315. chunk.Dispose();
  1316. }
  1317. }
  1318. foreach (var (_, chunks) in latestChunks)
  1319. {
  1320. foreach (var (_, chunk) in chunks)
  1321. {
  1322. chunk.Dispose();
  1323. }
  1324. }
  1325. disposed = true;
  1326. }
  1327. public object Clone()
  1328. {
  1329. lock (lockObject)
  1330. {
  1331. ThrowIfDisposed();
  1332. ChunkyImage clone = CloneFromCommitted();
  1333. return clone;
  1334. }
  1335. }
  1336. public int GetCacheHash()
  1337. {
  1338. return commitCounter + queuedOperations.Count + operationCounter + activeClips.Count
  1339. + (int)blendMode + (lockTransparency ? 1 : 0)
  1340. + (horizontalSymmetryAxis is not null ? (int)(horizontalSymmetryAxis * 100) : 0)
  1341. + (verticalSymmetryAxis is not null ? (int)(verticalSymmetryAxis * 100) : 0)
  1342. + (clippingPath is not null ? 1 : 0);
  1343. }
  1344. }