From 3fabb76ab62b4412036de1a687dd38c9bea27928 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 8 Oct 2023 21:32:33 +1000 Subject: [PATCH 01/24] Pool Vp8LHistogram memory. --- .../Webp/Lossless/BackwardReferenceEncoder.cs | 47 +-- .../Formats/Webp/Lossless/CostModel.cs | 16 +- .../Formats/Webp/Lossless/HistogramEncoder.cs | 189 ++++++------ .../Formats/Webp/Lossless/HuffmanUtils.cs | 18 +- .../Formats/Webp/Lossless/PixOrCopy.cs | 2 +- .../Formats/Webp/Lossless/Vp8LBitEntropy.cs | 6 +- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 117 +++++--- .../Formats/Webp/Lossless/Vp8LHistogram.cs | 273 +++++++++++------- .../Formats/Webp/Lossless/Vp8LHistogramSet.cs | 122 ++++++++ .../Codecs/Webp/EncodeWebp.cs | 13 +- .../Formats/WebP/DominantCostRangeTests.cs | 10 +- .../Formats/WebP/Vp8LHistogramTests.cs | 14 +- 12 files changed, 539 insertions(+), 288 deletions(-) create mode 100644 src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs diff --git a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs index 61133142bf..922ae0193d 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs @@ -50,8 +50,8 @@ internal static class BackwardReferenceEncoder double bitCostBest = -1; int cacheBitsInitial = cacheBits; Vp8LHashChain? hashChainBox = null; - var stats = new Vp8LStreaks(); - var bitsEntropy = new Vp8LBitEntropy(); + Vp8LStreaks stats = new(); + Vp8LBitEntropy bitsEntropy = new(); for (int lz77Type = 1; lz77TypesToTry > 0; lz77TypesToTry &= ~lz77Type, lz77Type <<= 1) { int cacheBitsTmp = cacheBitsInitial; @@ -76,21 +76,19 @@ internal static class BackwardReferenceEncoder } // Next, try with a color cache and update the references. - cacheBitsTmp = CalculateBestCacheSize(bgra, quality, worst, cacheBitsTmp); + cacheBitsTmp = CalculateBestCacheSize(memoryAllocator, bgra, quality, worst, cacheBitsTmp); if (cacheBitsTmp > 0) { BackwardRefsWithLocalCache(bgra, cacheBitsTmp, worst); } // Keep the best backward references. - var histo = new Vp8LHistogram(worst, cacheBitsTmp); + using Vp8LHistogram histo = new(memoryAllocator, worst, cacheBitsTmp); double bitCost = histo.EstimateBits(stats, bitsEntropy); if (lz77TypeBest == 0 || bitCost < bitCostBest) { - Vp8LBackwardRefs tmp = worst; - worst = best; - best = tmp; + (best, worst) = (worst, best); bitCostBest = bitCost; cacheBits = cacheBitsTmp; lz77TypeBest = lz77Type; @@ -102,7 +100,7 @@ internal static class BackwardReferenceEncoder { Vp8LHashChain hashChainTmp = lz77TypeBest == (int)Vp8LLz77Type.Lz77Standard ? hashChain : hashChainBox!; BackwardReferencesTraceBackwards(width, height, memoryAllocator, bgra, cacheBits, hashChainTmp, best, worst); - var histo = new Vp8LHistogram(worst, cacheBits); + using Vp8LHistogram histo = new(memoryAllocator, worst, cacheBits); double bitCostTrace = histo.EstimateBits(stats, bitsEntropy); if (bitCostTrace < bitCostBest) { @@ -123,7 +121,12 @@ internal static class BackwardReferenceEncoder /// The local color cache is also disabled for the lower (smaller then 25) quality. /// /// Best cache size. - private static int CalculateBestCacheSize(ReadOnlySpan bgra, uint quality, Vp8LBackwardRefs refs, int bestCacheBits) + private static int CalculateBestCacheSize( + MemoryAllocator memoryAllocator, + ReadOnlySpan bgra, + uint quality, + Vp8LBackwardRefs refs, + int bestCacheBits) { int cacheBitsMax = quality <= 25 ? 0 : bestCacheBits; if (cacheBitsMax == 0) @@ -134,11 +137,15 @@ internal static class BackwardReferenceEncoder double entropyMin = MaxEntropy; int pos = 0; - var colorCache = new ColorCache[WebpConstants.MaxColorCacheBits + 1]; - var histos = new Vp8LHistogram[WebpConstants.MaxColorCacheBits + 1]; + + // TODO: Pass from outer loop and clear. + ColorCache[] colorCache = new ColorCache[WebpConstants.MaxColorCacheBits + 1]; + + // TODO: Use fixed size. + using Vp8LHistogramSet histos = new(memoryAllocator, WebpConstants.MaxColorCacheBits + 1, 0); for (int i = 0; i <= WebpConstants.MaxColorCacheBits; i++) { - histos[i] = new Vp8LHistogram(paletteCodeBits: i); + histos[i].PaletteCodeBits = i; colorCache[i] = new ColorCache(i); } @@ -149,10 +156,10 @@ internal static class BackwardReferenceEncoder if (v.IsLiteral()) { uint pix = bgra[pos++]; - uint a = (pix >> 24) & 0xff; - uint r = (pix >> 16) & 0xff; - uint g = (pix >> 8) & 0xff; - uint b = (pix >> 0) & 0xff; + int a = (int)(pix >> 24) & 0xff; + int r = (int)(pix >> 16) & 0xff; + int g = (int)(pix >> 8) & 0xff; + int b = (int)(pix >> 0) & 0xff; // The keys of the caches can be derived from the longest one. int key = ColorCache.HashPix(pix, 32 - cacheBitsMax); @@ -218,8 +225,8 @@ internal static class BackwardReferenceEncoder } } - var stats = new Vp8LStreaks(); - var bitsEntropy = new Vp8LBitEntropy(); + Vp8LStreaks stats = new(); + Vp8LBitEntropy bitsEntropy = new(); for (int i = 0; i <= cacheBitsMax; i++) { double entropy = histos[i].EstimateBits(stats, bitsEntropy); @@ -266,7 +273,7 @@ internal static class BackwardReferenceEncoder int pixCount = xSize * ySize; bool useColorCache = cacheBits > 0; int literalArraySize = WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes + (cacheBits > 0 ? 1 << cacheBits : 0); - var costModel = new CostModel(literalArraySize); + CostModel costModel = new(memoryAllocator, literalArraySize); int offsetPrev = -1; int lenPrev = -1; double offsetCost = -1; @@ -280,7 +287,7 @@ internal static class BackwardReferenceEncoder } costModel.Build(xSize, cacheBits, refs); - using var costManager = new CostManager(memoryAllocator, distArrayBuffer, pixCount, costModel); + using CostManager costManager = new(memoryAllocator, distArrayBuffer, pixCount, costModel); Span costManagerCosts = costManager.Costs.GetSpan(); Span distArray = distArrayBuffer.GetSpan(); diff --git a/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs b/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs index c99e8fe6e2..975fd581d7 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs @@ -1,18 +1,23 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Memory; + namespace SixLabors.ImageSharp.Formats.Webp.Lossless; internal class CostModel { + private readonly MemoryAllocator memoryAllocator; private const int ValuesInBytes = 256; /// /// Initializes a new instance of the class. /// + /// The memory allocator. /// The literal array size. - public CostModel(int literalArraySize) + public CostModel(MemoryAllocator memoryAllocator, int literalArraySize) { + this.memoryAllocator = memoryAllocator; this.Alpha = new double[ValuesInBytes]; this.Red = new double[ValuesInBytes]; this.Blue = new double[ValuesInBytes]; @@ -32,13 +37,12 @@ internal class CostModel public void Build(int xSize, int cacheBits, Vp8LBackwardRefs backwardRefs) { - var histogram = new Vp8LHistogram(cacheBits); - using System.Collections.Generic.List.Enumerator refsEnumerator = backwardRefs.Refs.GetEnumerator(); + using Vp8LHistogram histogram = new(this.memoryAllocator, cacheBits); // The following code is similar to HistogramCreate but converts the distance to plane code. - while (refsEnumerator.MoveNext()) + for (int i = 0; i < backwardRefs.Refs.Count; i++) { - histogram.AddSinglePixOrCopy(refsEnumerator.Current, true, xSize); + histogram.AddSinglePixOrCopy(backwardRefs.Refs[i], true, xSize); } ConvertPopulationCountTableToBitEstimates(histogram.NumCodes(), histogram.Literal, this.Literal); @@ -70,7 +74,7 @@ internal class CostModel public double GetLiteralCost(uint v) => this.Alpha[v >> 24] + this.Red[(v >> 16) & 0xff] + this.Literal[(v >> 8) & 0xff] + this.Blue[v & 0xff]; - private static void ConvertPopulationCountTableToBitEstimates(int numSymbols, uint[] populationCounts, double[] output) + private static void ConvertPopulationCountTableToBitEstimates(int numSymbols, Span populationCounts, double[] output) { uint sum = 0; int nonzeros = 0; diff --git a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs index dd59ed2097..8a0d132063 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs @@ -3,6 +3,7 @@ #nullable disable using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Webp.Lossless; @@ -27,19 +28,28 @@ internal static class HistogramEncoder private const ushort InvalidHistogramSymbol = ushort.MaxValue; - public static void GetHistoImageSymbols(int xSize, int ySize, Vp8LBackwardRefs refs, uint quality, int histoBits, int cacheBits, List imageHisto, Vp8LHistogram tmpHisto, Span histogramSymbols) + public static void GetHistoImageSymbols( + MemoryAllocator memoryAllocator, + int xSize, + int ySize, + Vp8LBackwardRefs refs, + uint quality, + int histoBits, + int cacheBits, + Vp8LHistogramSet imageHisto, + Vp8LHistogram tmpHisto, + Span histogramSymbols) { int histoXSize = histoBits > 0 ? LosslessUtils.SubSampleSize(xSize, histoBits) : 1; int histoYSize = histoBits > 0 ? LosslessUtils.SubSampleSize(ySize, histoBits) : 1; int imageHistoRawSize = histoXSize * histoYSize; - int entropyCombineNumBins = BinSize; + const int entropyCombineNumBins = BinSize; + + // TODO: Allocations! ushort[] mapTmp = new ushort[imageHistoRawSize]; ushort[] clusterMappings = new ushort[imageHistoRawSize]; - var origHisto = new List(imageHistoRawSize); - for (int i = 0; i < imageHistoRawSize; i++) - { - origHisto.Add(new Vp8LHistogram(cacheBits)); - } + + using Vp8LHistogramSet origHisto = new(memoryAllocator, imageHistoRawSize, cacheBits); // Construct the histograms from the backward references. HistogramBuild(xSize, histoBits, refs, origHisto); @@ -61,7 +71,7 @@ internal static class HistogramEncoder OptimizeHistogramSymbols(clusterMappings, numClusters, mapTmp, histogramSymbols); } - float x = quality / 100.0f; + float x = quality / 100F; // Cubic ramp between 1 and MaxHistoGreedy: int thresholdSize = (int)(1 + (x * x * x * (MaxHistoGreedy - 1))); @@ -77,26 +87,25 @@ internal static class HistogramEncoder HistogramRemap(origHisto, imageHisto, histogramSymbols); } - private static void RemoveEmptyHistograms(List histograms) + private static void RemoveEmptyHistograms(Vp8LHistogramSet histograms) { - int size = 0; - for (int i = 0; i < histograms.Count; i++) + for (int i = histograms.Count - 1; i >= 0; i--) { if (histograms[i] == null) { - continue; + histograms.RemoveAt(i); } - - histograms[size++] = histograms[i]; } - - histograms.RemoveRange(size, histograms.Count - size); } /// /// Construct the histograms from the backward references. /// - private static void HistogramBuild(int xSize, int histoBits, Vp8LBackwardRefs backwardRefs, List histograms) + private static void HistogramBuild( + int xSize, + int histoBits, + Vp8LBackwardRefs backwardRefs, + Vp8LHistogramSet histograms) { int x = 0, y = 0; int histoXSize = LosslessUtils.SubSampleSize(xSize, histoBits); @@ -119,10 +128,10 @@ internal static class HistogramEncoder /// Partition histograms to different entropy bins for three dominant (literal, /// red and blue) symbol costs and compute the histogram aggregate bitCost. /// - private static void HistogramAnalyzeEntropyBin(List histograms, ushort[] binMap) + private static void HistogramAnalyzeEntropyBin(Vp8LHistogramSet histograms, ushort[] binMap) { int histoSize = histograms.Count; - var costRange = new DominantCostRange(); + DominantCostRange costRange = new(); // Analyze the dominant (literal, red and blue) entropy costs. for (int i = 0; i < histoSize; i++) @@ -148,25 +157,28 @@ internal static class HistogramEncoder } } - private static int HistogramCopyAndAnalyze(List origHistograms, List histograms, Span histogramSymbols) + private static int HistogramCopyAndAnalyze( + Vp8LHistogramSet origHistograms, + Vp8LHistogramSet histograms, + Span histogramSymbols) { - var stats = new Vp8LStreaks(); - var bitsEntropy = new Vp8LBitEntropy(); + Vp8LStreaks stats = new(); + Vp8LBitEntropy bitsEntropy = new(); for (int clusterId = 0, i = 0; i < origHistograms.Count; i++) { Vp8LHistogram origHistogram = origHistograms[i]; origHistogram.UpdateHistogramCost(stats, bitsEntropy); // Skip the histogram if it is completely empty, which can happen for tiles with no information (when they are skipped because of LZ77). - if (!origHistogram.IsUsed[0] && !origHistogram.IsUsed[1] && !origHistogram.IsUsed[2] && !origHistogram.IsUsed[3] && !origHistogram.IsUsed[4]) + if (!origHistogram.IsUsed(0) && !origHistogram.IsUsed(1) && !origHistogram.IsUsed(2) && !origHistogram.IsUsed(3) && !origHistogram.IsUsed(4)) { - origHistograms[i] = null; - histograms[i] = null; + origHistograms.DisposeAt(i); + histograms.DisposeAt(i); histogramSymbols[i] = InvalidHistogramSymbol; } else { - histograms[i] = (Vp8LHistogram)origHistogram.DeepClone(); + origHistogram.CopyTo(histograms[i]); histogramSymbols[i] = (ushort)clusterId++; } } @@ -184,7 +196,7 @@ internal static class HistogramEncoder } private static void HistogramCombineEntropyBin( - List histograms, + Vp8LHistogramSet histograms, Span clusters, ushort[] clusterMappings, Vp8LHistogram curCombo, @@ -205,9 +217,9 @@ internal static class HistogramEncoder clusterMappings[idx] = (ushort)idx; } - var indicesToRemove = new List(); - var stats = new Vp8LStreaks(); - var bitsEntropy = new Vp8LBitEntropy(); + List indicesToRemove = new(); + Vp8LStreaks stats = new(); + Vp8LBitEntropy bitsEntropy = new(); for (int idx = 0; idx < histograms.Count; idx++) { if (histograms[idx] == null) @@ -236,15 +248,13 @@ internal static class HistogramEncoder // histogram pairs. In that case, we fallback to combining // histograms as usual to avoid increasing the header size. bool tryCombine = curCombo.TrivialSymbol != NonTrivialSym || (histograms[idx].TrivialSymbol == NonTrivialSym && histograms[first].TrivialSymbol == NonTrivialSym); - int maxCombineFailures = 32; + const int maxCombineFailures = 32; if (tryCombine || binInfo[binId].NumCombineFailures >= maxCombineFailures) { // Move the (better) merged histogram to its final slot. - Vp8LHistogram tmp = curCombo; - curCombo = histograms[first]; - histograms[first] = tmp; + (histograms[first], curCombo) = (curCombo, histograms[first]); - histograms[idx] = null; + histograms.DisposeAt(idx); indicesToRemove.Add(idx); clusterMappings[clusters[idx]] = clusters[first]; } @@ -256,9 +266,9 @@ internal static class HistogramEncoder } } - foreach (int index in indicesToRemove.OrderByDescending(i => i)) + for (int i = indicesToRemove.Count - 1; i >= 0; i--) { - histograms.RemoveAt(index); + histograms.RemoveAt(indicesToRemove[i]); } } @@ -318,15 +328,15 @@ internal static class HistogramEncoder /// Perform histogram aggregation using a stochastic approach. /// /// true if a greedy approach needs to be performed afterwards, false otherwise. - private static bool HistogramCombineStochastic(List histograms, int minClusterSize) + private static bool HistogramCombineStochastic(Vp8LHistogramSet histograms, int minClusterSize) { uint seed = 1; int triesWithNoSuccess = 0; int numUsed = histograms.Count(h => h != null); int outerIters = numUsed; int numTriesNoSuccess = (int)((uint)outerIters / 2); - var stats = new Vp8LStreaks(); - var bitsEntropy = new Vp8LBitEntropy(); + Vp8LStreaks stats = new(); + Vp8LBitEntropy bitsEntropy = new(); if (numUsed < minClusterSize) { @@ -335,25 +345,25 @@ internal static class HistogramEncoder // Priority list of histogram pairs. Its size impacts the quality of the compression and the speed: // the smaller the faster but the worse for the compression. - var histoPriorityList = new List(); - int maxSize = 9; + List histoPriorityList = new(); + const int maxSize = 9; // Fill the initial mapping. Span mappings = histograms.Count <= 64 ? stackalloc int[histograms.Count] : new int[histograms.Count]; - for (int j = 0, iter = 0; iter < histograms.Count; iter++) + for (int j = 0, i = 0; i < histograms.Count; i++) { - if (histograms[iter] == null) + if (histograms[i] == null) { continue; } - mappings[j++] = iter; + mappings[j++] = i; } // Collapse similar histograms. - for (int iter = 0; iter < outerIters && numUsed >= minClusterSize && ++triesWithNoSuccess < numTriesNoSuccess; iter++) + for (int i = 0; i < outerIters && numUsed >= minClusterSize && ++triesWithNoSuccess < numTriesNoSuccess; i++) { - double bestCost = histoPriorityList.Count == 0 ? 0.0d : histoPriorityList[0].CostDiff; + double bestCost = histoPriorityList.Count == 0 ? 0D : histoPriorityList[0].CostDiff; int numTries = (int)((uint)numUsed / 2); uint randRange = (uint)((numUsed - 1) * numUsed); @@ -373,7 +383,8 @@ internal static class HistogramEncoder idx2 = mappings[idx2]; // Calculate cost reduction on combination. - double currCost = HistoPriorityListPush(histoPriorityList, maxSize, histograms, idx1, idx2, bestCost, stats, bitsEntropy); + double currCost = 0; + currCost = HistoPriorityListPush(histoPriorityList, maxSize, histograms, idx1, idx2, bestCost, stats, bitsEntropy); // Found a better pair? if (currCost < 0) @@ -398,13 +409,13 @@ internal static class HistogramEncoder int mappingIndex = mappings.IndexOf(bestIdx2); Span src = mappings.Slice(mappingIndex + 1, numUsed - mappingIndex - 1); - Span dst = mappings.Slice(mappingIndex); + Span dst = mappings[mappingIndex..]; src.CopyTo(dst); // Merge the histograms and remove bestIdx2 from the list. HistogramAdd(histograms[bestIdx2], histograms[bestIdx1], histograms[bestIdx1]); - histograms.ElementAt(bestIdx1).BitCost = histoPriorityList[0].CostCombo; - histograms[bestIdx2] = null; + histograms[bestIdx1].BitCost = histoPriorityList[0].CostCombo; + histograms.DisposeAt(bestIdx2); numUsed--; for (int j = 0; j < histoPriorityList.Count;) @@ -418,7 +429,7 @@ internal static class HistogramEncoder // check for it all the time nevertheless. if (isIdx1Best && isIdx2Best) { - histoPriorityList[j] = histoPriorityList[histoPriorityList.Count - 1]; + histoPriorityList[j] = histoPriorityList[^1]; histoPriorityList.RemoveAt(histoPriorityList.Count - 1); continue; } @@ -439,18 +450,17 @@ internal static class HistogramEncoder // Make sure the index order is respected. if (p.Idx1 > p.Idx2) { - int tmp = p.Idx2; - p.Idx2 = p.Idx1; - p.Idx1 = tmp; + (p.Idx1, p.Idx2) = (p.Idx2, p.Idx1); } if (doEval) { // Re-evaluate the cost of an updated pair. - HistoListUpdatePair(histograms[p.Idx1], histograms[p.Idx2], stats, bitsEntropy, 0.0d, p); - if (p.CostDiff >= 0.0d) + HistoListUpdatePair(histograms[p.Idx1], histograms[p.Idx2], stats, bitsEntropy, 0D, p); + + if (p.CostDiff >= 0D) { - histoPriorityList[j] = histoPriorityList[histoPriorityList.Count - 1]; + histoPriorityList[j] = histoPriorityList[^1]; histoPriorityList.RemoveAt(histoPriorityList.Count - 1); continue; } @@ -463,20 +473,18 @@ internal static class HistogramEncoder triesWithNoSuccess = 0; } - bool doGreedy = numUsed <= minClusterSize; - - return doGreedy; + return numUsed <= minClusterSize; } - private static void HistogramCombineGreedy(List histograms) + private static void HistogramCombineGreedy(Vp8LHistogramSet histograms) { int histoSize = histograms.Count(h => h != null); // Priority list of histogram pairs. - var histoPriorityList = new List(); + List histoPriorityList = new(); int maxSize = histoSize * histoSize; - var stats = new Vp8LStreaks(); - var bitsEntropy = new Vp8LBitEntropy(); + Vp8LStreaks stats = new(); + Vp8LBitEntropy bitsEntropy = new(); for (int i = 0; i < histoSize; i++) { @@ -504,16 +512,17 @@ internal static class HistogramEncoder histograms[idx1].BitCost = histoPriorityList[0].CostCombo; // Remove merged histogram. - histograms[idx2] = null; + histograms.DisposeAt(idx2); // Remove pairs intersecting the just combined best pair. + // TODO: Reversing this will avoid the need to remove from the end of the list. for (int i = 0; i < histoPriorityList.Count;) { - HistogramPair p = histoPriorityList.ElementAt(i); + HistogramPair p = histoPriorityList[i]; if (p.Idx1 == idx1 || p.Idx2 == idx1 || p.Idx1 == idx2 || p.Idx2 == idx2) { // Replace item at pos i with the last one and shrinking the list. - histoPriorityList[i] = histoPriorityList[histoPriorityList.Count - 1]; + histoPriorityList[i] = histoPriorityList[^1]; histoPriorityList.RemoveAt(histoPriorityList.Count - 1); } else @@ -536,12 +545,15 @@ internal static class HistogramEncoder } } - private static void HistogramRemap(List input, List output, Span symbols) + private static void HistogramRemap( + Vp8LHistogramSet input, + Vp8LHistogramSet output, + Span symbols) { int inSize = input.Count; int outSize = output.Count; - var stats = new Vp8LStreaks(); - var bitsEntropy = new Vp8LBitEntropy(); + Vp8LStreaks stats = new(); + Vp8LBitEntropy bitsEntropy = new(); if (outSize > 1) { for (int i = 0; i < inSize; i++) @@ -577,11 +589,11 @@ internal static class HistogramEncoder } // Recompute each output. - int paletteCodeBits = output.First().PaletteCodeBits; - output.Clear(); + int paletteCodeBits = output[0].PaletteCodeBits; for (int i = 0; i < outSize; i++) { - output.Add(new Vp8LHistogram(paletteCodeBits)); + output[i].Clear(); + output[i].PaletteCodeBits = paletteCodeBits; } for (int i = 0; i < inSize; i++) @@ -600,20 +612,26 @@ internal static class HistogramEncoder /// Create a pair from indices "idx1" and "idx2" provided its cost is inferior to "threshold", a negative entropy. /// /// The cost of the pair, or 0 if it superior to threshold. - private static double HistoPriorityListPush(List histoList, int maxSize, List histograms, int idx1, int idx2, double threshold, Vp8LStreaks stats, Vp8LBitEntropy bitsEntropy) + private static double HistoPriorityListPush( + List histoList, + int maxSize, + Vp8LHistogramSet histograms, + int idx1, + int idx2, + double threshold, + Vp8LStreaks stats, + Vp8LBitEntropy bitsEntropy) { - var pair = new HistogramPair(); + HistogramPair pair = new(); if (histoList.Count == maxSize) { - return 0.0d; + return 0D; } if (idx1 > idx2) { - int tmp = idx2; - idx2 = idx1; - idx1 = tmp; + (idx1, idx2) = (idx2, idx1); } pair.Idx1 = idx1; @@ -637,9 +655,16 @@ internal static class HistogramEncoder } /// - /// Update the cost diff and combo of a pair of histograms. This needs to be called when the the histograms have been merged with a third one. + /// Update the cost diff and combo of a pair of histograms. This needs to be called when the histograms have been + /// merged with a third one. /// - private static void HistoListUpdatePair(Vp8LHistogram h1, Vp8LHistogram h2, Vp8LStreaks stats, Vp8LBitEntropy bitsEntropy, double threshold, HistogramPair pair) + private static void HistoListUpdatePair( + Vp8LHistogram h1, + Vp8LHistogram h2, + Vp8LStreaks stats, + Vp8LBitEntropy bitsEntropy, + double threshold, + HistogramPair pair) { double sumCost = h1.BitCost + h2.BitCost; pair.CostCombo = 0.0d; diff --git a/src/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs b/src/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs index 39ad967e38..027d4f7ee9 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs @@ -25,7 +25,7 @@ internal static class HuffmanUtils 0x1, 0x9, 0x5, 0xd, 0x3, 0xb, 0x7, 0xf }; - public static void CreateHuffmanTree(uint[] histogram, int treeDepthLimit, bool[] bufRle, Span huffTree, HuffmanTreeCode huffCode) + public static void CreateHuffmanTree(Span histogram, int treeDepthLimit, bool[] bufRle, Span huffTree, HuffmanTreeCode huffCode) { int numSymbols = huffCode.NumSymbols; bufRle.AsSpan().Clear(); @@ -40,7 +40,7 @@ internal static class HuffmanUtils /// Change the population counts in a way that the consequent /// Huffman tree compression, especially its RLE-part, give smaller output. /// - public static void OptimizeHuffmanForRle(int length, bool[] goodForRle, uint[] counts) + public static void OptimizeHuffmanForRle(int length, bool[] goodForRle, Span counts) { // 1) Let's make the Huffman code more compatible with rle encoding. for (; length >= 0; --length) @@ -116,7 +116,7 @@ internal static class HuffmanUtils { // We don't want to change value at counts[i], // that is already belonging to the next stride. Thus - 1. - counts[i - k - 1] = count; + counts[(int)(i - k - 1)] = count; } } @@ -159,7 +159,7 @@ internal static class HuffmanUtils /// The size of the histogram. /// The tree depth limit. /// How many bits are used for the symbol. - public static void GenerateOptimalTree(Span tree, uint[] histogram, int histogramSize, int treeDepthLimit, byte[] bitDepths) + public static void GenerateOptimalTree(Span tree, Span histogram, int histogramSize, int treeDepthLimit, byte[] bitDepths) { uint countMin; int treeSizeOrig = 0; @@ -177,7 +177,7 @@ internal static class HuffmanUtils return; } - Span treePool = tree.Slice(treeSizeOrig); + Span treePool = tree[treeSizeOrig..]; // For block sizes with less than 64k symbols we never need to do a // second iteration of this loop. @@ -202,7 +202,7 @@ internal static class HuffmanUtils } // Build the Huffman tree. - Span treeSlice = tree.Slice(0, treeSize); + Span treeSlice = tree[..treeSize]; treeSlice.Sort(HuffmanTree.Compare); if (treeSize > 1) @@ -357,7 +357,7 @@ internal static class HuffmanUtils // Special case code with only one value. if (offsets[WebpConstants.MaxAllowedCodeLength] == 1) { - var huffmanCode = new HuffmanCode() + HuffmanCode huffmanCode = new() { BitsUsed = 0, Value = (uint)sorted[0] @@ -390,7 +390,7 @@ internal static class HuffmanUtils for (; countsLen > 0; countsLen--) { - var huffmanCode = new HuffmanCode() + HuffmanCode huffmanCode = new() { BitsUsed = len, Value = (uint)sorted[symbol++] @@ -432,7 +432,7 @@ internal static class HuffmanUtils }; } - var huffmanCode = new HuffmanCode + HuffmanCode huffmanCode = new() { BitsUsed = len - rootBits, Value = (uint)sorted[symbol++] diff --git a/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs b/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs index 6a28e5b3fb..cedc809382 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs @@ -37,7 +37,7 @@ internal sealed class PixOrCopy Len = len }; - public uint Literal(int component) => (this.BgraOrDistance >> (component * 8)) & 0xff; + public int Literal(int component) => (int)(this.BgraOrDistance >> (component * 8)) & 0xFF; public uint CacheIdx() => this.BgraOrDistance; diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LBitEntropy.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LBitEntropy.cs index 649845b025..330d1c555e 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LBitEntropy.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LBitEntropy.cs @@ -125,7 +125,7 @@ internal class Vp8LBitEntropy /// /// Get the entropy for the distribution 'X'. /// - public void BitsEntropyUnrefined(uint[] x, int length, Vp8LStreaks stats) + public void BitsEntropyUnrefined(Span x, int length, Vp8LStreaks stats) { int i; int iPrev = 0; @@ -147,7 +147,7 @@ internal class Vp8LBitEntropy this.Entropy += LosslessUtils.FastSLog2(this.Sum); } - public void GetCombinedEntropyUnrefined(uint[] x, uint[] y, int length, Vp8LStreaks stats) + public void GetCombinedEntropyUnrefined(Span x, Span y, int length, Vp8LStreaks stats) { int i; int iPrev = 0; @@ -169,7 +169,7 @@ internal class Vp8LBitEntropy this.Entropy += LosslessUtils.FastSLog2(this.Sum); } - public void GetEntropyUnrefined(uint[] x, int length, Vp8LStreaks stats) + public void GetEntropyUnrefined(Span x, int length, Vp8LStreaks stats) { int i; int iPrev = 0; diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 1f7c7586eb..10fc8ab804 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -589,15 +589,21 @@ internal class Vp8LEncoder : IDisposable Vp8LBackwardRefs refsTmp = this.Refs[refsBest.Equals(this.Refs[0]) ? 1 : 0]; this.bitWriter.Reset(bwInit); - Vp8LHistogram tmpHisto = new(cacheBits); - List histogramImage = new(histogramImageXySize); - for (int i = 0; i < histogramImageXySize; i++) - { - histogramImage.Add(new Vp8LHistogram(cacheBits)); - } + using Vp8LHistogram tmpHisto = new(this.memoryAllocator, cacheBits); + using Vp8LHistogramSet histogramImage = new(this.memoryAllocator, histogramImageXySize, cacheBits); // Build histogram image and symbols from backward references. - HistogramEncoder.GetHistoImageSymbols(width, height, refsBest, this.quality, this.HistoBits, cacheBits, histogramImage, tmpHisto, histogramSymbols); + HistogramEncoder.GetHistoImageSymbols( + this.memoryAllocator, + width, + height, + refsBest, + this.quality, + this.HistoBits, + cacheBits, + histogramImage, + tmpHisto, + histogramSymbols); // Create Huffman bit lengths and codes for each histogram image. int histogramImageSize = histogramImage.Count; @@ -678,9 +684,7 @@ internal class Vp8LEncoder : IDisposable // Keep track of the smallest image so far. if (isFirstIteration || (bitWriterBest != null && this.bitWriter.NumBytes() < bitWriterBest.NumBytes())) { - Vp8LBitWriter tmp = this.bitWriter; - this.bitWriter = bitWriterBest; - bitWriterBest = tmp; + (bitWriterBest, this.bitWriter) = (this.bitWriter, bitWriterBest); } isFirstIteration = false; @@ -787,13 +791,8 @@ internal class Vp8LEncoder : IDisposable refsTmp1, refsTmp2); - List histogramImage = new() - { - new(cacheBits) - }; - // Build histogram image and symbols from backward references. - histogramImage[0].StoreRefs(refs); + using Vp8LHistogramSet histogramImage = new(this.memoryAllocator, refs, 1, cacheBits); // Create Huffman bit lengths and codes for each histogram image. GetHuffBitLengthsAndCodes(histogramImage, huffmanCodes); @@ -833,7 +832,7 @@ internal class Vp8LEncoder : IDisposable private void StoreHuffmanCode(Span huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode huffmanCode) { int count = 0; - Span symbols = this.scratch.Span.Slice(0, 2); + Span symbols = this.scratch.Span[..2]; symbols.Clear(); const int maxBits = 8; const int maxSymbol = 1 << maxBits; @@ -886,6 +885,7 @@ internal class Vp8LEncoder : IDisposable private void StoreFullHuffmanCode(Span huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode tree) { + // TODO: Allocations. int i; byte[] codeLengthBitDepth = new byte[WebpConstants.CodeLengthCodes]; short[] codeLengthBitDepthSymbols = new short[WebpConstants.CodeLengthCodes]; @@ -996,7 +996,12 @@ internal class Vp8LEncoder : IDisposable } } - private void StoreImageToBitMask(int width, int histoBits, Vp8LBackwardRefs backwardRefs, Span histogramSymbols, HuffmanTreeCode[] huffmanCodes) + private void StoreImageToBitMask( + int width, + int histoBits, + Vp8LBackwardRefs backwardRefs, + Span histogramSymbols, + HuffmanTreeCode[] huffmanCodes) { int histoXSize = histoBits > 0 ? LosslessUtils.SubSampleSize(width, histoBits) : 1; int tileMask = histoBits == 0 ? 0 : -(1 << histoBits); @@ -1008,10 +1013,10 @@ internal class Vp8LEncoder : IDisposable int tileY = y & tileMask; int histogramIx = histogramSymbols[0]; Span codes = huffmanCodes.AsSpan(5 * histogramIx); - using List.Enumerator c = backwardRefs.Refs.GetEnumerator(); - while (c.MoveNext()) + + for (int i = 0; i < backwardRefs.Refs.Count; i++) { - PixOrCopy v = c.Current; + PixOrCopy v = backwardRefs.Refs[i]; if (tileX != (x & tileMask) || tileY != (y & tileMask)) { tileX = x & tileMask; @@ -1024,7 +1029,7 @@ internal class Vp8LEncoder : IDisposable { for (int k = 0; k < 4; k++) { - int code = (int)v.Literal(Order[k]); + int code = v.Literal(Order[k]); this.bitWriter.WriteHuffmanCode(codes[k], code); } } @@ -1379,10 +1384,8 @@ internal class Vp8LEncoder : IDisposable useLut = false; break; } - else - { - buffer[ind] = (uint)j; - } + + buffer[ind] = (uint)j; } if (useLut) @@ -1591,14 +1594,12 @@ internal class Vp8LEncoder : IDisposable } // Swap color(palette[bestIdx], palette[i]); - uint best = palette[bestIdx]; - palette[bestIdx] = palette[i]; - palette[i] = best; + (palette[i], palette[bestIdx]) = (palette[bestIdx], palette[i]); predict = palette[i]; } } - private static void GetHuffBitLengthsAndCodes(List histogramImage, HuffmanTreeCode[] huffmanCodes) + private static void GetHuffBitLengthsAndCodes(Vp8LHistogramSet histogramImage, HuffmanTreeCode[] huffmanCodes) { int maxNumSymbols = 0; @@ -1609,9 +1610,20 @@ internal class Vp8LEncoder : IDisposable int startIdx = 5 * i; for (int k = 0; k < 5; k++) { - int numSymbols = - k == 0 ? histo.NumCodes() : - k == 4 ? WebpConstants.NumDistanceCodes : 256; + int numSymbols; + if (k == 0) + { + numSymbols = histo.NumCodes(); + } + else if (k == 4) + { + numSymbols = WebpConstants.NumDistanceCodes; + } + else + { + numSymbols = 256; + } + huffmanCodes[startIdx + k].NumSymbols = numSymbols; } } @@ -1629,6 +1641,7 @@ internal class Vp8LEncoder : IDisposable } // Create Huffman trees. + // TODO: Allocations. Size here has a max and can be sliced. bool[] bufRle = new bool[maxNumSymbols]; Span huffTree = stackalloc HuffmanTree[3 * maxNumSymbols]; @@ -1682,8 +1695,18 @@ internal class Vp8LEncoder : IDisposable histoBits++; } - return histoBits < WebpConstants.MinHuffmanBits ? WebpConstants.MinHuffmanBits : - histoBits > WebpConstants.MaxHuffmanBits ? WebpConstants.MaxHuffmanBits : histoBits; + if (histoBits < WebpConstants.MinHuffmanBits) + { + return WebpConstants.MinHuffmanBits; + } + else if (histoBits > WebpConstants.MaxHuffmanBits) + { + return WebpConstants.MaxHuffmanBits; + } + else + { + return histoBits; + } } /// @@ -1720,11 +1743,7 @@ internal class Vp8LEncoder : IDisposable [MethodImpl(InliningOptions.ShortMethod)] private static void BitWriterSwap(ref Vp8LBitWriter src, ref Vp8LBitWriter dst) - { - Vp8LBitWriter tmp = src; - src = dst; - dst = tmp; - } + => (dst, src) = (src, dst); /// /// Calculates the bits used for the transformation. @@ -1732,9 +1751,21 @@ internal class Vp8LEncoder : IDisposable [MethodImpl(InliningOptions.ShortMethod)] private static int GetTransformBits(WebpEncodingMethod method, int histoBits) { - int maxTransformBits = (int)method < 4 ? 6 : method > WebpEncodingMethod.Level4 ? 4 : 5; - int res = histoBits > maxTransformBits ? maxTransformBits : histoBits; - return res; + int maxTransformBits; + if ((int)method < 4) + { + maxTransformBits = 6; + } + else if (method > WebpEncodingMethod.Level4) + { + maxTransformBits = 4; + } + else + { + maxTransformBits = 5; + } + + return histoBits > maxTransformBits ? maxTransformBits : histoBits; } [MethodImpl(InliningOptions.ShortMethod)] diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs index 5ec3f0d53d..07ee88f259 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs @@ -1,63 +1,73 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Webp.Lossless; -internal sealed class Vp8LHistogram : IDeepCloneable +internal sealed class Vp8LHistogram : IDisposable { private const uint NonTrivialSym = 0xffffffff; + private readonly IMemoryOwner buffer; + private const int RedSize = WebpConstants.NumLiteralCodes; + private const int BlueSize = WebpConstants.NumLiteralCodes; + private const int AlphaSize = WebpConstants.NumLiteralCodes; + private const int DistanceSize = WebpConstants.NumDistanceCodes; + public const int LiteralSize = WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes + (1 << WebpConstants.MaxColorCacheBits) + 1; + private const int UsedSize = 5; // 5 for literal, red, blue, alpha, distance + public const int BufferSize = RedSize + BlueSize + AlphaSize + DistanceSize + LiteralSize + UsedSize; + private readonly bool isSetMember; /// /// Initializes a new instance of the class. /// - /// The histogram to create an instance from. - private Vp8LHistogram(Vp8LHistogram other) - : this(other.PaletteCodeBits) + /// The memory allocator. + /// The backward references to initialize the histogram with. + /// The palette code bits. + public Vp8LHistogram(MemoryAllocator memoryAllocator, Vp8LBackwardRefs refs, int paletteCodeBits) + : this(memoryAllocator, paletteCodeBits) => this.StoreRefs(refs); + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The palette code bits. + public Vp8LHistogram(MemoryAllocator memoryAllocator, int paletteCodeBits) { - other.Red.AsSpan().CopyTo(this.Red); - other.Blue.AsSpan().CopyTo(this.Blue); - other.Alpha.AsSpan().CopyTo(this.Alpha); - other.Literal.AsSpan().CopyTo(this.Literal); - other.Distance.AsSpan().CopyTo(this.Distance); - other.IsUsed.AsSpan().CopyTo(this.IsUsed); - this.LiteralCost = other.LiteralCost; - this.RedCost = other.RedCost; - this.BlueCost = other.BlueCost; - this.BitCost = other.BitCost; - this.TrivialSymbol = other.TrivialSymbol; - this.PaletteCodeBits = other.PaletteCodeBits; + this.buffer = memoryAllocator.Allocate(BufferSize, AllocationOptions.Clean); + this.PaletteCodeBits = paletteCodeBits; } /// /// Initializes a new instance of the class. /// + /// + /// This constructor should be used when the histogram is a member of a . + /// + /// The backing buffer. /// The backward references to initialize the histogram with. /// The palette code bits. - public Vp8LHistogram(Vp8LBackwardRefs refs, int paletteCodeBits) - : this(paletteCodeBits) => this.StoreRefs(refs); + public Vp8LHistogram(IMemoryOwner buffer, Vp8LBackwardRefs refs, int paletteCodeBits) + : this(buffer, paletteCodeBits) => this.StoreRefs(refs); /// /// Initializes a new instance of the class. /// + /// + /// This constructor should be used when the histogram is a member of a . + /// + /// The backing buffer. /// The palette code bits. - public Vp8LHistogram(int paletteCodeBits) + public Vp8LHistogram(IMemoryOwner buffer, int paletteCodeBits) { + this.buffer = buffer; this.PaletteCodeBits = paletteCodeBits; - this.Red = new uint[WebpConstants.NumLiteralCodes + 1]; - this.Blue = new uint[WebpConstants.NumLiteralCodes + 1]; - this.Alpha = new uint[WebpConstants.NumLiteralCodes + 1]; - this.Distance = new uint[WebpConstants.NumDistanceCodes]; - - int literalSize = WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes + (1 << WebpConstants.MaxColorCacheBits); - this.Literal = new uint[literalSize + 1]; - - // 5 for literal, red, blue, alpha, distance. - this.IsUsed = new bool[5]; + this.isSetMember = true; } /// @@ -85,22 +95,59 @@ internal sealed class Vp8LHistogram : IDeepCloneable /// public double BlueCost { get; set; } - public uint[] Red { get; } + public Span Red => this.buffer.GetSpan()[..RedSize]; - public uint[] Blue { get; } + public Span Blue => this.buffer.GetSpan().Slice(RedSize, BlueSize); - public uint[] Alpha { get; } + public Span Alpha => this.buffer.GetSpan().Slice(RedSize + BlueSize, AlphaSize); - public uint[] Literal { get; } + public Span Distance => this.buffer.GetSpan().Slice(RedSize + BlueSize + AlphaSize, DistanceSize); - public uint[] Distance { get; } + public Span Literal => this.buffer.GetSpan().Slice(RedSize + BlueSize + AlphaSize + DistanceSize, LiteralSize); public uint TrivialSymbol { get; set; } - public bool[] IsUsed { get; } + private Span IsUsedSpan => this.buffer.GetSpan().Slice(RedSize + BlueSize + AlphaSize + DistanceSize + LiteralSize, UsedSize); + + public bool IsDisposed { get; set; } - /// - public IDeepCloneable DeepClone() => new Vp8LHistogram(this); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsUsed(int index) => this.IsUsedSpan[index] == 1u; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void IsUsed(int index, bool value) => this.IsUsedSpan[index] = value ? 1u : 0; + + /// + /// Creates a copy of the given class. + /// + /// The histogram to copy to. + public void CopyTo(Vp8LHistogram other) + { + this.Red.CopyTo(other.Red); + this.Blue.CopyTo(other.Blue); + this.Alpha.CopyTo(other.Alpha); + this.Literal.CopyTo(other.Literal); + this.Distance.CopyTo(other.Distance); + this.IsUsedSpan.CopyTo(other.IsUsedSpan); + + other.LiteralCost = this.LiteralCost; + other.RedCost = this.RedCost; + other.BlueCost = this.BlueCost; + other.BitCost = this.BitCost; + other.TrivialSymbol = this.TrivialSymbol; + other.PaletteCodeBits = this.PaletteCodeBits; + } + + public void Clear() + { + this.buffer.Clear(); + this.PaletteCodeBits = 0; + this.BitCost = 0; + this.LiteralCost = 0; + this.RedCost = 0; + this.BlueCost = 0; + this.TrivialSymbol = 0; + } /// /// Collect all the references into a histogram (without reset). @@ -108,10 +155,9 @@ internal sealed class Vp8LHistogram : IDeepCloneable /// The backward references. public void StoreRefs(Vp8LBackwardRefs refs) { - using List.Enumerator c = refs.Refs.GetEnumerator(); - while (c.MoveNext()) + for (int i = 0; i < refs.Refs.Count; i++) { - this.AddSinglePixOrCopy(c.Current, false); + this.AddSinglePixOrCopy(refs.Refs[i], false); } } @@ -163,12 +209,12 @@ internal sealed class Vp8LHistogram : IDeepCloneable { uint notUsed = 0; return - PopulationCost(this.Literal, this.NumCodes(), ref notUsed, ref this.IsUsed[0], stats, bitsEntropy) - + PopulationCost(this.Red, WebpConstants.NumLiteralCodes, ref notUsed, ref this.IsUsed[1], stats, bitsEntropy) - + PopulationCost(this.Blue, WebpConstants.NumLiteralCodes, ref notUsed, ref this.IsUsed[2], stats, bitsEntropy) - + PopulationCost(this.Alpha, WebpConstants.NumLiteralCodes, ref notUsed, ref this.IsUsed[3], stats, bitsEntropy) - + PopulationCost(this.Distance, WebpConstants.NumDistanceCodes, ref notUsed, ref this.IsUsed[4], stats, bitsEntropy) - + ExtraCost(this.Literal.AsSpan(WebpConstants.NumLiteralCodes), WebpConstants.NumLengthCodes) + this.PopulationCost(this.Literal, this.NumCodes(), ref notUsed, 0, stats, bitsEntropy) + + this.PopulationCost(this.Red, WebpConstants.NumLiteralCodes, ref notUsed, 1, stats, bitsEntropy) + + this.PopulationCost(this.Blue, WebpConstants.NumLiteralCodes, ref notUsed, 2, stats, bitsEntropy) + + this.PopulationCost(this.Alpha, WebpConstants.NumLiteralCodes, ref notUsed, 3, stats, bitsEntropy) + + this.PopulationCost(this.Distance, WebpConstants.NumDistanceCodes, ref notUsed, 4, stats, bitsEntropy) + + ExtraCost(this.Literal[WebpConstants.NumLiteralCodes..], WebpConstants.NumLengthCodes) + ExtraCost(this.Distance, WebpConstants.NumDistanceCodes); } @@ -177,12 +223,12 @@ internal sealed class Vp8LHistogram : IDeepCloneable uint alphaSym = 0, redSym = 0, blueSym = 0; uint notUsed = 0; - double alphaCost = PopulationCost(this.Alpha, WebpConstants.NumLiteralCodes, ref alphaSym, ref this.IsUsed[3], stats, bitsEntropy); - double distanceCost = PopulationCost(this.Distance, WebpConstants.NumDistanceCodes, ref notUsed, ref this.IsUsed[4], stats, bitsEntropy) + ExtraCost(this.Distance, WebpConstants.NumDistanceCodes); + double alphaCost = this.PopulationCost(this.Alpha, WebpConstants.NumLiteralCodes, ref alphaSym, 3, stats, bitsEntropy); + double distanceCost = this.PopulationCost(this.Distance, WebpConstants.NumDistanceCodes, ref notUsed, 4, stats, bitsEntropy) + ExtraCost(this.Distance, WebpConstants.NumDistanceCodes); int numCodes = this.NumCodes(); - this.LiteralCost = PopulationCost(this.Literal, numCodes, ref notUsed, ref this.IsUsed[0], stats, bitsEntropy) + ExtraCost(this.Literal.AsSpan(WebpConstants.NumLiteralCodes), WebpConstants.NumLengthCodes); - this.RedCost = PopulationCost(this.Red, WebpConstants.NumLiteralCodes, ref redSym, ref this.IsUsed[1], stats, bitsEntropy); - this.BlueCost = PopulationCost(this.Blue, WebpConstants.NumLiteralCodes, ref blueSym, ref this.IsUsed[2], stats, bitsEntropy); + this.LiteralCost = this.PopulationCost(this.Literal, numCodes, ref notUsed, 0, stats, bitsEntropy) + ExtraCost(this.Literal[WebpConstants.NumLiteralCodes..], WebpConstants.NumLengthCodes); + this.RedCost = this.PopulationCost(this.Red, WebpConstants.NumLiteralCodes, ref redSym, 1, stats, bitsEntropy); + this.BlueCost = this.PopulationCost(this.Blue, WebpConstants.NumLiteralCodes, ref blueSym, 2, stats, bitsEntropy); this.BitCost = this.LiteralCost + this.RedCost + this.BlueCost + alphaCost + distanceCost; if ((alphaSym | redSym | blueSym) == NonTrivialSym) { @@ -234,7 +280,7 @@ internal sealed class Vp8LHistogram : IDeepCloneable for (int i = 0; i < 5; i++) { - output.IsUsed[i] = this.IsUsed[i] | b.IsUsed[i]; + output.IsUsed(i, this.IsUsed(i) | b.IsUsed(i)); } output.TrivialSymbol = this.TrivialSymbol == b.TrivialSymbol @@ -247,9 +293,9 @@ internal sealed class Vp8LHistogram : IDeepCloneable bool trivialAtEnd = false; cost = costInitial; - cost += GetCombinedEntropy(this.Literal, b.Literal, this.NumCodes(), this.IsUsed[0], b.IsUsed[0], false, stats, bitEntropy); + cost += GetCombinedEntropy(this.Literal, b.Literal, this.NumCodes(), this.IsUsed(0), b.IsUsed(0), false, stats, bitEntropy); - cost += ExtraCostCombined(this.Literal.AsSpan(WebpConstants.NumLiteralCodes), b.Literal.AsSpan(WebpConstants.NumLiteralCodes), WebpConstants.NumLengthCodes); + cost += ExtraCostCombined(this.Literal[WebpConstants.NumLiteralCodes..], b.Literal[WebpConstants.NumLiteralCodes..], WebpConstants.NumLengthCodes); if (cost > costThreshold) { @@ -270,155 +316,158 @@ internal sealed class Vp8LHistogram : IDeepCloneable } } - cost += GetCombinedEntropy(this.Red, b.Red, WebpConstants.NumLiteralCodes, this.IsUsed[1], b.IsUsed[1], trivialAtEnd, stats, bitEntropy); + cost += GetCombinedEntropy(this.Red, b.Red, WebpConstants.NumLiteralCodes, this.IsUsed(1), b.IsUsed(1), trivialAtEnd, stats, bitEntropy); if (cost > costThreshold) { return false; } - cost += GetCombinedEntropy(this.Blue, b.Blue, WebpConstants.NumLiteralCodes, this.IsUsed[2], b.IsUsed[2], trivialAtEnd, stats, bitEntropy); + cost += GetCombinedEntropy(this.Blue, b.Blue, WebpConstants.NumLiteralCodes, this.IsUsed(2), b.IsUsed(2), trivialAtEnd, stats, bitEntropy); if (cost > costThreshold) { return false; } - cost += GetCombinedEntropy(this.Alpha, b.Alpha, WebpConstants.NumLiteralCodes, this.IsUsed[3], b.IsUsed[3], trivialAtEnd, stats, bitEntropy); + cost += GetCombinedEntropy(this.Alpha, b.Alpha, WebpConstants.NumLiteralCodes, this.IsUsed(3), b.IsUsed(3), trivialAtEnd, stats, bitEntropy); if (cost > costThreshold) { return false; } - cost += GetCombinedEntropy(this.Distance, b.Distance, WebpConstants.NumDistanceCodes, this.IsUsed[4], b.IsUsed[4], false, stats, bitEntropy); + cost += GetCombinedEntropy(this.Distance, b.Distance, WebpConstants.NumDistanceCodes, this.IsUsed(4), b.IsUsed(4), false, stats, bitEntropy); if (cost > costThreshold) { return false; } cost += ExtraCostCombined(this.Distance, b.Distance, WebpConstants.NumDistanceCodes); - if (cost > costThreshold) - { - return false; - } - - return true; + return cost <= costThreshold; } private void AddLiteral(Vp8LHistogram b, Vp8LHistogram output, int literalSize) { - if (this.IsUsed[0]) + if (this.IsUsed(0)) { - if (b.IsUsed[0]) + if (b.IsUsed(0)) { AddVector(this.Literal, b.Literal, output.Literal, literalSize); } else { - this.Literal.AsSpan(0, literalSize).CopyTo(output.Literal); + this.Literal[..literalSize].CopyTo(output.Literal); } } - else if (b.IsUsed[0]) + else if (b.IsUsed(0)) { - b.Literal.AsSpan(0, literalSize).CopyTo(output.Literal); + b.Literal[..literalSize].CopyTo(output.Literal); } else { - output.Literal.AsSpan(0, literalSize).Clear(); + output.Literal[..literalSize].Clear(); } } private void AddRed(Vp8LHistogram b, Vp8LHistogram output, int size) { - if (this.IsUsed[1]) + if (this.IsUsed(1)) { - if (b.IsUsed[1]) + if (b.IsUsed(1)) { AddVector(this.Red, b.Red, output.Red, size); } else { - this.Red.AsSpan(0, size).CopyTo(output.Red); + this.Red[..size].CopyTo(output.Red); } } - else if (b.IsUsed[1]) + else if (b.IsUsed(1)) { - b.Red.AsSpan(0, size).CopyTo(output.Red); + b.Red[..size].CopyTo(output.Red); } else { - output.Red.AsSpan(0, size).Clear(); + output.Red[..size].Clear(); } } private void AddBlue(Vp8LHistogram b, Vp8LHistogram output, int size) { - if (this.IsUsed[2]) + if (this.IsUsed(2)) { - if (b.IsUsed[2]) + if (b.IsUsed(2)) { AddVector(this.Blue, b.Blue, output.Blue, size); } else { - this.Blue.AsSpan(0, size).CopyTo(output.Blue); + this.Blue[..size].CopyTo(output.Blue); } } - else if (b.IsUsed[2]) + else if (b.IsUsed(2)) { - b.Blue.AsSpan(0, size).CopyTo(output.Blue); + b.Blue[..size].CopyTo(output.Blue); } else { - output.Blue.AsSpan(0, size).Clear(); + output.Blue[..size].Clear(); } } private void AddAlpha(Vp8LHistogram b, Vp8LHistogram output, int size) { - if (this.IsUsed[3]) + if (this.IsUsed(3)) { - if (b.IsUsed[3]) + if (b.IsUsed(3)) { AddVector(this.Alpha, b.Alpha, output.Alpha, size); } else { - this.Alpha.AsSpan(0, size).CopyTo(output.Alpha); + this.Alpha[..size].CopyTo(output.Alpha); } } - else if (b.IsUsed[3]) + else if (b.IsUsed(3)) { - b.Alpha.AsSpan(0, size).CopyTo(output.Alpha); + b.Alpha[..size].CopyTo(output.Alpha); } else { - output.Alpha.AsSpan(0, size).Clear(); + output.Alpha[..size].Clear(); } } private void AddDistance(Vp8LHistogram b, Vp8LHistogram output, int size) { - if (this.IsUsed[4]) + if (this.IsUsed(4)) { - if (b.IsUsed[4]) + if (b.IsUsed(4)) { AddVector(this.Distance, b.Distance, output.Distance, size); } else { - this.Distance.AsSpan(0, size).CopyTo(output.Distance); + this.Distance[..size].CopyTo(output.Distance); } } - else if (b.IsUsed[4]) + else if (b.IsUsed(4)) { - b.Distance.AsSpan(0, size).CopyTo(output.Distance); + b.Distance[..size].CopyTo(output.Distance); } else { - output.Distance.AsSpan(0, size).Clear(); + output.Distance[..size].Clear(); } } - private static double GetCombinedEntropy(uint[] x, uint[] y, int length, bool isXUsed, bool isYUsed, bool trivialAtEnd, Vp8LStreaks stats, Vp8LBitEntropy bitEntropy) + private static double GetCombinedEntropy( + Span x, + Span y, + int length, + bool isXUsed, + bool isYUsed, + bool trivialAtEnd, + Vp8LStreaks stats, + Vp8LBitEntropy bitEntropy) { stats.Clear(); bitEntropy.Init(); @@ -450,18 +499,15 @@ internal sealed class Vp8LHistogram : IDeepCloneable bitEntropy.GetEntropyUnrefined(x, length, stats); } } + else if (isYUsed) + { + bitEntropy.GetEntropyUnrefined(y, length, stats); + } else { - if (isYUsed) - { - bitEntropy.GetEntropyUnrefined(y, length, stats); - } - else - { - stats.Counts[0] = 1; - stats.Streaks[0][length > 3 ? 1 : 0] = length; - bitEntropy.Init(); - } + stats.Counts[0] = 1; + stats.Streaks[0][length > 3 ? 1 : 0] = length; + bitEntropy.Init(); } return bitEntropy.BitsEntropyRefine() + stats.FinalHuffmanCost(); @@ -482,7 +528,7 @@ internal sealed class Vp8LHistogram : IDeepCloneable /// /// Get the symbol entropy for the distribution 'population'. /// - private static double PopulationCost(uint[] population, int length, ref uint trivialSym, ref bool isUsed, Vp8LStreaks stats, Vp8LBitEntropy bitEntropy) + private double PopulationCost(Span population, int length, ref uint trivialSym, int isUsedIndex, Vp8LStreaks stats, Vp8LBitEntropy bitEntropy) { bitEntropy.Init(); stats.Clear(); @@ -491,7 +537,7 @@ internal sealed class Vp8LHistogram : IDeepCloneable trivialSym = (bitEntropy.NoneZeros == 1) ? bitEntropy.NoneZeroCode : NonTrivialSym; // The histogram is used if there is at least one non-zero streak. - isUsed = stats.Streaks[1][0] != 0 || stats.Streaks[1][1] != 0; + this.IsUsed(isUsedIndex, stats.Streaks[1][0] != 0 || stats.Streaks[1][1] != 0); return bitEntropy.BitsEntropyRefine() + stats.FinalHuffmanCost(); } @@ -556,4 +602,17 @@ internal sealed class Vp8LHistogram : IDeepCloneable } } } + + public void Dispose() + { + if (!this.IsDisposed) + { + if (!this.isSetMember) + { + this.buffer.Dispose(); + } + + this.IsDisposed = true; + } + } } diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs new file mode 100644 index 0000000000..0044c7376e --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs @@ -0,0 +1,122 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +#nullable disable + +using System.Buffers; +using System.Collections; +using System.Diagnostics; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless; + +internal sealed class Vp8LHistogramSet : IEnumerable, IDisposable +{ + private readonly IMemoryOwner buffer; + private readonly List items; + private bool isDisposed; + + public Vp8LHistogramSet(MemoryAllocator memoryAllocator, int capacity, int cacheBits) + { + this.buffer = memoryAllocator.Allocate(Vp8LHistogram.BufferSize * capacity, AllocationOptions.Clean); + + this.items = new List(capacity); + for (int i = 0; i < capacity; i++) + { + SetItemMemoryOwner owner = new(this.buffer.Memory.Slice(Vp8LHistogram.BufferSize * i, Vp8LHistogram.BufferSize)); + this.items.Add(new Vp8LHistogram(owner, cacheBits)); + } + } + + public Vp8LHistogramSet(MemoryAllocator memoryAllocator, Vp8LBackwardRefs refs, int capacity, int cacheBits) + { + this.buffer = memoryAllocator.Allocate(Vp8LHistogram.BufferSize * capacity, AllocationOptions.Clean); + + this.items = new List(capacity); + for (int i = 0; i < capacity; i++) + { + SetItemMemoryOwner owner = new(this.buffer.Memory.Slice(Vp8LHistogram.BufferSize * i, Vp8LHistogram.BufferSize)); + this.items.Add(new Vp8LHistogram(owner, refs, cacheBits)); + } + } + + public Vp8LHistogramSet(int capacity) => this.items = new(capacity); + + public Vp8LHistogramSet() => this.items = new(); + + public int Count => this.items.Count; + + public Vp8LHistogram this[int index] + { + get => this.items[index]; + + // TODO: Should we check and throw for null? + set => this.items[index] = value; + } + + public void DisposeAt(int index) + { + this.CheckDisposed(); + + Vp8LHistogram item = this.items[index]; + item?.Dispose(); + this.items[index] = null; + } + + public void RemoveAt(int index) + { + this.CheckDisposed(); + + Vp8LHistogram item = this.items[index]; + item?.Dispose(); + this.items.RemoveAt(index); +#pragma warning disable IDE0059 // Unnecessary assignment of a value + item = null; +#pragma warning restore IDE0059 // Unnecessary assignment of a value + } + + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + this.buffer.Dispose(); + + foreach (Vp8LHistogram item in this.items) + { + item?.Dispose(); + } + + this.items.Clear(); + this.isDisposed = true; + } + + public IEnumerator GetEnumerator() => ((IEnumerable)this.items).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this.items).GetEnumerator(); + + [Conditional("DEBUG")] + private void CheckDisposed() + { + if (this.isDisposed) + { + ThrowDisposed(); + } + } + + private static void ThrowDisposed() => throw new ObjectDisposedException(nameof(Vp8LHistogramSet)); + + private sealed class SetItemMemoryOwner : IMemoryOwner + { + public SetItemMemoryOwner(Memory memory) => this.Memory = memory; + + public Memory Memory { get; } + + public void Dispose() + { + // Do nothing, the underlying memory is owned by the parent set. + } + } +} diff --git a/tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs b/tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs index 65b4ae2c31..c5fa8d03da 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs @@ -43,9 +43,9 @@ public class EncodeWebp [Benchmark(Description = "Magick Webp Lossy")] public void MagickWebpLossy() { - using var memoryStream = new MemoryStream(); + using MemoryStream memoryStream = new(); - var defines = new WebPWriteDefines + WebPWriteDefines defines = new() { Lossless = false, Method = 4, @@ -65,7 +65,7 @@ public class EncodeWebp [Benchmark(Description = "ImageSharp Webp Lossy")] public void ImageSharpWebpLossy() { - using var memoryStream = new MemoryStream(); + using MemoryStream memoryStream = new(); this.webp.Save(memoryStream, new WebpEncoder() { FileFormat = WebpFileFormatType.Lossy, @@ -80,8 +80,8 @@ public class EncodeWebp [Benchmark(Baseline = true, Description = "Magick Webp Lossless")] public void MagickWebpLossless() { - using var memoryStream = new MemoryStream(); - var defines = new WebPWriteDefines + using MemoryStream memoryStream = new(); + WebPWriteDefines defines = new() { Lossless = true, Method = 4, @@ -97,12 +97,13 @@ public class EncodeWebp [Benchmark(Description = "ImageSharp Webp Lossless")] public void ImageSharpWebpLossless() { - using var memoryStream = new MemoryStream(); + using MemoryStream memoryStream = new(); this.webp.Save(memoryStream, new WebpEncoder() { FileFormat = WebpFileFormatType.Lossless, Method = WebpEncodingMethod.Level4, NearLossless = false, + Quality = 75, // This is equal to exact = false in libwebp, which is the default. TransparentColorMode = WebpTransparentColorMode.Clear diff --git a/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs b/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs index 11c4bb62e7..80b41c5e4e 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs @@ -11,7 +11,7 @@ public class DominantCostRangeTests [Fact] public void DominantCost_Constructor() { - var dominantCostRange = new DominantCostRange(); + DominantCostRange dominantCostRange = new(); Assert.Equal(0, dominantCostRange.LiteralMax); Assert.Equal(double.MaxValue, dominantCostRange.LiteralMin); Assert.Equal(0, dominantCostRange.RedMax); @@ -24,8 +24,8 @@ public class DominantCostRangeTests public void UpdateDominantCostRange_Works() { // arrange - var dominantCostRange = new DominantCostRange(); - var histogram = new Vp8LHistogram(10) + DominantCostRange dominantCostRange = new(); + using Vp8LHistogram histogram = new(Configuration.Default.MemoryAllocator, 10) { LiteralCost = 1.0d, RedCost = 2.0d, @@ -50,7 +50,7 @@ public class DominantCostRangeTests public void GetHistoBinIndex_Works(int partitions, int expectedIndex) { // arrange - var dominantCostRange = new DominantCostRange() + DominantCostRange dominantCostRange = new() { BlueMax = 253.4625, BlueMin = 109.0, @@ -59,7 +59,7 @@ public class DominantCostRangeTests RedMax = 191.0, RedMin = 109.0 }; - var histogram = new Vp8LHistogram(6) + using Vp8LHistogram histogram = new(Configuration.Default.MemoryAllocator, 6) { LiteralCost = 247.0d, RedCost = 112.0d, diff --git a/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs b/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs index 39c3c89550..755b735452 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Webp.Lossless; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Tests.TestUtilities; namespace SixLabors.ImageSharp.Tests.Formats.Webp; @@ -65,7 +66,7 @@ public class Vp8LHistogramTests // All remaining values are expected to be zero. literals.AsSpan().CopyTo(expectedLiterals); - var backwardRefs = new Vp8LBackwardRefs(pixelData.Length); + Vp8LBackwardRefs backwardRefs = new(pixelData.Length); for (int i = 0; i < pixelData.Length; i++) { backwardRefs.Add(new PixOrCopy() @@ -76,15 +77,16 @@ public class Vp8LHistogramTests }); } - var histogram0 = new Vp8LHistogram(backwardRefs, 3); - var histogram1 = new Vp8LHistogram(backwardRefs, 3); + MemoryAllocator memoryAllocator = Configuration.Default.MemoryAllocator; + using Vp8LHistogram histogram0 = new(memoryAllocator, backwardRefs, 3); + using Vp8LHistogram histogram1 = new(memoryAllocator, backwardRefs, 3); for (int i = 0; i < 5; i++) { - histogram0.IsUsed[i] = true; - histogram1.IsUsed[i] = true; + histogram0.IsUsed(i, true); + histogram1.IsUsed(i, true); } - var output = new Vp8LHistogram(3); + using Vp8LHistogram output = new(memoryAllocator, 3); // act histogram0.Add(histogram1, output); From 63829e84377dcfd00b3ee43347d297977a355fd9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 8 Oct 2023 22:18:48 +1000 Subject: [PATCH 02/24] Remove more allocations and add tasks. --- .../Webp/Lossless/BackwardReferenceEncoder.cs | 23 ++++++++----------- .../Formats/Webp/Lossless/HistogramEncoder.cs | 23 +++++++++---------- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 7 +++--- 3 files changed, 25 insertions(+), 28 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs index 922ae0193d..a56fc0faca 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs @@ -52,6 +52,8 @@ internal static class BackwardReferenceEncoder Vp8LHashChain? hashChainBox = null; Vp8LStreaks stats = new(); Vp8LBitEntropy bitsEntropy = new(); + + ColorCache[] colorCache = new ColorCache[WebpConstants.MaxColorCacheBits + 1]; for (int lz77Type = 1; lz77TypesToTry > 0; lz77TypesToTry &= ~lz77Type, lz77Type <<= 1) { int cacheBitsTmp = cacheBitsInitial; @@ -76,7 +78,7 @@ internal static class BackwardReferenceEncoder } // Next, try with a color cache and update the references. - cacheBitsTmp = CalculateBestCacheSize(memoryAllocator, bgra, quality, worst, cacheBitsTmp); + cacheBitsTmp = CalculateBestCacheSize(memoryAllocator, colorCache, bgra, quality, worst, cacheBitsTmp); if (cacheBitsTmp > 0) { BackwardRefsWithLocalCache(bgra, cacheBitsTmp, worst); @@ -123,6 +125,7 @@ internal static class BackwardReferenceEncoder /// Best cache size. private static int CalculateBestCacheSize( MemoryAllocator memoryAllocator, + Span colorCache, ReadOnlySpan bgra, uint quality, Vp8LBackwardRefs refs, @@ -138,12 +141,8 @@ internal static class BackwardReferenceEncoder double entropyMin = MaxEntropy; int pos = 0; - // TODO: Pass from outer loop and clear. - ColorCache[] colorCache = new ColorCache[WebpConstants.MaxColorCacheBits + 1]; - - // TODO: Use fixed size. - using Vp8LHistogramSet histos = new(memoryAllocator, WebpConstants.MaxColorCacheBits + 1, 0); - for (int i = 0; i <= WebpConstants.MaxColorCacheBits; i++) + using Vp8LHistogramSet histos = new(memoryAllocator, colorCache.Length, 0); + for (int i = 0; i < colorCache.Length; i++) { histos[i].PaletteCodeBits = i; colorCache[i] = new ColorCache(i); @@ -448,12 +447,12 @@ internal static class BackwardReferenceEncoder int ix = useColorCache ? colorCache!.Contains(color) : -1; if (ix >= 0) { - double mul0 = 0.68; + const double mul0 = 0.68; costVal += costModel.GetCacheCost((uint)ix) * mul0; } else { - double mul1 = 0.82; + const double mul1 = 0.82; if (useColorCache) { colorCache!.Insert(color); @@ -700,10 +699,8 @@ internal static class BackwardReferenceEncoder bestLength = MaxLength; break; } - else - { - bestLength = currLength; - } + + bestLength = currLength; } } } diff --git a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs index 8a0d132063..cce0356176 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. #nullable disable +using System.Buffers; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; @@ -45,9 +46,9 @@ internal static class HistogramEncoder int imageHistoRawSize = histoXSize * histoYSize; const int entropyCombineNumBins = BinSize; - // TODO: Allocations! - ushort[] mapTmp = new ushort[imageHistoRawSize]; - ushort[] clusterMappings = new ushort[imageHistoRawSize]; + using IMemoryOwner tmp = memoryAllocator.Allocate(imageHistoRawSize * 2, AllocationOptions.Clean); + Span mapTmp = tmp.Slice(0, imageHistoRawSize); + Span clusterMappings = tmp.Slice(imageHistoRawSize, imageHistoRawSize); using Vp8LHistogramSet origHisto = new(memoryAllocator, imageHistoRawSize, cacheBits); @@ -60,13 +61,12 @@ internal static class HistogramEncoder bool entropyCombine = numUsed > entropyCombineNumBins * 2 && quality < 100; if (entropyCombine) { - ushort[] binMap = mapTmp; int numClusters = numUsed; double combineCostFactor = GetCombineCostFactor(imageHistoRawSize, quality); - HistogramAnalyzeEntropyBin(imageHisto, binMap); + HistogramAnalyzeEntropyBin(imageHisto, mapTmp); // Collapse histograms with similar entropy. - HistogramCombineEntropyBin(imageHisto, histogramSymbols, clusterMappings, tmpHisto, binMap, entropyCombineNumBins, combineCostFactor); + HistogramCombineEntropyBin(imageHisto, histogramSymbols, clusterMappings, tmpHisto, mapTmp, entropyCombineNumBins, combineCostFactor); OptimizeHistogramSymbols(clusterMappings, numClusters, mapTmp, histogramSymbols); } @@ -128,7 +128,7 @@ internal static class HistogramEncoder /// Partition histograms to different entropy bins for three dominant (literal, /// red and blue) symbol costs and compute the histogram aggregate bitCost. /// - private static void HistogramAnalyzeEntropyBin(Vp8LHistogramSet histograms, ushort[] binMap) + private static void HistogramAnalyzeEntropyBin(Vp8LHistogramSet histograms, Span binMap) { int histoSize = histograms.Count; DominantCostRange costRange = new(); @@ -198,9 +198,9 @@ internal static class HistogramEncoder private static void HistogramCombineEntropyBin( Vp8LHistogramSet histograms, Span clusters, - ushort[] clusterMappings, + Span clusterMappings, Vp8LHistogram curCombo, - ushort[] binMap, + ReadOnlySpan binMap, int numBins, double combineCostFactor) { @@ -276,7 +276,7 @@ internal static class HistogramEncoder /// Given a Histogram set, the mapping of clusters 'clusterMapping' and the /// current assignment of the cells in 'symbols', merge the clusters and assign the smallest possible clusters values. /// - private static void OptimizeHistogramSymbols(ushort[] clusterMappings, int numClusters, ushort[] clusterMappingsTmp, Span symbols) + private static void OptimizeHistogramSymbols(Span clusterMappings, int numClusters, Span clusterMappingsTmp, Span symbols) { bool doContinue = true; @@ -303,7 +303,7 @@ internal static class HistogramEncoder // Create a mapping from a cluster id to its minimal version. int clusterMax = 0; - clusterMappingsTmp.AsSpan().Clear(); + clusterMappingsTmp.Clear(); // Re-map the ids. for (int i = 0; i < symbols.Length; i++) @@ -515,7 +515,6 @@ internal static class HistogramEncoder histograms.DisposeAt(idx2); // Remove pairs intersecting the just combined best pair. - // TODO: Reversing this will avoid the need to remove from the end of the list. for (int i = 0; i < histoPriorityList.Count;) { HistogramPair p = histoPriorityList[i]; diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 10fc8ab804..d570bd448a 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -885,7 +885,7 @@ internal class Vp8LEncoder : IDisposable private void StoreFullHuffmanCode(Span huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode tree) { - // TODO: Allocations. + // TODO: Allocations. This method is called in a loop. int i; byte[] codeLengthBitDepth = new byte[WebpConstants.CodeLengthCodes]; short[] codeLengthBitDepthSymbols = new short[WebpConstants.CodeLengthCodes]; @@ -1628,6 +1628,7 @@ internal class Vp8LEncoder : IDisposable } } + // TODO: Allocations. int end = 5 * histogramImage.Count; for (int i = 0; i < end; i++) { @@ -1641,9 +1642,9 @@ internal class Vp8LEncoder : IDisposable } // Create Huffman trees. - // TODO: Allocations. Size here has a max and can be sliced. + // TODO: Allocations. bool[] bufRle = new bool[maxNumSymbols]; - Span huffTree = stackalloc HuffmanTree[3 * maxNumSymbols]; + HuffmanTree[] huffTree = new HuffmanTree[3 * maxNumSymbols]; for (int i = 0; i < histogramImage.Count; i++) { From bea65987a067c0f1f9efa8bc5953d86db2a21976 Mon Sep 17 00:00:00 2001 From: antonfirsov Date: Mon, 9 Oct 2023 00:35:51 +0200 Subject: [PATCH 03/24] use pinned buffers in Vp8LHistogram --- .../Webp/Lossless/BackwardReferenceEncoder.cs | 4 +- .../Formats/Webp/Lossless/CostModel.cs | 2 +- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 2 +- .../Formats/Webp/Lossless/Vp8LHistogram.cs | 92 +++++++++++-------- .../Formats/Webp/Lossless/Vp8LHistogramSet.cs | 24 ++--- .../Formats/WebP/DominantCostRangeTests.cs | 23 ++--- .../Formats/WebP/Vp8LHistogramTests.cs | 6 +- 7 files changed, 77 insertions(+), 76 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs index a56fc0faca..185ba1e346 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs @@ -85,7 +85,7 @@ internal static class BackwardReferenceEncoder } // Keep the best backward references. - using Vp8LHistogram histo = new(memoryAllocator, worst, cacheBitsTmp); + using Vp8LHistogram histo = Vp8LHistogram.Create(memoryAllocator, worst, cacheBitsTmp); double bitCost = histo.EstimateBits(stats, bitsEntropy); if (lz77TypeBest == 0 || bitCost < bitCostBest) @@ -102,7 +102,7 @@ internal static class BackwardReferenceEncoder { Vp8LHashChain hashChainTmp = lz77TypeBest == (int)Vp8LLz77Type.Lz77Standard ? hashChain : hashChainBox!; BackwardReferencesTraceBackwards(width, height, memoryAllocator, bgra, cacheBits, hashChainTmp, best, worst); - using Vp8LHistogram histo = new(memoryAllocator, worst, cacheBits); + using Vp8LHistogram histo = Vp8LHistogram.Create(memoryAllocator, worst, cacheBits); double bitCostTrace = histo.EstimateBits(stats, bitsEntropy); if (bitCostTrace < bitCostBest) { diff --git a/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs b/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs index 975fd581d7..94d60b4ee3 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs @@ -37,7 +37,7 @@ internal class CostModel public void Build(int xSize, int cacheBits, Vp8LBackwardRefs backwardRefs) { - using Vp8LHistogram histogram = new(this.memoryAllocator, cacheBits); + using Vp8LHistogram histogram = Vp8LHistogram.Create(this.memoryAllocator, cacheBits); // The following code is similar to HistogramCreate but converts the distance to plane code. for (int i = 0; i < backwardRefs.Refs.Count; i++) diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index d570bd448a..532e75359d 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -589,7 +589,7 @@ internal class Vp8LEncoder : IDisposable Vp8LBackwardRefs refsTmp = this.Refs[refsBest.Equals(this.Refs[0]) ? 1 : 0]; this.bitWriter.Reset(bwInit); - using Vp8LHistogram tmpHisto = new(this.memoryAllocator, cacheBits); + using Vp8LHistogram tmpHisto = Vp8LHistogram.Create(this.memoryAllocator, cacheBits); using Vp8LHistogramSet histogramImage = new(this.memoryAllocator, histogramImageXySize, cacheBits); // Build histogram image and symbols from backward references. diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs index 07ee88f259..023f1c943b 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs @@ -10,10 +10,20 @@ using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Webp.Lossless; -internal sealed class Vp8LHistogram : IDisposable +internal sealed unsafe class Vp8LHistogram : IDisposable { private const uint NonTrivialSym = 0xffffffff; - private readonly IMemoryOwner buffer; + private readonly IMemoryOwner? bufferOwner; + private readonly Memory buffer; + private readonly MemoryHandle bufferHandle; + + private readonly uint* red; + private readonly uint* blue; + private readonly uint* alpha; + private readonly uint* distance; + private readonly uint* literal; + private readonly uint* isUsed; + private const int RedSize = WebpConstants.NumLiteralCodes; private const int BlueSize = WebpConstants.NumLiteralCodes; private const int AlphaSize = WebpConstants.NumLiteralCodes; @@ -21,27 +31,6 @@ internal sealed class Vp8LHistogram : IDisposable public const int LiteralSize = WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes + (1 << WebpConstants.MaxColorCacheBits) + 1; private const int UsedSize = 5; // 5 for literal, red, blue, alpha, distance public const int BufferSize = RedSize + BlueSize + AlphaSize + DistanceSize + LiteralSize + UsedSize; - private readonly bool isSetMember; - - /// - /// Initializes a new instance of the class. - /// - /// The memory allocator. - /// The backward references to initialize the histogram with. - /// The palette code bits. - public Vp8LHistogram(MemoryAllocator memoryAllocator, Vp8LBackwardRefs refs, int paletteCodeBits) - : this(memoryAllocator, paletteCodeBits) => this.StoreRefs(refs); - - /// - /// Initializes a new instance of the class. - /// - /// The memory allocator. - /// The palette code bits. - public Vp8LHistogram(MemoryAllocator memoryAllocator, int paletteCodeBits) - { - this.buffer = memoryAllocator.Allocate(BufferSize, AllocationOptions.Clean); - this.PaletteCodeBits = paletteCodeBits; - } /// /// Initializes a new instance of the class. @@ -52,7 +41,7 @@ internal sealed class Vp8LHistogram : IDisposable /// The backing buffer. /// The backward references to initialize the histogram with. /// The palette code bits. - public Vp8LHistogram(IMemoryOwner buffer, Vp8LBackwardRefs refs, int paletteCodeBits) + public Vp8LHistogram(Memory buffer, Vp8LBackwardRefs refs, int paletteCodeBits) : this(buffer, paletteCodeBits) => this.StoreRefs(refs); /// @@ -63,11 +52,20 @@ internal sealed class Vp8LHistogram : IDisposable /// /// The backing buffer. /// The palette code bits. - public Vp8LHistogram(IMemoryOwner buffer, int paletteCodeBits) + /// Optional buffer owner to dispose. + public Vp8LHistogram(Memory buffer, int paletteCodeBits, IMemoryOwner? bufferOwner = null) { + this.bufferOwner = bufferOwner; this.buffer = buffer; + this.bufferHandle = this.buffer.Pin(); this.PaletteCodeBits = paletteCodeBits; - this.isSetMember = true; + + this.red = (uint*)this.bufferHandle.Pointer; + this.blue = this.red + RedSize; + this.alpha = this.blue + BlueSize; + this.distance = this.alpha + AlphaSize; + this.literal = this.distance + DistanceSize; + this.isUsed = this.literal + LiteralSize; } /// @@ -95,22 +93,43 @@ internal sealed class Vp8LHistogram : IDisposable /// public double BlueCost { get; set; } - public Span Red => this.buffer.GetSpan()[..RedSize]; + public Span Red => new(this.red, RedSize); - public Span Blue => this.buffer.GetSpan().Slice(RedSize, BlueSize); + public Span Blue => new(this.blue, BlueSize); - public Span Alpha => this.buffer.GetSpan().Slice(RedSize + BlueSize, AlphaSize); + public Span Alpha => new(this.alpha, AlphaSize); - public Span Distance => this.buffer.GetSpan().Slice(RedSize + BlueSize + AlphaSize, DistanceSize); + public Span Distance => new(this.distance, DistanceSize); - public Span Literal => this.buffer.GetSpan().Slice(RedSize + BlueSize + AlphaSize + DistanceSize, LiteralSize); + public Span Literal => new(this.literal, LiteralSize); public uint TrivialSymbol { get; set; } - private Span IsUsedSpan => this.buffer.GetSpan().Slice(RedSize + BlueSize + AlphaSize + DistanceSize + LiteralSize, UsedSize); + private Span IsUsedSpan => new(this.isUsed, UsedSize); + + private Span TotalSpan => new(this.red, BufferSize); public bool IsDisposed { get; set; } + /// + /// Creates an that is not a member of a . + /// + public static Vp8LHistogram Create(MemoryAllocator memoryAllocator, int paletteCodeBits) + { + IMemoryOwner bufferOwner = memoryAllocator.Allocate(BufferSize, AllocationOptions.Clean); + return new Vp8LHistogram(bufferOwner.Memory, paletteCodeBits, bufferOwner); + } + + /// + /// Creates an that is not a member of a . + /// + public static Vp8LHistogram Create(MemoryAllocator memoryAllocator, Vp8LBackwardRefs refs, int paletteCodeBits) + { + Vp8LHistogram histogram = Create(memoryAllocator, paletteCodeBits); + histogram.StoreRefs(refs); + return histogram; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IsUsed(int index) => this.IsUsedSpan[index] == 1u; @@ -140,7 +159,7 @@ internal sealed class Vp8LHistogram : IDisposable public void Clear() { - this.buffer.Clear(); + this.TotalSpan.Clear(); this.PaletteCodeBits = 0; this.BitCost = 0; this.LiteralCost = 0; @@ -607,11 +626,8 @@ internal sealed class Vp8LHistogram : IDisposable { if (!this.IsDisposed) { - if (!this.isSetMember) - { - this.buffer.Dispose(); - } - + this.bufferHandle.Dispose(); + this.bufferOwner?.Dispose(); this.IsDisposed = true; } } diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs index 0044c7376e..b7b884dfc8 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs @@ -23,8 +23,8 @@ internal sealed class Vp8LHistogramSet : IEnumerable, IDisposable this.items = new List(capacity); for (int i = 0; i < capacity; i++) { - SetItemMemoryOwner owner = new(this.buffer.Memory.Slice(Vp8LHistogram.BufferSize * i, Vp8LHistogram.BufferSize)); - this.items.Add(new Vp8LHistogram(owner, cacheBits)); + Memory subBuffer = this.buffer.Memory.Slice(Vp8LHistogram.BufferSize * i, Vp8LHistogram.BufferSize); + this.items.Add(new Vp8LHistogram(subBuffer, cacheBits)); } } @@ -35,8 +35,8 @@ internal sealed class Vp8LHistogramSet : IEnumerable, IDisposable this.items = new List(capacity); for (int i = 0; i < capacity; i++) { - SetItemMemoryOwner owner = new(this.buffer.Memory.Slice(Vp8LHistogram.BufferSize * i, Vp8LHistogram.BufferSize)); - this.items.Add(new Vp8LHistogram(owner, refs, cacheBits)); + Memory subBuffer = this.buffer.Memory.Slice(Vp8LHistogram.BufferSize * i, Vp8LHistogram.BufferSize); + this.items.Add(new Vp8LHistogram(subBuffer, refs, cacheBits)); } } @@ -82,13 +82,13 @@ internal sealed class Vp8LHistogramSet : IEnumerable, IDisposable return; } - this.buffer.Dispose(); - foreach (Vp8LHistogram item in this.items) { + // First, make sure to unpin individual sub buffers. item?.Dispose(); } + this.buffer.Dispose(); this.items.Clear(); this.isDisposed = true; } @@ -107,16 +107,4 @@ internal sealed class Vp8LHistogramSet : IEnumerable, IDisposable } private static void ThrowDisposed() => throw new ObjectDisposedException(nameof(Vp8LHistogramSet)); - - private sealed class SetItemMemoryOwner : IMemoryOwner - { - public SetItemMemoryOwner(Memory memory) => this.Memory = memory; - - public Memory Memory { get; } - - public void Dispose() - { - // Do nothing, the underlying memory is owned by the parent set. - } - } } diff --git a/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs b/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs index 80b41c5e4e..5e3f6d0c9f 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs @@ -25,12 +25,10 @@ public class DominantCostRangeTests { // arrange DominantCostRange dominantCostRange = new(); - using Vp8LHistogram histogram = new(Configuration.Default.MemoryAllocator, 10) - { - LiteralCost = 1.0d, - RedCost = 2.0d, - BlueCost = 3.0d - }; + using Vp8LHistogram histogram = Vp8LHistogram.Create(Configuration.Default.MemoryAllocator, 10); + histogram.LiteralCost = 1.0d; + histogram.RedCost = 2.0d; + histogram.BlueCost = 3.0d; // act dominantCostRange.UpdateDominantCostRange(histogram); @@ -59,13 +57,12 @@ public class DominantCostRangeTests RedMax = 191.0, RedMin = 109.0 }; - using Vp8LHistogram histogram = new(Configuration.Default.MemoryAllocator, 6) - { - LiteralCost = 247.0d, - RedCost = 112.0d, - BlueCost = 202.0d, - BitCost = 733.0d - }; + using Vp8LHistogram histogram = Vp8LHistogram.Create(Configuration.Default.MemoryAllocator, 6); + histogram.LiteralCost = 247.0d; + histogram.RedCost = 112.0d; + histogram.BlueCost = 202.0d; + histogram.BitCost = 733.0d; + dominantCostRange.UpdateDominantCostRange(histogram); // act diff --git a/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs b/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs index 755b735452..c27d30eeab 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs @@ -78,15 +78,15 @@ public class Vp8LHistogramTests } MemoryAllocator memoryAllocator = Configuration.Default.MemoryAllocator; - using Vp8LHistogram histogram0 = new(memoryAllocator, backwardRefs, 3); - using Vp8LHistogram histogram1 = new(memoryAllocator, backwardRefs, 3); + using Vp8LHistogram histogram0 = Vp8LHistogram.Create(memoryAllocator, backwardRefs, 3); + using Vp8LHistogram histogram1 = Vp8LHistogram.Create(memoryAllocator, backwardRefs, 3); for (int i = 0; i < 5; i++) { histogram0.IsUsed(i, true); histogram1.IsUsed(i, true); } - using Vp8LHistogram output = new(memoryAllocator, 3); + using Vp8LHistogram output = Vp8LHistogram.Create(memoryAllocator, 3); // act histogram0.Add(histogram1, output); From 83ced124c4a077eea9d913de7b6b2c74b62be182 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 16 Oct 2023 15:38:58 +1000 Subject: [PATCH 04/24] Split Vp8LHistogram and clean up --- .../Webp/Lossless/BackwardReferenceEncoder.cs | 4 +- .../Formats/Webp/Lossless/CostModel.cs | 2 +- .../Formats/Webp/Lossless/HistogramEncoder.cs | 10 +- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 2 +- .../Formats/Webp/Lossless/Vp8LHistogram.cs | 99 ++++++++++--------- .../Formats/Webp/Lossless/Vp8LHistogramSet.cs | 62 ++++++------ .../Formats/WebP/DominantCostRangeTests.cs | 4 +- .../Formats/WebP/Vp8LHistogramTests.cs | 6 +- 8 files changed, 98 insertions(+), 91 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs index 185ba1e346..211185dbba 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs @@ -85,7 +85,7 @@ internal static class BackwardReferenceEncoder } // Keep the best backward references. - using Vp8LHistogram histo = Vp8LHistogram.Create(memoryAllocator, worst, cacheBitsTmp); + using OwnedVp8LHistogram histo = OwnedVp8LHistogram.Create(memoryAllocator, worst, cacheBitsTmp); double bitCost = histo.EstimateBits(stats, bitsEntropy); if (lz77TypeBest == 0 || bitCost < bitCostBest) @@ -102,7 +102,7 @@ internal static class BackwardReferenceEncoder { Vp8LHashChain hashChainTmp = lz77TypeBest == (int)Vp8LLz77Type.Lz77Standard ? hashChain : hashChainBox!; BackwardReferencesTraceBackwards(width, height, memoryAllocator, bgra, cacheBits, hashChainTmp, best, worst); - using Vp8LHistogram histo = Vp8LHistogram.Create(memoryAllocator, worst, cacheBits); + using OwnedVp8LHistogram histo = OwnedVp8LHistogram.Create(memoryAllocator, worst, cacheBits); double bitCostTrace = histo.EstimateBits(stats, bitsEntropy); if (bitCostTrace < bitCostBest) { diff --git a/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs b/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs index 94d60b4ee3..beebc48abc 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs @@ -37,7 +37,7 @@ internal class CostModel public void Build(int xSize, int cacheBits, Vp8LBackwardRefs backwardRefs) { - using Vp8LHistogram histogram = Vp8LHistogram.Create(this.memoryAllocator, cacheBits); + using OwnedVp8LHistogram histogram = OwnedVp8LHistogram.Create(this.memoryAllocator, cacheBits); // The following code is similar to HistogramCreate but converts the distance to plane code. for (int i = 0; i < backwardRefs.Refs.Count; i++) diff --git a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs index cce0356176..6c2d18a919 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs @@ -172,8 +172,8 @@ internal static class HistogramEncoder // Skip the histogram if it is completely empty, which can happen for tiles with no information (when they are skipped because of LZ77). if (!origHistogram.IsUsed(0) && !origHistogram.IsUsed(1) && !origHistogram.IsUsed(2) && !origHistogram.IsUsed(3) && !origHistogram.IsUsed(4)) { - origHistograms.DisposeAt(i); - histograms.DisposeAt(i); + origHistograms[i] = null; + histograms[i] = null; histogramSymbols[i] = InvalidHistogramSymbol; } else @@ -254,7 +254,7 @@ internal static class HistogramEncoder // Move the (better) merged histogram to its final slot. (histograms[first], curCombo) = (curCombo, histograms[first]); - histograms.DisposeAt(idx); + histograms[idx] = null; indicesToRemove.Add(idx); clusterMappings[clusters[idx]] = clusters[first]; } @@ -415,7 +415,7 @@ internal static class HistogramEncoder // Merge the histograms and remove bestIdx2 from the list. HistogramAdd(histograms[bestIdx2], histograms[bestIdx1], histograms[bestIdx1]); histograms[bestIdx1].BitCost = histoPriorityList[0].CostCombo; - histograms.DisposeAt(bestIdx2); + histograms[bestIdx2] = null; numUsed--; for (int j = 0; j < histoPriorityList.Count;) @@ -512,7 +512,7 @@ internal static class HistogramEncoder histograms[idx1].BitCost = histoPriorityList[0].CostCombo; // Remove merged histogram. - histograms.DisposeAt(idx2); + histograms[idx2] = null; // Remove pairs intersecting the just combined best pair. for (int i = 0; i < histoPriorityList.Count;) diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index b53180a4ff..878d487a86 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -589,7 +589,7 @@ internal class Vp8LEncoder : IDisposable Vp8LBackwardRefs refsTmp = this.Refs[refsBest.Equals(this.Refs[0]) ? 1 : 0]; this.bitWriter.Reset(bwInit); - using Vp8LHistogram tmpHisto = Vp8LHistogram.Create(this.memoryAllocator, cacheBits); + using OwnedVp8LHistogram tmpHisto = OwnedVp8LHistogram.Create(this.memoryAllocator, cacheBits); using Vp8LHistogramSet histogramImage = new(this.memoryAllocator, histogramImageXySize, cacheBits); // Build histogram image and symbols from backward references. diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs index 023f1c943b..f473977908 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs @@ -10,13 +10,9 @@ using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Webp.Lossless; -internal sealed unsafe class Vp8LHistogram : IDisposable +internal abstract unsafe class Vp8LHistogram { private const uint NonTrivialSym = 0xffffffff; - private readonly IMemoryOwner? bufferOwner; - private readonly Memory buffer; - private readonly MemoryHandle bufferHandle; - private readonly uint* red; private readonly uint* blue; private readonly uint* alpha; @@ -35,32 +31,21 @@ internal sealed unsafe class Vp8LHistogram : IDisposable /// /// Initializes a new instance of the class. /// - /// - /// This constructor should be used when the histogram is a member of a . - /// - /// The backing buffer. + /// The base pointer to the backing memory. /// The backward references to initialize the histogram with. /// The palette code bits. - public Vp8LHistogram(Memory buffer, Vp8LBackwardRefs refs, int paletteCodeBits) - : this(buffer, paletteCodeBits) => this.StoreRefs(refs); + protected Vp8LHistogram(uint* basePointer, Vp8LBackwardRefs refs, int paletteCodeBits) + : this(basePointer, paletteCodeBits) => this.StoreRefs(refs); /// /// Initializes a new instance of the class. /// - /// - /// This constructor should be used when the histogram is a member of a . - /// - /// The backing buffer. + /// The base pointer to the backing memory. /// The palette code bits. - /// Optional buffer owner to dispose. - public Vp8LHistogram(Memory buffer, int paletteCodeBits, IMemoryOwner? bufferOwner = null) + protected Vp8LHistogram(uint* basePointer, int paletteCodeBits) { - this.bufferOwner = bufferOwner; - this.buffer = buffer; - this.bufferHandle = this.buffer.Pin(); this.PaletteCodeBits = paletteCodeBits; - - this.red = (uint*)this.bufferHandle.Pointer; + this.red = basePointer; this.blue = this.red + RedSize; this.alpha = this.blue + BlueSize; this.distance = this.alpha + AlphaSize; @@ -109,27 +94,6 @@ internal sealed unsafe class Vp8LHistogram : IDisposable private Span TotalSpan => new(this.red, BufferSize); - public bool IsDisposed { get; set; } - - /// - /// Creates an that is not a member of a . - /// - public static Vp8LHistogram Create(MemoryAllocator memoryAllocator, int paletteCodeBits) - { - IMemoryOwner bufferOwner = memoryAllocator.Allocate(BufferSize, AllocationOptions.Clean); - return new Vp8LHistogram(bufferOwner.Memory, paletteCodeBits, bufferOwner); - } - - /// - /// Creates an that is not a member of a . - /// - public static Vp8LHistogram Create(MemoryAllocator memoryAllocator, Vp8LBackwardRefs refs, int paletteCodeBits) - { - Vp8LHistogram histogram = Create(memoryAllocator, paletteCodeBits); - histogram.StoreRefs(refs); - return histogram; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IsUsed(int index) => this.IsUsedSpan[index] == 1u; @@ -621,14 +585,57 @@ internal sealed unsafe class Vp8LHistogram : IDisposable } } } +} + +internal sealed unsafe class OwnedVp8LHistogram : Vp8LHistogram, IDisposable +{ + private readonly IMemoryOwner bufferOwner; + private MemoryHandle bufferHandle; + private bool isDisposed; + + private OwnedVp8LHistogram( + IMemoryOwner bufferOwner, + ref MemoryHandle bufferHandle, + uint* basePointer, + int paletteCodeBits) + : base(basePointer, paletteCodeBits) + { + this.bufferOwner = bufferOwner; + this.bufferHandle = bufferHandle; + } + + /// + /// Creates an that is not a member of a . + /// + /// The memory allocator. + /// The palette code bits. + public static OwnedVp8LHistogram Create(MemoryAllocator memoryAllocator, int paletteCodeBits) + { + IMemoryOwner bufferOwner = memoryAllocator.Allocate(BufferSize, AllocationOptions.Clean); + MemoryHandle bufferHandle = bufferOwner.Memory.Pin(); + return new OwnedVp8LHistogram(bufferOwner, ref bufferHandle, (uint*)bufferHandle.Pointer, paletteCodeBits); + } + + /// + /// Creates an that is not a member of a . + /// + /// The memory allocator. + /// The backward references to initialize the histogram with. + /// The palette code bits. + public static OwnedVp8LHistogram Create(MemoryAllocator memoryAllocator, Vp8LBackwardRefs refs, int paletteCodeBits) + { + OwnedVp8LHistogram histogram = Create(memoryAllocator, paletteCodeBits); + histogram.StoreRefs(refs); + return histogram; + } public void Dispose() { - if (!this.IsDisposed) + if (!this.isDisposed) { this.bufferHandle.Dispose(); - this.bufferOwner?.Dispose(); - this.IsDisposed = true; + this.bufferOwner.Dispose(); + this.isDisposed = true; } } } diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs index b7b884dfc8..a46838ee67 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs @@ -13,30 +13,39 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless; internal sealed class Vp8LHistogramSet : IEnumerable, IDisposable { private readonly IMemoryOwner buffer; + private MemoryHandle bufferHandle; private readonly List items; private bool isDisposed; public Vp8LHistogramSet(MemoryAllocator memoryAllocator, int capacity, int cacheBits) { this.buffer = memoryAllocator.Allocate(Vp8LHistogram.BufferSize * capacity, AllocationOptions.Clean); + this.bufferHandle = this.buffer.Memory.Pin(); - this.items = new List(capacity); - for (int i = 0; i < capacity; i++) + unsafe { - Memory subBuffer = this.buffer.Memory.Slice(Vp8LHistogram.BufferSize * i, Vp8LHistogram.BufferSize); - this.items.Add(new Vp8LHistogram(subBuffer, cacheBits)); + uint* basePointer = (uint*)this.bufferHandle.Pointer; + this.items = new List(capacity); + for (int i = 0; i < capacity; i++) + { + this.items.Add(new MemberVp8LHistogram(basePointer + (Vp8LHistogram.BufferSize * i), cacheBits)); + } } } public Vp8LHistogramSet(MemoryAllocator memoryAllocator, Vp8LBackwardRefs refs, int capacity, int cacheBits) { this.buffer = memoryAllocator.Allocate(Vp8LHistogram.BufferSize * capacity, AllocationOptions.Clean); + this.bufferHandle = this.buffer.Memory.Pin(); - this.items = new List(capacity); - for (int i = 0; i < capacity; i++) + unsafe { - Memory subBuffer = this.buffer.Memory.Slice(Vp8LHistogram.BufferSize * i, Vp8LHistogram.BufferSize); - this.items.Add(new Vp8LHistogram(subBuffer, refs, cacheBits)); + uint* basePointer = (uint*)this.bufferHandle.Pointer; + this.items = new List(capacity); + for (int i = 0; i < capacity; i++) + { + this.items.Add(new MemberVp8LHistogram(basePointer + (Vp8LHistogram.BufferSize * i), refs, cacheBits)); + } } } @@ -49,30 +58,13 @@ internal sealed class Vp8LHistogramSet : IEnumerable, IDisposable public Vp8LHistogram this[int index] { get => this.items[index]; - - // TODO: Should we check and throw for null? set => this.items[index] = value; } - public void DisposeAt(int index) - { - this.CheckDisposed(); - - Vp8LHistogram item = this.items[index]; - item?.Dispose(); - this.items[index] = null; - } - public void RemoveAt(int index) { this.CheckDisposed(); - - Vp8LHistogram item = this.items[index]; - item?.Dispose(); this.items.RemoveAt(index); -#pragma warning disable IDE0059 // Unnecessary assignment of a value - item = null; -#pragma warning restore IDE0059 // Unnecessary assignment of a value } public void Dispose() @@ -82,13 +74,8 @@ internal sealed class Vp8LHistogramSet : IEnumerable, IDisposable return; } - foreach (Vp8LHistogram item in this.items) - { - // First, make sure to unpin individual sub buffers. - item?.Dispose(); - } - this.buffer.Dispose(); + this.bufferHandle.Dispose(); this.items.Clear(); this.isDisposed = true; } @@ -107,4 +94,17 @@ internal sealed class Vp8LHistogramSet : IEnumerable, IDisposable } private static void ThrowDisposed() => throw new ObjectDisposedException(nameof(Vp8LHistogramSet)); + + private sealed unsafe class MemberVp8LHistogram : Vp8LHistogram + { + public MemberVp8LHistogram(uint* basePointer, int paletteCodeBits) + : base(basePointer, paletteCodeBits) + { + } + + public MemberVp8LHistogram(uint* basePointer, Vp8LBackwardRefs refs, int paletteCodeBits) + : base(basePointer, refs, paletteCodeBits) + { + } + } } diff --git a/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs b/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs index 5e3f6d0c9f..9c48e61823 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs @@ -25,7 +25,7 @@ public class DominantCostRangeTests { // arrange DominantCostRange dominantCostRange = new(); - using Vp8LHistogram histogram = Vp8LHistogram.Create(Configuration.Default.MemoryAllocator, 10); + using OwnedVp8LHistogram histogram = OwnedVp8LHistogram.Create(Configuration.Default.MemoryAllocator, 10); histogram.LiteralCost = 1.0d; histogram.RedCost = 2.0d; histogram.BlueCost = 3.0d; @@ -57,7 +57,7 @@ public class DominantCostRangeTests RedMax = 191.0, RedMin = 109.0 }; - using Vp8LHistogram histogram = Vp8LHistogram.Create(Configuration.Default.MemoryAllocator, 6); + using OwnedVp8LHistogram histogram = OwnedVp8LHistogram.Create(Configuration.Default.MemoryAllocator, 6); histogram.LiteralCost = 247.0d; histogram.RedCost = 112.0d; histogram.BlueCost = 202.0d; diff --git a/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs b/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs index c27d30eeab..cfe79e49e6 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs @@ -78,15 +78,15 @@ public class Vp8LHistogramTests } MemoryAllocator memoryAllocator = Configuration.Default.MemoryAllocator; - using Vp8LHistogram histogram0 = Vp8LHistogram.Create(memoryAllocator, backwardRefs, 3); - using Vp8LHistogram histogram1 = Vp8LHistogram.Create(memoryAllocator, backwardRefs, 3); + using OwnedVp8LHistogram histogram0 = OwnedVp8LHistogram.Create(memoryAllocator, backwardRefs, 3); + using OwnedVp8LHistogram histogram1 = OwnedVp8LHistogram.Create(memoryAllocator, backwardRefs, 3); for (int i = 0; i < 5; i++) { histogram0.IsUsed(i, true); histogram1.IsUsed(i, true); } - using Vp8LHistogram output = Vp8LHistogram.Create(memoryAllocator, 3); + using OwnedVp8LHistogram output = OwnedVp8LHistogram.Create(memoryAllocator, 3); // act histogram0.Add(histogram1, output); From 2f61d94e37c9f7a954c0f48c4bb47ba0db80f419 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 17 Oct 2023 10:25:50 +1000 Subject: [PATCH 05/24] Update src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs Co-authored-by: Anton Firszov --- src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs index 6c2d18a919..3a96362cfd 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs @@ -383,8 +383,7 @@ internal static class HistogramEncoder idx2 = mappings[idx2]; // Calculate cost reduction on combination. - double currCost = 0; - currCost = HistoPriorityListPush(histoPriorityList, maxSize, histograms, idx1, idx2, bestCost, stats, bitsEntropy); + double currCost = HistoPriorityListPush(histoPriorityList, maxSize, histograms, idx1, idx2, bestCost, stats, bitsEntropy); // Found a better pair? if (currCost < 0) From 6f52a0d13c38bceecb9187534996cf21b83f1483 Mon Sep 17 00:00:00 2001 From: Poker Date: Sat, 21 Oct 2023 10:40:50 +0800 Subject: [PATCH 06/24] Preparation --- src/ImageSharp/Formats/Webp/AlphaEncoder.cs | 42 ++++----- .../Formats/Webp/AnimationFrameData.cs | 43 +++++++++ .../Formats/Webp/BitWriter/BitWriterBase.cs | 93 +++++++++++++------ .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 11 +-- .../Formats/Webp/BitWriter/Vp8LBitWriter.cs | 11 ++- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 80 ++++++++-------- .../Formats/Webp/Lossy/Vp8Encoder.cs | 4 +- .../Formats/Webp/Lossy/YuvConversion.cs | 6 +- .../Formats/Webp/WebpAnimationDecoder.cs | 42 +-------- .../Formats/Webp/WebpAnimationEncoder.cs | 12 +++ .../Formats/Webp/WebpChunkParsingUtils.cs | 52 +++++++---- src/ImageSharp/Formats/Webp/WebpChunkType.cs | 9 ++ src/ImageSharp/Formats/Webp/WebpConstants.cs | 33 ------- .../Formats/Webp/WebpDecoderCore.cs | 5 - .../Formats/WebP/YuvConversionTests.cs | 4 +- 15 files changed, 248 insertions(+), 199 deletions(-) create mode 100644 src/ImageSharp/Formats/Webp/WebpAnimationEncoder.cs diff --git a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs index 596715b205..a18d44fde4 100644 --- a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs +++ b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs @@ -19,7 +19,7 @@ internal static class AlphaEncoder /// Data is either compressed as lossless webp image or uncompressed. /// /// The pixel format. - /// The to encode from. + /// The to encode from. /// The global configuration. /// The memory manager. /// Whether to skip metadata encoding. @@ -27,7 +27,7 @@ internal static class AlphaEncoder /// The size in bytes of the alpha data. /// The encoded alpha data. public static IMemoryOwner EncodeAlpha( - Image image, + ImageFrame frame, Configuration configuration, MemoryAllocator memoryAllocator, bool skipMetadata, @@ -35,9 +35,9 @@ internal static class AlphaEncoder out int size) where TPixel : unmanaged, IPixel { - int width = image.Width; - int height = image.Height; - IMemoryOwner alphaData = ExtractAlphaChannel(image, configuration, memoryAllocator); + int width = frame.Width; + int height = frame.Height; + IMemoryOwner alphaData = ExtractAlphaChannel(frame, configuration, memoryAllocator); if (compress) { @@ -58,9 +58,9 @@ internal static class AlphaEncoder // The transparency information will be stored in the green channel of the ARGB quadruplet. // The green channel is allowed extra transformation steps in the specification -- unlike the other channels, // that can improve compression. - using Image alphaAsImage = DispatchAlphaToGreen(image, alphaData.GetSpan()); + using ImageFrame alphaAsFrame = DispatchAlphaToGreen(frame, alphaData.GetSpan()); - size = lossLessEncoder.EncodeAlphaImageData(alphaAsImage, alphaData); + size = lossLessEncoder.EncodeAlphaImageData(alphaAsFrame, alphaData); return alphaData; } @@ -73,45 +73,45 @@ internal static class AlphaEncoder /// Store the transparency in the green channel. /// /// The pixel format. - /// The to encode from. + /// The to encode from. /// A byte sequence of length width * height, containing all the 8-bit transparency values in scan order. - /// The transparency image. - private static Image DispatchAlphaToGreen(Image image, Span alphaData) + /// The transparency frame. + private static ImageFrame DispatchAlphaToGreen(ImageFrame frame, Span alphaData) where TPixel : unmanaged, IPixel { - int width = image.Width; - int height = image.Height; - Image alphaAsImage = new(width, height); + int width = frame.Width; + int height = frame.Height; + ImageFrame alphaAsFrame = new(Configuration.Default, width, height); for (int y = 0; y < height; y++) { - Memory rowBuffer = alphaAsImage.DangerousGetPixelRowMemory(y); + Memory rowBuffer = alphaAsFrame.DangerousGetPixelRowMemory(y); Span pixelRow = rowBuffer.Span; Span alphaRow = alphaData.Slice(y * width, width); for (int x = 0; x < width; x++) { // Leave A/R/B channels zero'd. - pixelRow[x] = new Rgba32(0, alphaRow[x], 0, 0); + pixelRow[x] = new(0, alphaRow[x], 0, 0); } } - return alphaAsImage; + return alphaAsFrame; } /// /// Extract the alpha data of the image. /// /// The pixel format. - /// The to encode from. + /// The to encode from. /// The global configuration. /// The memory manager. /// A byte sequence of length width * height, containing all the 8-bit transparency values in scan order. - private static IMemoryOwner ExtractAlphaChannel(Image image, Configuration configuration, MemoryAllocator memoryAllocator) + private static IMemoryOwner ExtractAlphaChannel(ImageFrame frame, Configuration configuration, MemoryAllocator memoryAllocator) where TPixel : unmanaged, IPixel { - Buffer2D imageBuffer = image.Frames.RootFrame.PixelBuffer; - int height = image.Height; - int width = image.Width; + Buffer2D imageBuffer = frame.PixelBuffer; + int height = frame.Height; + int width = frame.Width; IMemoryOwner alphaDataBuffer = memoryAllocator.Allocate(width * height); Span alphaData = alphaDataBuffer.GetSpan(); diff --git a/src/ImageSharp/Formats/Webp/AnimationFrameData.cs b/src/ImageSharp/Formats/Webp/AnimationFrameData.cs index 714ec428ec..3400fef17d 100644 --- a/src/ImageSharp/Formats/Webp/AnimationFrameData.cs +++ b/src/ImageSharp/Formats/Webp/AnimationFrameData.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.IO; + namespace SixLabors.ImageSharp.Formats.Webp; internal struct AnimationFrameData @@ -10,6 +12,11 @@ internal struct AnimationFrameData /// public uint DataSize; + /// + /// X(3) + Y(3) + Width(3) + Height(3) + Duration(3) + 1 byte for flags. + /// + public const uint HeaderSize = 16; + /// /// The X coordinate of the upper left corner of the frame is Frame X * 2. /// @@ -45,4 +52,40 @@ internal struct AnimationFrameData /// Indicates how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. /// public AnimationDisposalMethod DisposalMethod; + + /// + /// Reads the animation frame header. + /// + /// The stream to read from. + /// Animation frame data. + public static AnimationFrameData Parse(BufferedReadStream stream) + { + Span buffer = stackalloc byte[4]; + + AnimationFrameData data = new() + { + DataSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer), + + // 3 bytes for the X coordinate of the upper left corner of the frame. + X = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer), + + // 3 bytes for the Y coordinate of the upper left corner of the frame. + Y = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer), + + // Frame width Minus One. + Width = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1, + + // Frame height Minus One. + Height = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1, + + // Frame duration. + Duration = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + }; + + byte flags = (byte)stream.ReadByte(); + data.DisposalMethod = (flags & 1) == 1 ? AnimationDisposalMethod.Dispose : AnimationDisposalMethod.DoNotDispose; + data.BlendingMethod = (flags & (1 << 1)) != 0 ? AnimationBlendingMethod.DoNotBlend : AnimationBlendingMethod.AlphaBlending; + + return data; + } } diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index ab78d18604..d7787b3a00 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers.Binary; +using System.Drawing; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; @@ -92,7 +93,7 @@ internal abstract class BitWriterBase { stream.Write(WebpConstants.RiffFourCc); BinaryPrimitives.WriteUInt32LittleEndian(this.scratchBuffer.Span, riffSize); - stream.Write(this.scratchBuffer.Span.Slice(0, 4)); + stream.Write(this.scratchBuffer.Span[..4]); stream.Write(WebpConstants.WebpHeader); } @@ -129,7 +130,7 @@ internal abstract class BitWriterBase DebugGuard.NotNull(metadataBytes, nameof(metadataBytes)); uint size = (uint)metadataBytes.Length; - Span buf = this.scratchBuffer.Span.Slice(0, 4); + Span buf = this.scratchBuffer.Span[..4]; BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)chunkType); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, size); @@ -143,6 +144,61 @@ internal abstract class BitWriterBase } } + /// + /// Writes the color profile() to the stream. + /// + /// The stream to write to. + /// The color profile bytes. + protected void WriteColorProfile(Stream stream, byte[] iccProfileBytes) => this.WriteMetadataProfile(stream, iccProfileBytes, WebpChunkType.Iccp); + + /// + /// Writes the animation parameter() to the stream. + /// + /// The stream to write to. + /// + /// The default background color of the canvas in [Blue, Green, Red, Alpha] byte order. + /// This color MAY be used to fill the unused space on the canvas around the frames, + /// as well as the transparent pixels of the first frame. + /// The background color is also used when the Disposal method is 1. + /// + /// The number of times to loop the animation. If it is 0, this means infinitely. + protected void WriteAnimationParameter(Stream stream, uint background, ushort loopCount) + { + Span buf = this.scratchBuffer.Span[..4]; + BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.AnimationParameter); + stream.Write(buf); + BinaryPrimitives.WriteUInt32LittleEndian(buf, sizeof(uint) + sizeof(ushort)); + stream.Write(buf); + BinaryPrimitives.WriteUInt32LittleEndian(buf, background); + stream.Write(buf); + BinaryPrimitives.WriteUInt16LittleEndian(buf[..2], loopCount); + stream.Write(buf[..2]); + } + + /// + /// Writes the animation frame() to the stream. + /// + /// The stream to write to. + /// Animation frame data. + /// Frame data. + protected void WriteAnimationFrame(Stream stream, AnimationFrameData animation, byte[] data) + { + uint size = AnimationFrameData.HeaderSize + (uint)data.Length; + Span buf = this.scratchBuffer.Span[..4]; + BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Animation); + stream.Write(buf); + BinaryPrimitives.WriteUInt32BigEndian(buf, size); + stream.Write(buf); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.X); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Y); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Width - 1); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Height - 1); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Duration); + byte flag = (byte)(((int)animation.BlendingMethod << 1) | (int)animation.DisposalMethod); + stream.WriteByte(flag); + stream.Write(data); + } + /// /// Writes the alpha chunk to the stream. /// @@ -152,7 +208,7 @@ internal abstract class BitWriterBase protected void WriteAlphaChunk(Stream stream, Span dataBytes, bool alphaDataIsCompressed) { uint size = (uint)dataBytes.Length + 1; - Span buf = this.scratchBuffer.Span.Slice(0, 4); + Span buf = this.scratchBuffer.Span[..4]; BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Alpha); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, size); @@ -161,7 +217,7 @@ internal abstract class BitWriterBase byte flags = 0; if (alphaDataIsCompressed) { - flags |= 1; + flags = 1; } stream.WriteByte(flags); @@ -174,30 +230,6 @@ internal abstract class BitWriterBase } } - /// - /// Writes the color profile to the stream. - /// - /// The stream to write to. - /// The color profile bytes. - protected void WriteColorProfile(Stream stream, byte[] iccProfileBytes) - { - uint size = (uint)iccProfileBytes.Length; - - Span buf = this.scratchBuffer.Span.Slice(0, 4); - BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Iccp); - stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, size); - stream.Write(buf); - - stream.Write(iccProfileBytes); - - // Add padding byte if needed. - if ((size & 1) == 1) - { - stream.WriteByte(0); - } - } - /// /// Writes a VP8X header to the stream. /// @@ -246,8 +278,9 @@ internal abstract class BitWriterBase flags |= 32; } - Span buf = this.scratchBuffer.Span.Slice(0, 4); - stream.Write(WebpConstants.Vp8XMagicBytes); + Span buf = this.scratchBuffer.Span[..4]; + BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Vp8X); + stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, WebpConstants.Vp8XChunkSize); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, flags); diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index 5b4eab64a3..597ecef42a 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -701,12 +701,11 @@ internal class Vp8BitWriter : BitWriterBase private void WriteVp8Header(Stream stream, uint size) { - Span vp8ChunkHeader = stackalloc byte[WebpConstants.ChunkHeaderSize]; - - WebpConstants.Vp8MagicBytes.AsSpan().CopyTo(vp8ChunkHeader); - BinaryPrimitives.WriteUInt32LittleEndian(vp8ChunkHeader[4..], size); - - stream.Write(vp8ChunkHeader); + Span buf = stackalloc byte[WebpConstants.TagSize]; + BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Vp8); + stream.Write(buf); + BinaryPrimitives.WriteUInt32LittleEndian(buf, size); + stream.Write(buf); } private void WriteFrameHeader(Stream stream, uint size0) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs index 9dc7912392..a042f68968 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -105,7 +105,7 @@ internal class Vp8LBitWriter : BitWriterBase { byte[] clonedBuffer = new byte[this.Buffer.Length]; System.Buffer.BlockCopy(this.Buffer, 0, clonedBuffer, 0, this.cur); - return new Vp8LBitWriter(clonedBuffer, this.bits, this.used, this.cur); + return new(clonedBuffer, this.bits, this.used, this.cur); } /// @@ -186,12 +186,13 @@ internal class Vp8LBitWriter : BitWriterBase } // Write magic bytes indicating its a lossless webp. - stream.Write(WebpConstants.Vp8LMagicBytes); + Span scratchBuffer = stackalloc byte[WebpConstants.TagSize]; + BinaryPrimitives.WriteUInt32BigEndian(scratchBuffer, (uint)WebpChunkType.Vp8L); + stream.Write(scratchBuffer); // Write Vp8 Header. - Span scratchBuffer = stackalloc byte[8]; BinaryPrimitives.WriteUInt32LittleEndian(scratchBuffer, size); - stream.Write(scratchBuffer.Slice(0, 4)); + stream.Write(scratchBuffer); stream.WriteByte(WebpConstants.Vp8LHeaderMagicByte); // Write the encoded bytes of the image to the stream. @@ -226,7 +227,7 @@ internal class Vp8LBitWriter : BitWriterBase Span scratchBuffer = stackalloc byte[8]; BinaryPrimitives.WriteUInt64LittleEndian(scratchBuffer, this.bits); - scratchBuffer.Slice(0, 4).CopyTo(this.Buffer.AsSpan(this.cur)); + scratchBuffer[..4].CopyTo(this.Buffer.AsSpan(this.cur)); this.cur += WriterBytes; this.bits >>= WriterBits; diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 469e4c9ab0..9b82cc5983 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -254,7 +254,7 @@ internal class Vp8LEncoder : IDisposable XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; // Convert image pixels to bgra array. - bool hasAlpha = this.ConvertPixelsToBgra(image, width, height); + bool hasAlpha = this.ConvertPixelsToBgra(image.Frames.RootFrame, width, height); // Write the image size. this.WriteImageSize(width, height); @@ -263,7 +263,7 @@ internal class Vp8LEncoder : IDisposable this.WriteAlphaAndVersion(hasAlpha); // Encode the main image stream. - this.EncodeStream(image); + this.EncodeStream(image.Frames.RootFrame); // Write bytes from the bitwriter buffer to the stream. this.bitWriter.WriteEncodedImageToStream(stream, exifProfile, xmpProfile, metadata.IccProfile, (uint)width, (uint)height, hasAlpha); @@ -273,23 +273,23 @@ internal class Vp8LEncoder : IDisposable /// Encodes the alpha image data using the webp lossless compression. /// /// The type of the pixel. - /// The to encode from. + /// The to encode from. /// The destination buffer to write the encoded alpha data to. /// The size of the compressed data in bytes. /// If the size of the data is the same as the pixel count, the compression would not yield in smaller data and is left uncompressed. /// - public int EncodeAlphaImageData(Image image, IMemoryOwner alphaData) + public int EncodeAlphaImageData(ImageFrame frame, IMemoryOwner alphaData) where TPixel : unmanaged, IPixel { - int width = image.Width; - int height = image.Height; + int width = frame.Width; + int height = frame.Height; int pixelCount = width * height; // Convert image pixels to bgra array. - this.ConvertPixelsToBgra(image, width, height); + this.ConvertPixelsToBgra(frame, width, height); // The image-stream will NOT contain any headers describing the image dimension, the dimension is already known. - this.EncodeStream(image); + this.EncodeStream(frame); this.bitWriter.Finish(); int size = this.bitWriter.NumBytes(); if (size >= pixelCount) @@ -333,12 +333,12 @@ internal class Vp8LEncoder : IDisposable /// Encodes the image stream using lossless webp format. /// /// The pixel type. - /// The image to encode. - private void EncodeStream(Image image) + /// The frame to encode. + private void EncodeStream(ImageFrame frame) where TPixel : unmanaged, IPixel { - int width = image.Width; - int height = image.Height; + int width = frame.Width; + int height = frame.Height; Span bgra = this.Bgra.GetSpan(); Span encodedData = this.EncodedData.GetSpan(); @@ -447,14 +447,14 @@ internal class Vp8LEncoder : IDisposable /// Converts the pixels of the image to bgra. /// /// The type of the pixels. - /// The image to convert. + /// The frame to convert. /// The width of the image. /// The height of the image. /// true, if the image is non opaque. - private bool ConvertPixelsToBgra(Image image, int width, int height) + private bool ConvertPixelsToBgra(ImageFrame frame, int width, int height) where TPixel : unmanaged, IPixel { - Buffer2D imageBuffer = image.Frames.RootFrame.PixelBuffer; + Buffer2D imageBuffer = frame.PixelBuffer; bool nonOpaque = false; Span bgra = this.Bgra.GetSpan(); Span bgraBytes = MemoryMarshal.Cast(bgra); @@ -1149,35 +1149,41 @@ internal class Vp8LEncoder : IDisposable entropyComp[j] = bitEntropy.BitsEntropyRefine(); } - entropy[(int)EntropyIx.Direct] = entropyComp[(int)HistoIx.HistoAlpha] + - entropyComp[(int)HistoIx.HistoRed] + - entropyComp[(int)HistoIx.HistoGreen] + - entropyComp[(int)HistoIx.HistoBlue]; - entropy[(int)EntropyIx.Spatial] = entropyComp[(int)HistoIx.HistoAlphaPred] + - entropyComp[(int)HistoIx.HistoRedPred] + - entropyComp[(int)HistoIx.HistoGreenPred] + - entropyComp[(int)HistoIx.HistoBluePred]; - entropy[(int)EntropyIx.SubGreen] = entropyComp[(int)HistoIx.HistoAlpha] + - entropyComp[(int)HistoIx.HistoRedSubGreen] + - entropyComp[(int)HistoIx.HistoGreen] + - entropyComp[(int)HistoIx.HistoBlueSubGreen]; - entropy[(int)EntropyIx.SpatialSubGreen] = entropyComp[(int)HistoIx.HistoAlphaPred] + - entropyComp[(int)HistoIx.HistoRedPredSubGreen] + - entropyComp[(int)HistoIx.HistoGreenPred] + - entropyComp[(int)HistoIx.HistoBluePredSubGreen]; + entropy[(int)EntropyIx.Direct] = + entropyComp[(int)HistoIx.HistoAlpha] + + entropyComp[(int)HistoIx.HistoRed] + + entropyComp[(int)HistoIx.HistoGreen] + + entropyComp[(int)HistoIx.HistoBlue]; + entropy[(int)EntropyIx.Spatial] = + entropyComp[(int)HistoIx.HistoAlphaPred] + + entropyComp[(int)HistoIx.HistoRedPred] + + entropyComp[(int)HistoIx.HistoGreenPred] + + entropyComp[(int)HistoIx.HistoBluePred]; + entropy[(int)EntropyIx.SubGreen] = + entropyComp[(int)HistoIx.HistoAlpha] + + entropyComp[(int)HistoIx.HistoRedSubGreen] + + entropyComp[(int)HistoIx.HistoGreen] + + entropyComp[(int)HistoIx.HistoBlueSubGreen]; + entropy[(int)EntropyIx.SpatialSubGreen] = + entropyComp[(int)HistoIx.HistoAlphaPred] + + entropyComp[(int)HistoIx.HistoRedPredSubGreen] + + entropyComp[(int)HistoIx.HistoGreenPred] + + entropyComp[(int)HistoIx.HistoBluePredSubGreen]; entropy[(int)EntropyIx.Palette] = entropyComp[(int)HistoIx.HistoPalette]; // When including transforms, there is an overhead in bits from // storing them. This overhead is small but matters for small images. // For spatial, there are 14 transformations. - entropy[(int)EntropyIx.Spatial] += LosslessUtils.SubSampleSize(width, transformBits) * - LosslessUtils.SubSampleSize(height, transformBits) * - LosslessUtils.FastLog2(14); + entropy[(int)EntropyIx.Spatial] += + LosslessUtils.SubSampleSize(width, transformBits) * + LosslessUtils.SubSampleSize(height, transformBits) * + LosslessUtils.FastLog2(14); // For color transforms: 24 as only 3 channels are considered in a ColorTransformElement. - entropy[(int)EntropyIx.SpatialSubGreen] += LosslessUtils.SubSampleSize(width, transformBits) * - LosslessUtils.SubSampleSize(height, transformBits) * - LosslessUtils.FastLog2(24); + entropy[(int)EntropyIx.SpatialSubGreen] += + LosslessUtils.SubSampleSize(width, transformBits) * + LosslessUtils.SubSampleSize(height, transformBits) * + LosslessUtils.FastLog2(24); // For palettes, add the cost of storing the palette. // We empirically estimate the cost of a compressed entry as 8 bits. diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index f17d965e87..ce5d3bac11 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -323,7 +323,7 @@ internal class Vp8Encoder : IDisposable Span y = this.Y.GetSpan(); Span u = this.U.GetSpan(); Span v = this.V.GetSpan(); - bool hasAlpha = YuvConversion.ConvertRgbToYuv(image, this.configuration, this.memoryAllocator, y, u, v); + bool hasAlpha = YuvConversion.ConvertRgbToYuv(image.Frames.RootFrame, this.configuration, this.memoryAllocator, y, u, v); int yStride = width; int uvStride = (yStride + 1) >> 1; @@ -393,7 +393,7 @@ internal class Vp8Encoder : IDisposable { // TODO: This can potentially run in an separate task. encodedAlphaData = AlphaEncoder.EncodeAlpha( - image, + image.Frames.RootFrame, this.configuration, this.memoryAllocator, this.skipMetadata, diff --git a/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs b/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs index 8ef7fe9cba..d669a37b74 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs @@ -262,17 +262,17 @@ internal static class YuvConversion /// Converts the RGB values of the image to YUV. /// /// The pixel type of the image. - /// The image to convert. + /// The frame to convert. /// The global configuration. /// The memory allocator. /// Span to store the luma component of the image. /// Span to store the u component of the image. /// Span to store the v component of the image. /// true, if the image contains alpha data. - public static bool ConvertRgbToYuv(Image image, Configuration configuration, MemoryAllocator memoryAllocator, Span y, Span u, Span v) + public static bool ConvertRgbToYuv(ImageFrame frame, Configuration configuration, MemoryAllocator memoryAllocator, Span y, Span u, Span v) where TPixel : unmanaged, IPixel { - Buffer2D imageBuffer = image.Frames.RootFrame.PixelBuffer; + Buffer2D imageBuffer = frame.PixelBuffer; int width = imageBuffer.Width; int height = imageBuffer.Height; int uvWidth = (width + 1) >> 1; diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 90c9c70b26..65f654dddc 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -138,7 +138,7 @@ internal class WebpAnimationDecoder : IDisposable private uint ReadFrame(BufferedReadStream stream, ref Image? image, ref ImageFrame? previousFrame, uint width, uint height, Color backgroundColor) where TPixel : unmanaged, IPixel { - AnimationFrameData frameData = this.ReadFrameHeader(stream); + AnimationFrameData frameData = AnimationFrameData.Parse(stream); long streamStartPosition = stream.Position; Span buffer = stackalloc byte[4]; @@ -173,7 +173,7 @@ internal class WebpAnimationDecoder : IDisposable ImageFrame imageFrame; if (previousFrame is null) { - image = new Image(this.configuration, (int)width, (int)height, backgroundColor.ToPixel(), this.metadata); + image = new(this.configuration, (int)width, (int)height, backgroundColor.ToPixel(), this.metadata); SetFrameMetadata(image.Frames.RootFrame.Metadata, frameData.Duration); @@ -258,7 +258,7 @@ internal class WebpAnimationDecoder : IDisposable try { - Buffer2D pixelBufferDecoded = decodedImage.Frames.RootFrame.PixelBuffer; + Buffer2D pixelBufferDecoded = decodedImage.GetRootFramePixelBuffer(); if (webpInfo.IsLossless) { WebpLosslessDecoder losslessDecoder = new(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); @@ -353,42 +353,6 @@ internal class WebpAnimationDecoder : IDisposable pixelRegion.Fill(backgroundPixel); } - /// - /// Reads the animation frame header. - /// - /// The stream to read from. - /// Animation frame data. - private AnimationFrameData ReadFrameHeader(BufferedReadStream stream) - { - Span buffer = stackalloc byte[4]; - - AnimationFrameData data = new() - { - DataSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer), - - // 3 bytes for the X coordinate of the upper left corner of the frame. - X = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer), - - // 3 bytes for the Y coordinate of the upper left corner of the frame. - Y = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer), - - // Frame width Minus One. - Width = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer) + 1, - - // Frame height Minus One. - Height = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer) + 1, - - // Frame duration. - Duration = WebpChunkParsingUtils.ReadUnsignedInt24Bit(stream, buffer) - }; - - byte flags = (byte)stream.ReadByte(); - data.DisposalMethod = (flags & 1) == 1 ? AnimationDisposalMethod.Dispose : AnimationDisposalMethod.DoNotDispose; - data.BlendingMethod = (flags & (1 << 1)) != 0 ? AnimationBlendingMethod.DoNotBlend : AnimationBlendingMethod.AlphaBlending; - - return data; - } - /// public void Dispose() => this.alphaData?.Dispose(); } diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationEncoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationEncoder.cs new file mode 100644 index 0000000000..bfa64b6797 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/WebpAnimationEncoder.cs @@ -0,0 +1,12 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp; + +/// +/// Encoder for animated webp images. +/// +public class WebpAnimationEncoder +{ + // 可能不需要这个屌东西 +} diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs index a7ae474e46..becd622e17 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs @@ -2,13 +2,12 @@ // Licensed under the Six Labors Split License. using System.Buffers.Binary; +using System.Drawing; using SixLabors.ImageSharp.Formats.Webp.BitReader; using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; -using SixLabors.ImageSharp.Metadata.Profiles.Exif; -using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Webp; @@ -91,7 +90,7 @@ internal static class WebpChunkParsingUtils uint tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer); uint width = tmp & 0x3fff; sbyte xScale = (sbyte)(tmp >> 6); - tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(2)); + tmp = BinaryPrimitives.ReadUInt16LittleEndian(buffer[2..]); uint height = tmp & 0x3fff; sbyte yScale = (sbyte)(tmp >> 6); remaining -= 7; @@ -105,14 +104,14 @@ internal static class WebpChunkParsingUtils WebpThrowHelper.ThrowImageFormatException("bad partition length"); } - var vp8FrameHeader = new Vp8FrameHeader() + Vp8FrameHeader vp8FrameHeader = new() { KeyFrame = true, Profile = (sbyte)version, PartitionLength = partitionLength }; - var bitReader = new Vp8BitReader( + Vp8BitReader bitReader = new( stream, remaining, memoryAllocator, @@ -121,7 +120,7 @@ internal static class WebpChunkParsingUtils Remaining = remaining }; - return new WebpImageInfo() + return new() { Width = width, Height = height, @@ -145,7 +144,7 @@ internal static class WebpChunkParsingUtils // VP8 data size. uint imageDataSize = ReadChunkSize(stream, buffer); - var bitReader = new Vp8LBitReader(stream, imageDataSize, memoryAllocator); + Vp8LBitReader bitReader = new(stream, imageDataSize, memoryAllocator); // One byte signature, should be 0x2f. uint signature = bitReader.ReadValue(8); @@ -174,7 +173,7 @@ internal static class WebpChunkParsingUtils WebpThrowHelper.ThrowNotSupportedException($"Unexpected version number {version} found in VP8L header"); } - return new WebpImageInfo() + return new() { Width = width, Height = height, @@ -231,13 +230,13 @@ internal static class WebpChunkParsingUtils } // 3 bytes for the width. - uint width = ReadUnsignedInt24Bit(stream, buffer) + 1; + uint width = ReadUInt24LittleEndian(stream, buffer) + 1; // 3 bytes for the height. - uint height = ReadUnsignedInt24Bit(stream, buffer) + 1; + uint height = ReadUInt24LittleEndian(stream, buffer) + 1; // Read all the chunks in the order they occur. - var info = new WebpImageInfo() + WebpImageInfo info = new() { Width = width, Height = height, @@ -253,7 +252,7 @@ internal static class WebpChunkParsingUtils /// The stream to read from. /// The buffer to store the read data into. /// A unsigned 24 bit integer. - public static uint ReadUnsignedInt24Bit(BufferedReadStream stream, Span buffer) + public static uint ReadUInt24LittleEndian(BufferedReadStream stream, Span buffer) { if (stream.Read(buffer, 0, 3) == 3) { @@ -261,7 +260,28 @@ internal static class WebpChunkParsingUtils return BinaryPrimitives.ReadUInt32LittleEndian(buffer); } - throw new ImageFormatException("Invalid Webp data, could not read unsigned integer."); + throw new ImageFormatException("Invalid Webp data, could not read unsigned 24 bit integer."); + } + + /// + /// Writes a unsigned 24 bit integer. + /// + /// The stream to read from. + /// The uint24 data to write. + public static unsafe void WriteUInt24LittleEndian(Stream stream, uint data) + { + if (data >= 1 << 24) + { + throw new InvalidDataException($"Invalid data, {data} is not a unsigned 24 bit integer."); + } + + uint* ptr = &data; + byte* b = (byte*)ptr; + + // Write the data in little endian. + stream.WriteByte(b[0]); + stream.WriteByte(b[1]); + stream.WriteByte(b[2]); } /// @@ -298,7 +318,7 @@ internal static class WebpChunkParsingUtils if (stream.Read(buffer) == 4) { - var chunkType = (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer); + WebpChunkType chunkType = (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer); return chunkType; } @@ -335,7 +355,7 @@ internal static class WebpChunkParsingUtils if (metadata.ExifProfile != null) { - metadata.ExifProfile = new ExifProfile(exifData); + metadata.ExifProfile = new(exifData); } break; @@ -349,7 +369,7 @@ internal static class WebpChunkParsingUtils if (metadata.XmpProfile != null) { - metadata.XmpProfile = new XmpProfile(xmpData); + metadata.XmpProfile = new(xmpData); } break; diff --git a/src/ImageSharp/Formats/Webp/WebpChunkType.cs b/src/ImageSharp/Formats/Webp/WebpChunkType.cs index 802d7f7288..5836dc6c09 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkType.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkType.cs @@ -12,45 +12,54 @@ internal enum WebpChunkType : uint /// /// Header signaling the use of the VP8 format. /// + /// VP8 (Single) Vp8 = 0x56503820U, /// /// Header signaling the image uses lossless encoding. /// + /// VP8L (Single) Vp8L = 0x5650384CU, /// /// Header for a extended-VP8 chunk. /// + /// VP8X (Single) Vp8X = 0x56503858U, /// /// Chunk contains information about the alpha channel. /// + /// ALPH (Single) Alpha = 0x414C5048U, /// /// Chunk which contains a color profile. /// + /// ICCP (Single) Iccp = 0x49434350U, /// /// Chunk which contains EXIF metadata about the image. /// + /// EXIF (Single) Exif = 0x45584946U, /// /// Chunk contains XMP metadata about the image. /// + /// XMP (Single) Xmp = 0x584D5020U, /// /// For an animated image, this chunk contains the global parameters of the animation. /// + /// ANIM (Single) AnimationParameter = 0x414E494D, /// /// For animated images, this chunk contains information about a single frame. If the Animation flag is not set, then this chunk SHOULD NOT be present. /// + /// ANMF (Multiple) Animation = 0x414E4D46, } diff --git a/src/ImageSharp/Formats/Webp/WebpConstants.cs b/src/ImageSharp/Formats/Webp/WebpConstants.cs index d105d8dd62..1433772757 100644 --- a/src/ImageSharp/Formats/Webp/WebpConstants.cs +++ b/src/ImageSharp/Formats/Webp/WebpConstants.cs @@ -33,39 +33,6 @@ internal static class WebpConstants /// public const byte Vp8LHeaderMagicByte = 0x2F; - /// - /// Signature bytes identifying a lossy image. - /// - public static readonly byte[] Vp8MagicBytes = - { - 0x56, // V - 0x50, // P - 0x38, // 8 - 0x20 // ' ' - }; - - /// - /// Signature bytes identifying a lossless image. - /// - public static readonly byte[] Vp8LMagicBytes = - { - 0x56, // V - 0x50, // P - 0x38, // 8 - 0x4C // L - }; - - /// - /// Signature bytes identifying a VP8X header. - /// - public static readonly byte[] Vp8XMagicBytes = - { - 0x56, // V - 0x50, // P - 0x38, // 8 - 0x58 // X - }; - /// /// The header bytes identifying RIFF file. /// diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 8832ac1068..63d3e1aead 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -93,11 +93,6 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable return animationDecoder.Decode(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize); } - if (this.webImageInfo.Features is { Animation: true }) - { - WebpThrowHelper.ThrowNotSupportedException("Animations are not supported"); - } - image = new Image(this.configuration, (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, metadata); Buffer2D pixels = image.GetRootFramePixelBuffer(); if (this.webImageInfo.IsLossless) diff --git a/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs b/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs index 9b03a447a9..433b280bc3 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs @@ -143,7 +143,7 @@ public class YuvConversionTests }; // act - YuvConversion.ConvertRgbToYuv(image, config, memoryAllocator, y, u, v); + YuvConversion.ConvertRgbToYuv(image.Frames.RootFrame, config, memoryAllocator, y, u, v); // assert Assert.True(expectedY.AsSpan().SequenceEqual(y)); @@ -249,7 +249,7 @@ public class YuvConversionTests }; // act - YuvConversion.ConvertRgbToYuv(image, config, memoryAllocator, y, u, v); + YuvConversion.ConvertRgbToYuv(image.Frames.RootFrame, config, memoryAllocator, y, u, v); // assert Assert.True(expectedY.AsSpan().SequenceEqual(y)); From 62ab3a1eef3c9e9af4af683f512471f81bf3e5d4 Mon Sep 17 00:00:00 2001 From: Poker Date: Sun, 22 Oct 2023 22:54:33 +0800 Subject: [PATCH 07/24] refactor --- .../Formats/Webp/BitWriter/BitWriterBase.cs | 11 +- .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 104 +++++++++--------- .../Formats/Webp/Lossy/Vp8EncIterator.cs | 5 + .../Formats/Webp/Lossy/Vp8Encoder.cs | 4 +- .../Formats/Webp/WebpAnimationDecoder.cs | 5 + 5 files changed, 73 insertions(+), 56 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index d7787b3a00..4252f895b8 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers.Binary; -using System.Drawing; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; @@ -181,7 +180,7 @@ internal abstract class BitWriterBase /// The stream to write to. /// Animation frame data. /// Frame data. - protected void WriteAnimationFrame(Stream stream, AnimationFrameData animation, byte[] data) + protected void WriteAnimationFrame(Stream stream, AnimationFrameData animation, Span data) { uint size = AnimationFrameData.HeaderSize + (uint)data.Length; Span buf = this.scratchBuffer.Span[..4]; @@ -260,6 +259,14 @@ internal abstract class BitWriterBase flags |= 8; } + /* + if (isAnimated) + { + // Set animated flag. + flags |= 2; + } + */ + if (xmpProfile != null) { // Set xmp bit. diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index 597ecef42a..cd84f109eb 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -116,7 +116,7 @@ internal class Vp8BitWriter : BitWriterBase else { this.PutBit(v >= 9, 165); - this.PutBit(!((v & 1) != 0), 145); + this.PutBit((v & 1) == 0, 145); } } else @@ -462,7 +462,7 @@ internal class Vp8BitWriter : BitWriterBase Vp8BitWriter bitWriterPartZero = new(expectedSize, this.enc); // Partition #0 with header and partition sizes. - uint size0 = this.GeneratePartition0(bitWriterPartZero); + uint size0 = bitWriterPartZero.GeneratePartition0(); uint vp8Size = WebpConstants.Vp8FrameHeaderSize + size0; vp8Size += numBytes; @@ -495,47 +495,47 @@ internal class Vp8BitWriter : BitWriterBase } } - private uint GeneratePartition0(Vp8BitWriter bitWriter) + private uint GeneratePartition0() { - bitWriter.PutBitUniform(0); // colorspace - bitWriter.PutBitUniform(0); // clamp type + this.PutBitUniform(0); // colorspace + this.PutBitUniform(0); // clamp type - this.WriteSegmentHeader(bitWriter); - this.WriteFilterHeader(bitWriter); + this.WriteSegmentHeader(); + this.WriteFilterHeader(); - bitWriter.PutBits(0, 2); + this.PutBits(0, 2); - this.WriteQuant(bitWriter); - bitWriter.PutBitUniform(0); - this.WriteProbas(bitWriter); - this.CodeIntraModes(bitWriter); + this.WriteQuant(); + this.PutBitUniform(0); + this.WriteProbas(); + this.CodeIntraModes(); - bitWriter.Finish(); + this.Finish(); - return (uint)bitWriter.NumBytes(); + return (uint)this.NumBytes(); } - private void WriteSegmentHeader(Vp8BitWriter bitWriter) + private void WriteSegmentHeader() { Vp8EncSegmentHeader hdr = this.enc.SegmentHeader; Vp8EncProba proba = this.enc.Proba; - if (bitWriter.PutBitUniform(hdr.NumSegments > 1 ? 1 : 0) != 0) + if (this.PutBitUniform(hdr.NumSegments > 1 ? 1 : 0) != 0) { // We always 'update' the quant and filter strength values. int updateData = 1; - bitWriter.PutBitUniform(hdr.UpdateMap ? 1 : 0); - if (bitWriter.PutBitUniform(updateData) != 0) + this.PutBitUniform(hdr.UpdateMap ? 1 : 0); + if (this.PutBitUniform(updateData) != 0) { // We always use absolute values, not relative ones. - bitWriter.PutBitUniform(1); // (segment_feature_mode = 1. Paragraph 9.3.) + this.PutBitUniform(1); // (segment_feature_mode = 1. Paragraph 9.3.) for (int s = 0; s < WebpConstants.NumMbSegments; ++s) { - bitWriter.PutSignedBits(this.enc.SegmentInfos[s].Quant, 7); + this.PutSignedBits(this.enc.SegmentInfos[s].Quant, 7); } for (int s = 0; s < WebpConstants.NumMbSegments; ++s) { - bitWriter.PutSignedBits(this.enc.SegmentInfos[s].FStrength, 6); + this.PutSignedBits(this.enc.SegmentInfos[s].FStrength, 6); } } @@ -543,50 +543,50 @@ internal class Vp8BitWriter : BitWriterBase { for (int s = 0; s < 3; ++s) { - if (bitWriter.PutBitUniform(proba.Segments[s] != 255 ? 1 : 0) != 0) + if (this.PutBitUniform(proba.Segments[s] != 255 ? 1 : 0) != 0) { - bitWriter.PutBits(proba.Segments[s], 8); + this.PutBits(proba.Segments[s], 8); } } } } } - private void WriteFilterHeader(Vp8BitWriter bitWriter) + private void WriteFilterHeader() { Vp8FilterHeader hdr = this.enc.FilterHeader; bool useLfDelta = hdr.I4x4LfDelta != 0; - bitWriter.PutBitUniform(hdr.Simple ? 1 : 0); - bitWriter.PutBits((uint)hdr.FilterLevel, 6); - bitWriter.PutBits((uint)hdr.Sharpness, 3); - if (bitWriter.PutBitUniform(useLfDelta ? 1 : 0) != 0) + this.PutBitUniform(hdr.Simple ? 1 : 0); + this.PutBits((uint)hdr.FilterLevel, 6); + this.PutBits((uint)hdr.Sharpness, 3); + if (this.PutBitUniform(useLfDelta ? 1 : 0) != 0) { // '0' is the default value for i4x4LfDelta at frame #0. bool needUpdate = hdr.I4x4LfDelta != 0; - if (bitWriter.PutBitUniform(needUpdate ? 1 : 0) != 0) + if (this.PutBitUniform(needUpdate ? 1 : 0) != 0) { // we don't use refLfDelta => emit four 0 bits. - bitWriter.PutBits(0, 4); + this.PutBits(0, 4); // we use modeLfDelta for i4x4 - bitWriter.PutSignedBits(hdr.I4x4LfDelta, 6); - bitWriter.PutBits(0, 3); // all others unused. + this.PutSignedBits(hdr.I4x4LfDelta, 6); + this.PutBits(0, 3); // all others unused. } } } // Nominal quantization parameters - private void WriteQuant(Vp8BitWriter bitWriter) + private void WriteQuant() { - bitWriter.PutBits((uint)this.enc.BaseQuant, 7); - bitWriter.PutSignedBits(this.enc.DqY1Dc, 4); - bitWriter.PutSignedBits(this.enc.DqY2Dc, 4); - bitWriter.PutSignedBits(this.enc.DqY2Ac, 4); - bitWriter.PutSignedBits(this.enc.DqUvDc, 4); - bitWriter.PutSignedBits(this.enc.DqUvAc, 4); + this.PutBits((uint)this.enc.BaseQuant, 7); + this.PutSignedBits(this.enc.DqY1Dc, 4); + this.PutSignedBits(this.enc.DqY2Dc, 4); + this.PutSignedBits(this.enc.DqY2Ac, 4); + this.PutSignedBits(this.enc.DqUvDc, 4); + this.PutSignedBits(this.enc.DqUvAc, 4); } - private void WriteProbas(Vp8BitWriter bitWriter) + private void WriteProbas() { Vp8EncProba probas = this.enc.Proba; for (int t = 0; t < WebpConstants.NumTypes; ++t) @@ -599,25 +599,25 @@ internal class Vp8BitWriter : BitWriterBase { byte p0 = probas.Coeffs[t][b].Probabilities[c].Probabilities[p]; bool update = p0 != WebpLookupTables.DefaultCoeffsProba[t, b, c, p]; - if (bitWriter.PutBit(update, WebpLookupTables.CoeffsUpdateProba[t, b, c, p])) + if (this.PutBit(update, WebpLookupTables.CoeffsUpdateProba[t, b, c, p])) { - bitWriter.PutBits(p0, 8); + this.PutBits(p0, 8); } } } } } - if (bitWriter.PutBitUniform(probas.UseSkipProba ? 1 : 0) != 0) + if (this.PutBitUniform(probas.UseSkipProba ? 1 : 0) != 0) { - bitWriter.PutBits(probas.SkipProba, 8); + this.PutBits(probas.SkipProba, 8); } } // Writes the partition #0 modes (that is: all intra modes) - private void CodeIntraModes(Vp8BitWriter bitWriter) + private void CodeIntraModes() { - var it = new Vp8EncIterator(this.enc.YTop, this.enc.UvTop, this.enc.Nz, this.enc.MbInfo, this.enc.Preds, this.enc.TopDerr, this.enc.Mbw, this.enc.Mbh); + Vp8EncIterator it = new(this.enc); int predsWidth = this.enc.PredsWidth; do @@ -627,18 +627,18 @@ internal class Vp8BitWriter : BitWriterBase Span preds = it.Preds.AsSpan(predIdx); if (this.enc.SegmentHeader.UpdateMap) { - bitWriter.PutSegment(mb.Segment, this.enc.Proba.Segments); + this.PutSegment(mb.Segment, this.enc.Proba.Segments); } if (this.enc.Proba.UseSkipProba) { - bitWriter.PutBit(mb.Skip, this.enc.Proba.SkipProba); + this.PutBit(mb.Skip, this.enc.Proba.SkipProba); } - if (bitWriter.PutBit(mb.MacroBlockType != 0, 145)) + if (this.PutBit(mb.MacroBlockType != 0, 145)) { // i16x16 - bitWriter.PutI16Mode(preds[0]); + this.PutI16Mode(preds[0]); } else { @@ -649,7 +649,7 @@ internal class Vp8BitWriter : BitWriterBase for (int x = 0; x < 4; x++) { byte[] probas = WebpLookupTables.ModesProba[topPred[x], left]; - left = bitWriter.PutI4Mode(it.Preds[predIdx + x], probas); + left = this.PutI4Mode(it.Preds[predIdx + x], probas); } topPred = it.Preds.AsSpan(predIdx); @@ -657,7 +657,7 @@ internal class Vp8BitWriter : BitWriterBase } } - bitWriter.PutUvMode(mb.UvMode); + this.PutUvMode(mb.UvMode); } while (it.Next()); } diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs index 7211f93766..a7c96edb7c 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs @@ -50,6 +50,11 @@ internal class Vp8EncIterator private int uvTopIdx; + public Vp8EncIterator(Vp8Encoder enc) + : this(enc.YTop, enc.UvTop, enc.Nz, enc.MbInfo, enc.Preds, enc.TopDerr, enc.Mbw, enc.Mbh) + { + } + public Vp8EncIterator(byte[] yTop, byte[] uvTop, uint[] nz, Vp8MacroBlockInfo[] mb, byte[] preds, sbyte[] topDerr, int mbw, int mbh) { this.YTop = yTop; diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index ce5d3bac11..c65099af88 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -328,7 +328,7 @@ internal class Vp8Encoder : IDisposable int yStride = width; int uvStride = (yStride + 1) >> 1; - Vp8EncIterator it = new(this.YTop, this.UvTop, this.Nz, this.MbInfo, this.Preds, this.TopDerr, this.Mbw, this.Mbh); + Vp8EncIterator it = new(this); Span alphas = stackalloc int[WebpConstants.MaxAlpha + 1]; this.alpha = this.MacroBlockAnalysis(width, height, it, y, u, v, yStride, uvStride, alphas, out this.uvAlpha); int totalMb = this.Mbw * this.Mbw; @@ -520,7 +520,7 @@ internal class Vp8Encoder : IDisposable Span y = this.Y.GetSpan(); Span u = this.U.GetSpan(); Span v = this.V.GetSpan(); - Vp8EncIterator it = new(this.YTop, this.UvTop, this.Nz, this.MbInfo, this.Preds, this.TopDerr, this.Mbw, this.Mbh); + Vp8EncIterator it = new(this); long size = 0; long sizeP0 = 0; long distortion = 0; diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 65f654dddc..81a7aebdf9 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -162,6 +162,11 @@ internal class WebpAnimationDecoder : IDisposable features.AlphaChunkHeader = alphaChunkHeader; break; case WebpChunkType.Vp8L: + if (hasAlpha) + { + WebpThrowHelper.ThrowNotSupportedException("Alpha channel is not supported for lossless webp images."); + } + webpInfo = WebpChunkParsingUtils.ReadVp8LHeader(this.memoryAllocator, stream, buffer, features); break; default: From 95d36af396d5f57a9d79ca1e3a158c9b18e95a4a Mon Sep 17 00:00:00 2001 From: Poker Date: Mon, 23 Oct 2023 19:50:28 +0800 Subject: [PATCH 08/24] (Vp8) Write total size after writing. Separate the writes of each block --- .../Formats/Webp/BitWriter/BitWriterBase.cs | 47 ++++-- .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 135 +++++++----------- .../Formats/Webp/BitWriter/Vp8LBitWriter.cs | 10 +- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 8 +- .../Formats/Webp/Lossy/Vp8Encoder.cs | 27 ++-- .../Formats/Webp/WebpChunkParsingUtils.cs | 11 +- .../Metadata/Profiles/ICC/IccProfile.cs | 4 +- 7 files changed, 116 insertions(+), 126 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index 4252f895b8..b82b764fc3 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -4,6 +4,7 @@ using System.Buffers.Binary; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Webp.BitWriter; @@ -41,17 +42,23 @@ internal abstract class BitWriterBase public byte[] Buffer => this.buffer; + /// + /// Gets the number of bytes of the encoded image data. + /// + /// The number of bytes of the image data. + public abstract int NumBytes { get; } + /// /// Writes the encoded bytes of the image to the stream. Call Finish() before this. /// /// The stream to write to. - public void WriteToStream(Stream stream) => stream.Write(this.Buffer.AsSpan(0, this.NumBytes())); + public void WriteToStream(Stream stream) => stream.Write(this.Buffer.AsSpan(0, this.NumBytes)); /// /// Writes the encoded bytes of the image to the given buffer. Call Finish() before this. /// /// The destination buffer. - public void WriteToBuffer(Span dest) => this.Buffer.AsSpan(0, this.NumBytes()).CopyTo(dest); + public void WriteToBuffer(Span dest) => this.Buffer.AsSpan(0, this.NumBytes).CopyTo(dest); /// /// Resizes the buffer to write to. @@ -59,12 +66,6 @@ internal abstract class BitWriterBase /// The extra size in bytes needed. public abstract void BitWriterResize(int extraSize); - /// - /// Returns the number of bytes of the encoded image data. - /// - /// The number of bytes of the image data. - public abstract int NumBytes(); - /// /// Flush leftover bits. /// @@ -86,6 +87,7 @@ internal abstract class BitWriterBase /// /// Writes the RIFF header to the stream. /// + /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The block length. protected void WriteRiffHeader(Stream stream, uint riffSize) @@ -99,6 +101,7 @@ internal abstract class BitWriterBase /// /// Calculates the chunk size of EXIF, XMP or ICCP metadata. /// + /// Think of it as a static method — none of the other members are called except for /// The metadata profile bytes. /// The metadata chunk size in bytes. protected static uint MetadataChunkSize(byte[] metadataBytes) @@ -118,9 +121,26 @@ internal abstract class BitWriterBase return WebpConstants.ChunkHeaderSize + alphaSize + (alphaSize & 1); } + /// + /// Overwrites ides the write file size. + /// + /// The stream to write to. + protected static void OverwriteFileSize(Stream stream) + { + uint position = (uint)stream.Position; + stream.Position = 4; + byte[] buffer = new byte[4]; + + // "RIFF"(4)+uint32 size(4) + BinaryPrimitives.WriteUInt32LittleEndian(buffer, position - WebpConstants.ChunkHeaderSize); + stream.Write(buffer); + stream.Position = position; + } + /// /// Writes a metadata profile (EXIF or XMP) to the stream. /// + /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The metadata profile's bytes. /// The chuck type to write. @@ -146,6 +166,7 @@ internal abstract class BitWriterBase /// /// Writes the color profile() to the stream. /// + /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The color profile bytes. protected void WriteColorProfile(Stream stream, byte[] iccProfileBytes) => this.WriteMetadataProfile(stream, iccProfileBytes, WebpChunkType.Iccp); @@ -153,6 +174,7 @@ internal abstract class BitWriterBase /// /// Writes the animation parameter() to the stream. /// + /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// /// The default background color of the canvas in [Blue, Green, Red, Alpha] byte order. @@ -177,6 +199,7 @@ internal abstract class BitWriterBase /// /// Writes the animation frame() to the stream. /// + /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// Animation frame data. /// Frame data. @@ -201,6 +224,7 @@ internal abstract class BitWriterBase /// /// Writes the alpha chunk to the stream. /// + /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The alpha channel data bytes. /// Indicates, if the alpha channel data is compressed. @@ -232,14 +256,15 @@ internal abstract class BitWriterBase /// /// Writes a VP8X header to the stream. /// + /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// A exif profile or null, if it does not exist. /// A XMP profile or null, if it does not exist. - /// The color profile bytes. + /// The color profile. /// The width of the image. /// The height of the image. /// Flag indicating, if a alpha channel is present. - protected void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, byte[]? iccProfileBytes, uint width, uint height, bool hasAlpha) + protected void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, uint width, uint height, bool hasAlpha) { if (width > MaxDimension || height > MaxDimension) { @@ -279,7 +304,7 @@ internal abstract class BitWriterBase flags |= 16; } - if (iccProfileBytes != null) + if (iccProfile != null) { // Set iccp flag. flags |= 32; diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index cd84f109eb..5dd5d335de 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -72,7 +72,7 @@ internal class Vp8BitWriter : BitWriterBase } /// - public override int NumBytes() => (int)this.pos; + public override int NumBytes => (int)this.pos; public int PutCoeffs(int ctx, Vp8Residual residual) { @@ -395,67 +395,58 @@ internal class Vp8BitWriter : BitWriterBase } /// - /// Writes the encoded image to the stream. + /// Write the trunks before data trunk. /// + /// Think of it as a static method — none of the other members are called except for /// The stream to write to. + /// The width of the image. + /// The height of the image. /// The exif profile. /// The XMP profile. /// The color profile. - /// The width of the image. - /// The height of the image. /// Flag indicating, if a alpha channel is present. /// The alpha channel data. /// Indicates, if the alpha data is compressed. - public void WriteEncodedImageToStream( + public void WriteTrunksBeforeData( Stream stream, + uint width, + uint height, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, - uint width, - uint height, bool hasAlpha, Span alphaData, bool alphaDataIsCompressed) { - bool isVp8X = false; - byte[]? exifBytes = null; - byte[]? xmpBytes = null; - byte[]? iccProfileBytes = null; - uint riffSize = 0; - if (exifProfile != null) - { - isVp8X = true; - exifBytes = exifProfile.ToByteArray(); - riffSize += MetadataChunkSize(exifBytes!); - } + // Write file size later + this.WriteRiffHeader(stream, 0); - if (xmpProfile != null) + // Write VP8X, header if necessary. + bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha; + if (isVp8X) { - isVp8X = true; - xmpBytes = xmpProfile.Data; - riffSize += MetadataChunkSize(xmpBytes!); - } + this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha); - if (iccProfile != null) - { - isVp8X = true; - iccProfileBytes = iccProfile.ToByteArray(); - riffSize += MetadataChunkSize(iccProfileBytes); - } + if (iccProfile != null) + { + this.WriteColorProfile(stream, iccProfile.ToByteArray()); + } - if (hasAlpha) - { - isVp8X = true; - riffSize += AlphaChunkSize(alphaData); + if (hasAlpha) + { + this.WriteAlphaChunk(stream, alphaData, alphaDataIsCompressed); + } } + } - if (isVp8X) - { - riffSize += ExtendedFileChunkSize; - } + /// + /// Writes the encoded image to the stream. + /// + /// The stream to write to. + public void WriteEncodedImageToStream(Stream stream) + { + uint numBytes = (uint)this.NumBytes; - this.Finish(); - uint numBytes = (uint)this.NumBytes(); int mbSize = this.enc.Mbw * this.enc.Mbh; int expectedSize = (int)((uint)mbSize * 7 / 8); @@ -469,12 +460,10 @@ internal class Vp8BitWriter : BitWriterBase uint pad = vp8Size & 1; vp8Size += pad; - // Compute RIFF size. - // At the minimum it is: "WEBPVP8 nnnn" + VP8 data size. - riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + vp8Size; + // Emit header and partition #0 + this.WriteVp8Header(stream, vp8Size); + this.WriteFrameHeader(stream, size0); - // Emit headers and partition #0 - this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, xmpProfile, iccProfileBytes, hasAlpha, alphaData, alphaDataIsCompressed); bitWriterPartZero.WriteToStream(stream); // Write the encoded image to the stream. @@ -483,16 +472,31 @@ internal class Vp8BitWriter : BitWriterBase { stream.WriteByte(0); } + } + /// + /// Write the trunks after data trunk. + /// + /// Think of it as a static method — none of the other members are called except for + /// The stream to write to. + /// The exif profile. + /// The XMP profile. + public void WriteTrunksAfterData( + Stream stream, + ExifProfile? exifProfile, + XmpProfile? xmpProfile) + { if (exifProfile != null) { - this.WriteMetadataProfile(stream, exifBytes, WebpChunkType.Exif); + this.WriteMetadataProfile(stream, exifProfile.ToByteArray(), WebpChunkType.Exif); } if (xmpProfile != null) { - this.WriteMetadataProfile(stream, xmpBytes, WebpChunkType.Xmp); + this.WriteMetadataProfile(stream, xmpProfile.Data, WebpChunkType.Xmp); } + + OverwriteFileSize(stream); } private uint GeneratePartition0() @@ -512,7 +516,7 @@ internal class Vp8BitWriter : BitWriterBase this.Finish(); - return (uint)this.NumBytes(); + return (uint)this.NumBytes; } private void WriteSegmentHeader() @@ -662,43 +666,6 @@ internal class Vp8BitWriter : BitWriterBase while (it.Next()); } - private void WriteWebpHeaders( - Stream stream, - uint size0, - uint vp8Size, - uint riffSize, - bool isVp8X, - uint width, - uint height, - ExifProfile? exifProfile, - XmpProfile? xmpProfile, - byte[]? iccProfileBytes, - bool hasAlpha, - Span alphaData, - bool alphaDataIsCompressed) - { - this.WriteRiffHeader(stream, riffSize); - - // Write VP8X, header if necessary. - if (isVp8X) - { - this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfileBytes, width, height, hasAlpha); - - if (iccProfileBytes != null) - { - this.WriteColorProfile(stream, iccProfileBytes); - } - - if (hasAlpha) - { - this.WriteAlphaChunk(stream, alphaData, alphaDataIsCompressed); - } - } - - this.WriteVp8Header(stream, vp8Size); - this.WriteFrameHeader(stream, size0); - } - private void WriteVp8Header(Stream stream, uint size) { Span buf = stackalloc byte[WebpConstants.TagSize]; diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs index a042f68968..0ac1b4038a 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -47,6 +47,9 @@ internal class Vp8LBitWriter : BitWriterBase { } + /// + public override int NumBytes => this.cur + ((this.used + 7) >> 3); + /// /// Initializes a new instance of the class. /// Used internally for cloning. @@ -98,9 +101,6 @@ internal class Vp8LBitWriter : BitWriterBase this.PutBits((uint)((bits << depth) | symbol), depth + nBits); } - /// - public override int NumBytes() => this.cur + ((this.used + 7) >> 3); - public Vp8LBitWriter Clone() { byte[] clonedBuffer = new byte[this.Buffer.Length]; @@ -166,7 +166,7 @@ internal class Vp8LBitWriter : BitWriterBase } this.Finish(); - uint size = (uint)this.NumBytes(); + uint size = (uint)this.NumBytes; size++; // One byte extra for the VP8L signature. // Write RIFF header. @@ -177,7 +177,7 @@ internal class Vp8LBitWriter : BitWriterBase // Write VP8X, header if necessary. if (isVp8X) { - this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccBytes, width, height, hasAlpha); + this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha); if (iccBytes != null) { diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 9b82cc5983..d27bfcd956 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -291,7 +291,7 @@ internal class Vp8LEncoder : IDisposable // The image-stream will NOT contain any headers describing the image dimension, the dimension is already known. this.EncodeStream(frame); this.bitWriter.Finish(); - int size = this.bitWriter.NumBytes(); + int size = this.bitWriter.NumBytes; if (size >= pixelCount) { // Compressing would not yield in smaller data -> leave the data uncompressed. @@ -425,9 +425,9 @@ internal class Vp8LEncoder : IDisposable lowEffort); // If we are better than what we already have. - if (isFirstConfig || this.bitWriter.NumBytes() < bestSize) + if (isFirstConfig || this.bitWriter.NumBytes < bestSize) { - bestSize = this.bitWriter.NumBytes(); + bestSize = this.bitWriter.NumBytes; BitWriterSwap(ref this.bitWriter, ref bitWriterBest); } @@ -676,7 +676,7 @@ internal class Vp8LEncoder : IDisposable this.StoreImageToBitMask(width, this.HistoBits, refsBest, histogramSymbols, huffmanCodes); // Keep track of the smallest image so far. - if (isFirstIteration || (bitWriterBest != null && this.bitWriter.NumBytes() < bitWriterBest.NumBytes())) + if (isFirstIteration || (bitWriterBest != null && this.bitWriter.NumBytes < bitWriterBest.NumBytes)) { Vp8LBitWriter tmp = this.bitWriter; this.bitWriter = bitWriterBest; diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index c65099af88..56397e66d4 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Formats.Webp.BitWriter; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; @@ -408,16 +409,21 @@ internal class Vp8Encoder : IDisposable } } - this.bitWriter.WriteEncodedImageToStream( + this.bitWriter.Finish(); + this.bitWriter.WriteTrunksBeforeData( stream, + (uint)width, + (uint)height, exifProfile, xmpProfile, metadata.IccProfile, - (uint)width, - (uint)height, hasAlpha, alphaData[..alphaDataSize], this.alphaCompression && alphaCompressionSucceeded); + + this.bitWriter.WriteEncodedImageToStream(stream); + + this.bitWriter.WriteTrunksAfterData(stream, exifProfile, xmpProfile); } finally { @@ -862,10 +868,11 @@ internal class Vp8Encoder : IDisposable this.ResetSegments(); } - this.SegmentHeader.Size = (p[0] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(0, probas[1]))) + - (p[1] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(1, probas[1]))) + - (p[2] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(0, probas[2]))) + - (p[3] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(1, probas[2]))); + this.SegmentHeader.Size = + (p[0] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(0, probas[1]))) + + (p[1] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(1, probas[1]))) + + (p[2] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(0, probas[2]))) + + (p[3] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(1, probas[2]))); } else { @@ -1027,7 +1034,7 @@ internal class Vp8Encoder : IDisposable it.NzToBytes(); - int pos1 = this.bitWriter.NumBytes(); + int pos1 = this.bitWriter.NumBytes; if (i16) { residual.Init(0, 1, this.Proba); @@ -1054,7 +1061,7 @@ internal class Vp8Encoder : IDisposable } } - int pos2 = this.bitWriter.NumBytes(); + int pos2 = this.bitWriter.NumBytes; // U/V residual.Init(0, 2, this.Proba); @@ -1072,7 +1079,7 @@ internal class Vp8Encoder : IDisposable } } - int pos3 = this.bitWriter.NumBytes(); + int pos3 = this.bitWriter.NumBytes; it.LumaBits = pos2 - pos1; it.UvBits = pos3 - pos2; it.BitCount[segment, i16 ? 1 : 0] += it.LumaBits; diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs index becd622e17..9e9f0f7f62 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs @@ -76,7 +76,7 @@ internal static class WebpChunkParsingUtils WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 magic bytes"); } - if (!buffer.Slice(0, 3).SequenceEqual(WebpConstants.Vp8HeaderMagicBytes)) + if (!buffer[..3].SequenceEqual(WebpConstants.Vp8HeaderMagicBytes)) { WebpThrowHelper.ThrowImageFormatException("VP8 magic bytes not found"); } @@ -111,14 +111,7 @@ internal static class WebpChunkParsingUtils PartitionLength = partitionLength }; - Vp8BitReader bitReader = new( - stream, - remaining, - memoryAllocator, - partitionLength) - { - Remaining = remaining - }; + Vp8BitReader bitReader = new(stream, remaining, memoryAllocator, partitionLength) { Remaining = remaining }; return new() { diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs index 3b5e438299..be7350bc44 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs @@ -158,8 +158,7 @@ public sealed class IccProfile : IDeepCloneable Enum.IsDefined(typeof(IccColorSpaceType), this.Header.DataColorSpace) && Enum.IsDefined(typeof(IccColorSpaceType), this.Header.ProfileConnectionSpace) && Enum.IsDefined(typeof(IccRenderingIntent), this.Header.RenderingIntent) && - this.Header.Size >= minSize && - this.Header.Size < maxSize; + this.Header.Size is >= minSize and < maxSize; } /// @@ -175,7 +174,6 @@ public sealed class IccProfile : IDeepCloneable return copy; } - IccWriter writer = new(); return IccWriter.Write(this); } From 437144dab5bb08743aa43e2411ec7bd38963b1c0 Mon Sep 17 00:00:00 2001 From: Poker Date: Mon, 23 Oct 2023 21:14:31 +0800 Subject: [PATCH 09/24] (Vp8L) Write total size after writing. Separate the writes of each block --- .../Formats/Webp/BitWriter/BitWriterBase.cs | 76 ++++++++++++++++++ .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 80 +------------------ .../Formats/Webp/BitWriter/Vp8LBitWriter.cs | 76 +----------------- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 17 +++- .../Formats/Webp/Lossy/Vp8Encoder.cs | 1 - 5 files changed, 97 insertions(+), 153 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index b82b764fc3..ad7d69f130 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -137,6 +137,82 @@ internal abstract class BitWriterBase stream.Position = position; } + /// + /// Write the trunks before data trunk. + /// + /// Think of it as a static method — none of the other members are called except for + /// The stream to write to. + /// The width of the image. + /// The height of the image. + /// The exif profile. + /// The XMP profile. + /// The color profile. + /// Flag indicating, if a alpha channel is present. + /// The alpha channel data. + /// Indicates, if the alpha data is compressed. + public void WriteTrunksBeforeData( + Stream stream, + uint width, + uint height, + ExifProfile? exifProfile, + XmpProfile? xmpProfile, + IccProfile? iccProfile, + bool hasAlpha, + Span alphaData, + bool alphaDataIsCompressed) + { + // Write file size later + this.WriteRiffHeader(stream, 0); + + // Write VP8X, header if necessary. + bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha; + if (isVp8X) + { + this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha); + + if (iccProfile != null) + { + this.WriteColorProfile(stream, iccProfile.ToByteArray()); + } + + if (hasAlpha) + { + this.WriteAlphaChunk(stream, alphaData, alphaDataIsCompressed); + } + } + } + + /// + /// Writes the encoded image to the stream. + /// + /// The stream to write to. + public abstract void WriteEncodedImageToStream(Stream stream); + + /// + /// Write the trunks after data trunk. + /// + /// Think of it as a static method — none of the other members are called except for + /// The stream to write to. + /// The exif profile. + /// The XMP profile. + public void WriteTrunksAfterData( + Stream stream, + ExifProfile? exifProfile, + XmpProfile? xmpProfile) + { + if (exifProfile != null) + { + this.WriteMetadataProfile(stream, exifProfile.ToByteArray(), WebpChunkType.Exif); + } + + if (xmpProfile != null) + { + this.WriteMetadataProfile(stream, xmpProfile.Data, WebpChunkType.Xmp); + } + + OverwriteFileSize(stream); + } + /// /// Writes a metadata profile (EXIF or XMP) to the stream. /// diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index 5dd5d335de..923d2a69c4 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -3,9 +3,6 @@ using System.Buffers.Binary; using SixLabors.ImageSharp.Formats.Webp.Lossy; -using SixLabors.ImageSharp.Metadata.Profiles.Exif; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; -using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Webp.BitWriter; @@ -394,56 +391,8 @@ internal class Vp8BitWriter : BitWriterBase } } - /// - /// Write the trunks before data trunk. - /// - /// Think of it as a static method — none of the other members are called except for - /// The stream to write to. - /// The width of the image. - /// The height of the image. - /// The exif profile. - /// The XMP profile. - /// The color profile. - /// Flag indicating, if a alpha channel is present. - /// The alpha channel data. - /// Indicates, if the alpha data is compressed. - public void WriteTrunksBeforeData( - Stream stream, - uint width, - uint height, - ExifProfile? exifProfile, - XmpProfile? xmpProfile, - IccProfile? iccProfile, - bool hasAlpha, - Span alphaData, - bool alphaDataIsCompressed) - { - // Write file size later - this.WriteRiffHeader(stream, 0); - - // Write VP8X, header if necessary. - bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha; - if (isVp8X) - { - this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha); - - if (iccProfile != null) - { - this.WriteColorProfile(stream, iccProfile.ToByteArray()); - } - - if (hasAlpha) - { - this.WriteAlphaChunk(stream, alphaData, alphaDataIsCompressed); - } - } - } - - /// - /// Writes the encoded image to the stream. - /// - /// The stream to write to. - public void WriteEncodedImageToStream(Stream stream) + /// + public override void WriteEncodedImageToStream(Stream stream) { uint numBytes = (uint)this.NumBytes; @@ -474,31 +423,6 @@ internal class Vp8BitWriter : BitWriterBase } } - /// - /// Write the trunks after data trunk. - /// - /// Think of it as a static method — none of the other members are called except for - /// The stream to write to. - /// The exif profile. - /// The XMP profile. - public void WriteTrunksAfterData( - Stream stream, - ExifProfile? exifProfile, - XmpProfile? xmpProfile) - { - if (exifProfile != null) - { - this.WriteMetadataProfile(stream, exifProfile.ToByteArray(), WebpChunkType.Exif); - } - - if (xmpProfile != null) - { - this.WriteMetadataProfile(stream, xmpProfile.Data, WebpChunkType.Xmp); - } - - OverwriteFileSize(stream); - } - private uint GeneratePartition0() { this.PutBitUniform(0); // colorspace diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs index 0ac1b4038a..bce77c9e5c 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -3,9 +3,6 @@ using System.Buffers.Binary; using SixLabors.ImageSharp.Formats.Webp.Lossless; -using SixLabors.ImageSharp.Metadata.Profiles.Exif; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; -using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Webp.BitWriter; @@ -122,68 +119,11 @@ internal class Vp8LBitWriter : BitWriterBase this.used = 0; } - /// - /// Writes the encoded image to the stream. - /// - /// The stream to write to. - /// The exif profile. - /// The XMP profile. - /// The color profile. - /// The width of the image. - /// The height of the image. - /// Flag indicating, if a alpha channel is present. - public void WriteEncodedImageToStream(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, uint width, uint height, bool hasAlpha) + /// + public override void WriteEncodedImageToStream(Stream stream) { - bool isVp8X = false; - byte[]? exifBytes = null; - byte[]? xmpBytes = null; - byte[]? iccBytes = null; - uint riffSize = 0; - if (exifProfile != null) - { - isVp8X = true; - exifBytes = exifProfile.ToByteArray(); - riffSize += MetadataChunkSize(exifBytes!); - } - - if (xmpProfile != null) - { - isVp8X = true; - xmpBytes = xmpProfile.Data; - riffSize += MetadataChunkSize(xmpBytes!); - } - - if (iccProfile != null) - { - isVp8X = true; - iccBytes = iccProfile.ToByteArray(); - riffSize += MetadataChunkSize(iccBytes); - } - - if (isVp8X) - { - riffSize += ExtendedFileChunkSize; - } - - this.Finish(); - uint size = (uint)this.NumBytes; - size++; // One byte extra for the VP8L signature. - - // Write RIFF header. + uint size = (uint)this.NumBytes + 1; // One byte extra for the VP8L signature uint pad = size & 1; - riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + size + pad; - this.WriteRiffHeader(stream, riffSize); - - // Write VP8X, header if necessary. - if (isVp8X) - { - this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha); - - if (iccBytes != null) - { - this.WriteColorProfile(stream, iccBytes); - } - } // Write magic bytes indicating its a lossless webp. Span scratchBuffer = stackalloc byte[WebpConstants.TagSize]; @@ -201,16 +141,6 @@ internal class Vp8LBitWriter : BitWriterBase { stream.WriteByte(0); } - - if (exifProfile != null) - { - this.WriteMetadataProfile(stream, exifBytes, WebpChunkType.Exif); - } - - if (xmpProfile != null) - { - this.WriteMetadataProfile(stream, xmpBytes, WebpChunkType.Xmp); - } } /// diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index d27bfcd956..4d526e7b4b 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Formats.Webp.BitWriter; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; @@ -265,8 +266,22 @@ internal class Vp8LEncoder : IDisposable // Encode the main image stream. this.EncodeStream(image.Frames.RootFrame); + this.bitWriter.Finish(); + this.bitWriter.WriteTrunksBeforeData( + stream, + (uint)width, + (uint)height, + exifProfile, + xmpProfile, + metadata.IccProfile, + false /*hasAlpha*/, + Span.Empty, + false); + // Write bytes from the bitwriter buffer to the stream. - this.bitWriter.WriteEncodedImageToStream(stream, exifProfile, xmpProfile, metadata.IccProfile, (uint)width, (uint)height, hasAlpha); + this.bitWriter.WriteEncodedImageToStream(stream); + + this.bitWriter.WriteTrunksAfterData(stream, exifProfile, xmpProfile); } /// diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 56397e66d4..f744827bf3 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -8,7 +8,6 @@ using SixLabors.ImageSharp.Formats.Webp.BitWriter; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; From fbc08bd6a683a8c96777dfc17441d3fabb7f554c Mon Sep 17 00:00:00 2001 From: Poker Date: Mon, 23 Oct 2023 23:21:11 +0800 Subject: [PATCH 10/24] Implement Vp8 encoder --- .../Formats/Webp/BitWriter/BitWriterBase.cs | 106 +++++++-------- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 5 +- .../Formats/Webp/Lossy/Vp8Encoder.cs | 121 ++++++++++++++---- .../Formats/Webp/WebpEncoderCore.cs | 30 ++++- .../Formats/WebP/WebpEncoderTests.cs | 7 + 5 files changed, 180 insertions(+), 89 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index ad7d69f130..4a9da3cbb1 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -87,21 +87,20 @@ internal abstract class BitWriterBase /// /// Writes the RIFF header to the stream. /// - /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The block length. - protected void WriteRiffHeader(Stream stream, uint riffSize) + protected static void WriteRiffHeader(Stream stream, uint riffSize) { stream.Write(WebpConstants.RiffFourCc); - BinaryPrimitives.WriteUInt32LittleEndian(this.scratchBuffer.Span, riffSize); - stream.Write(this.scratchBuffer.Span[..4]); + Span buf = stackalloc byte[4]; + BinaryPrimitives.WriteUInt32LittleEndian(buf, riffSize); + stream.Write(buf); stream.Write(WebpConstants.WebpHeader); } /// /// Calculates the chunk size of EXIF, XMP or ICCP metadata. /// - /// Think of it as a static method — none of the other members are called except for /// The metadata profile bytes. /// The metadata chunk size in bytes. protected static uint MetadataChunkSize(byte[] metadataBytes) @@ -125,22 +124,11 @@ internal abstract class BitWriterBase /// Overwrites ides the write file size. /// /// The stream to write to. - protected static void OverwriteFileSize(Stream stream) - { - uint position = (uint)stream.Position; - stream.Position = 4; - byte[] buffer = new byte[4]; - - // "RIFF"(4)+uint32 size(4) - BinaryPrimitives.WriteUInt32LittleEndian(buffer, position - WebpConstants.ChunkHeaderSize); - stream.Write(buffer); - stream.Position = position; - } + protected static void OverwriteFileSize(Stream stream) => OverwriteFrameSize(stream, 4); /// /// Write the trunks before data trunk. /// - /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The width of the image. /// The height of the image. @@ -148,9 +136,8 @@ internal abstract class BitWriterBase /// The XMP profile. /// The color profile. /// Flag indicating, if a alpha channel is present. - /// The alpha channel data. - /// Indicates, if the alpha data is compressed. - public void WriteTrunksBeforeData( + /// Flag indicating, if an animation parameter is present. + public static void WriteTrunksBeforeData( Stream stream, uint width, uint height, @@ -158,26 +145,20 @@ internal abstract class BitWriterBase XmpProfile? xmpProfile, IccProfile? iccProfile, bool hasAlpha, - Span alphaData, - bool alphaDataIsCompressed) + bool hasAnimation) { // Write file size later - this.WriteRiffHeader(stream, 0); + WriteRiffHeader(stream, 0); // Write VP8X, header if necessary. - bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha; + bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha || hasAnimation; if (isVp8X) { - this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha); + WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfile, width, height, hasAlpha, hasAnimation); if (iccProfile != null) { - this.WriteColorProfile(stream, iccProfile.ToByteArray()); - } - - if (hasAlpha) - { - this.WriteAlphaChunk(stream, alphaData, alphaDataIsCompressed); + WriteColorProfile(stream, iccProfile.ToByteArray()); } } } @@ -191,23 +172,22 @@ internal abstract class BitWriterBase /// /// Write the trunks after data trunk. /// - /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The exif profile. /// The XMP profile. - public void WriteTrunksAfterData( + public static void WriteTrunksAfterData( Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile) { if (exifProfile != null) { - this.WriteMetadataProfile(stream, exifProfile.ToByteArray(), WebpChunkType.Exif); + WriteMetadataProfile(stream, exifProfile.ToByteArray(), WebpChunkType.Exif); } if (xmpProfile != null) { - this.WriteMetadataProfile(stream, xmpProfile.Data, WebpChunkType.Xmp); + WriteMetadataProfile(stream, xmpProfile.Data, WebpChunkType.Xmp); } OverwriteFileSize(stream); @@ -216,16 +196,15 @@ internal abstract class BitWriterBase /// /// Writes a metadata profile (EXIF or XMP) to the stream. /// - /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The metadata profile's bytes. /// The chuck type to write. - protected void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpChunkType chunkType) + protected static void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpChunkType chunkType) { DebugGuard.NotNull(metadataBytes, nameof(metadataBytes)); uint size = (uint)metadataBytes.Length; - Span buf = this.scratchBuffer.Span[..4]; + Span buf = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)chunkType); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, size); @@ -242,15 +221,13 @@ internal abstract class BitWriterBase /// /// Writes the color profile() to the stream. /// - /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The color profile bytes. - protected void WriteColorProfile(Stream stream, byte[] iccProfileBytes) => this.WriteMetadataProfile(stream, iccProfileBytes, WebpChunkType.Iccp); + protected static void WriteColorProfile(Stream stream, byte[] iccProfileBytes) => WriteMetadataProfile(stream, iccProfileBytes, WebpChunkType.Iccp); /// /// Writes the animation parameter() to the stream. /// - /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// /// The default background color of the canvas in [Blue, Green, Red, Alpha] byte order. @@ -259,9 +236,9 @@ internal abstract class BitWriterBase /// The background color is also used when the Disposal method is 1. /// /// The number of times to loop the animation. If it is 0, this means infinitely. - protected void WriteAnimationParameter(Stream stream, uint background, ushort loopCount) + public static void WriteAnimationParameter(Stream stream, uint background, ushort loopCount) { - Span buf = this.scratchBuffer.Span[..4]; + Span buf = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.AnimationParameter); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, sizeof(uint) + sizeof(ushort)); @@ -275,17 +252,15 @@ internal abstract class BitWriterBase /// /// Writes the animation frame() to the stream. /// - /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// Animation frame data. - /// Frame data. - protected void WriteAnimationFrame(Stream stream, AnimationFrameData animation, Span data) + public static long WriteAnimationFrame(Stream stream, AnimationFrameData animation) { - uint size = AnimationFrameData.HeaderSize + (uint)data.Length; - Span buf = this.scratchBuffer.Span[..4]; + Span buf = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Animation); stream.Write(buf); - BinaryPrimitives.WriteUInt32BigEndian(buf, size); + long position = stream.Position; + BinaryPrimitives.WriteUInt32BigEndian(buf, 0); stream.Write(buf); WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.X); WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Y); @@ -294,20 +269,35 @@ internal abstract class BitWriterBase WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Duration); byte flag = (byte)(((int)animation.BlendingMethod << 1) | (int)animation.DisposalMethod); stream.WriteByte(flag); - stream.Write(data); + return position; + } + + /// + /// Overwrites ides the write frame size. + /// + /// The stream to write to. + /// Previous position. + public static void OverwriteFrameSize(Stream stream, long prevPosition) + { + uint position = (uint)stream.Position; + stream.Position = prevPosition; + byte[] buffer = new byte[4]; + + BinaryPrimitives.WriteUInt32LittleEndian(buffer, (uint)(position - prevPosition - 4)); + stream.Write(buffer); + stream.Position = position; } /// /// Writes the alpha chunk to the stream. /// - /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// The alpha channel data bytes. /// Indicates, if the alpha channel data is compressed. - protected void WriteAlphaChunk(Stream stream, Span dataBytes, bool alphaDataIsCompressed) + public static void WriteAlphaChunk(Stream stream, Span dataBytes, bool alphaDataIsCompressed) { uint size = (uint)dataBytes.Length + 1; - Span buf = this.scratchBuffer.Span[..4]; + Span buf = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Alpha); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, size); @@ -332,7 +322,6 @@ internal abstract class BitWriterBase /// /// Writes a VP8X header to the stream. /// - /// Think of it as a static method — none of the other members are called except for /// The stream to write to. /// A exif profile or null, if it does not exist. /// A XMP profile or null, if it does not exist. @@ -340,7 +329,8 @@ internal abstract class BitWriterBase /// The width of the image. /// The height of the image. /// Flag indicating, if a alpha channel is present. - protected void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, uint width, uint height, bool hasAlpha) + /// Flag indicating, if an animation parameter is present. + protected static void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, uint width, uint height, bool hasAlpha, bool hasAnimation) { if (width > MaxDimension || height > MaxDimension) { @@ -360,13 +350,11 @@ internal abstract class BitWriterBase flags |= 8; } - /* - if (isAnimated) + if (hasAnimation) { // Set animated flag. flags |= 2; } - */ if (xmpProfile != null) { @@ -386,7 +374,7 @@ internal abstract class BitWriterBase flags |= 32; } - Span buf = this.scratchBuffer.Span[..4]; + Span buf = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Vp8X); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, WebpConstants.Vp8XChunkSize); diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 4d526e7b4b..5859d8a872 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -267,7 +267,7 @@ internal class Vp8LEncoder : IDisposable this.EncodeStream(image.Frames.RootFrame); this.bitWriter.Finish(); - this.bitWriter.WriteTrunksBeforeData( + BitWriterBase.WriteTrunksBeforeData( stream, (uint)width, (uint)height, @@ -275,13 +275,12 @@ internal class Vp8LEncoder : IDisposable xmpProfile, metadata.IccProfile, false /*hasAlpha*/, - Span.Empty, false); // Write bytes from the bitwriter buffer to the stream. this.bitWriter.WriteEncodedImageToStream(stream); - this.bitWriter.WriteTrunksAfterData(stream, exifProfile, xmpProfile); + BitWriterBase.WriteTrunksAfterData(stream, exifProfile, xmpProfile); } /// diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index f744827bf3..ccd7d8b6d5 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -88,7 +88,8 @@ internal class Vp8Encoder : IDisposable private const ulong Partition0SizeLimit = (WebpConstants.Vp8MaxPartition0Size - 2048UL) << 11; - private const long HeaderSizeEstimate = WebpConstants.RiffHeaderSize + WebpConstants.ChunkHeaderSize + WebpConstants.Vp8FrameHeaderSize; + private const long HeaderSizeEstimate = + WebpConstants.RiffHeaderSize + WebpConstants.ChunkHeaderSize + WebpConstants.Vp8FrameHeaderSize; private const int QMin = 0; @@ -165,7 +166,7 @@ internal class Vp8Encoder : IDisposable // TODO: make partition_limit configurable? const int limit = 100; // original code: limit = 100 - config->partition_limit; this.maxI4HeaderBits = - 256 * 16 * 16 * limit * limit / (100 * 100); // ... modulated with a quadratic curve. + 256 * 16 * 16 * limit * limit / (100 * 100); // ... modulated with a quadratic curve. this.MbInfo = new Vp8MacroBlockInfo[this.Mbw * this.Mbh]; for (int i = 0; i < this.MbInfo.Length; i++) @@ -308,22 +309,88 @@ internal class Vp8Encoder : IDisposable /// private int MbHeaderLimit { get; } + public void EncodeHeader(Image image, Stream stream, bool hasAlpha, bool hasAnimation, uint background = 0, uint loopCount = 0) + where TPixel : unmanaged, IPixel + { + // Write bytes from the bitwriter buffer to the stream. + ImageMetadata metadata = image.Metadata; + metadata.SyncProfiles(); + + ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; + XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; + + BitWriterBase.WriteTrunksBeforeData( + stream, + (uint)image.Width, + (uint)image.Height, + exifProfile, + xmpProfile, + metadata.IccProfile, + hasAlpha, + hasAnimation); + + if (hasAnimation) + { + BitWriterBase.WriteAnimationParameter(stream, background, (ushort)loopCount); + } + } + + public void EncodeFooter(Image image, Stream stream) + where TPixel : unmanaged, IPixel + { + // Write bytes from the bitwriter buffer to the stream. + ImageMetadata metadata = image.Metadata; + + ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; + XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; + + BitWriterBase.WriteTrunksAfterData(stream, exifProfile, xmpProfile); + } + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + public void EncodeAnimation(ImageFrame frame, Stream stream) + where TPixel : unmanaged, IPixel => + this.Encode(frame, stream, true, null); + /// /// Encodes the image to the specified stream from the . /// /// The pixel format. /// The to encode from. /// The to encode the image data to. - public void Encode(Image image, Stream stream) + public void EncodeStatic(Image image, Stream stream) + where TPixel : unmanaged, IPixel => + this.Encode(image.Frames.RootFrame, stream, false, image); + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// Flag indicating, if an animation parameter is present. + /// The to encode from. + private void Encode(ImageFrame frame, Stream stream, bool hasAnimation, Image image) where TPixel : unmanaged, IPixel { - int width = image.Width; - int height = image.Height; + int width = frame.Width; + int height = frame.Height; + int pixelCount = width * height; Span y = this.Y.GetSpan(); Span u = this.U.GetSpan(); Span v = this.V.GetSpan(); - bool hasAlpha = YuvConversion.ConvertRgbToYuv(image.Frames.RootFrame, this.configuration, this.memoryAllocator, y, u, v); + bool hasAlpha = YuvConversion.ConvertRgbToYuv(frame, this.configuration, this.memoryAllocator, y, u, v); + + if (!hasAnimation) + { + this.EncodeHeader(image, stream, hasAlpha, false); + } int yStride = width; int uvStride = (yStride + 1) >> 1; @@ -375,13 +442,6 @@ internal class Vp8Encoder : IDisposable // Store filter stats. this.AdjustFilterStrength(); - // Write bytes from the bitwriter buffer to the stream. - ImageMetadata metadata = image.Metadata; - metadata.SyncProfiles(); - - ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; - XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; - // Extract and encode alpha channel data, if present. int alphaDataSize = 0; bool alphaCompressionSucceeded = false; @@ -393,7 +453,7 @@ internal class Vp8Encoder : IDisposable { // TODO: This can potentially run in an separate task. encodedAlphaData = AlphaEncoder.EncodeAlpha( - image.Frames.RootFrame, + frame, this.configuration, this.memoryAllocator, this.skipMetadata, @@ -409,20 +469,31 @@ internal class Vp8Encoder : IDisposable } this.bitWriter.Finish(); - this.bitWriter.WriteTrunksBeforeData( - stream, - (uint)width, - (uint)height, - exifProfile, - xmpProfile, - metadata.IccProfile, - hasAlpha, - alphaData[..alphaDataSize], - this.alphaCompression && alphaCompressionSucceeded); + + long prevPosition = 0; + + if (hasAnimation) + { + prevPosition = BitWriterBase.WriteAnimationFrame(stream, new() + { + Width = (uint)frame.Width, + Height = (uint)frame.Height + }); + } + + if (hasAlpha) + { + Span data = alphaData[..alphaDataSize]; + bool alphaDataIsCompressed = this.alphaCompression && alphaCompressionSucceeded; + BitWriterBase.WriteAlphaChunk(stream, data, alphaDataIsCompressed); + } this.bitWriter.WriteEncodedImageToStream(stream); - this.bitWriter.WriteTrunksAfterData(stream, exifProfile, xmpProfile); + if (hasAnimation) + { + BitWriterBase.OverwriteFrameSize(stream, prevPosition); + } } finally { diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index 49512e03b5..2751f99134 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -144,7 +144,7 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals } else { - using Vp8Encoder enc = new( + using Vp8Encoder encoder = new( this.memoryAllocator, this.configuration, image.Width, @@ -156,7 +156,33 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals this.filterStrength, this.spatialNoiseShaping, this.alphaCompression); - enc.Encode(image, stream); + if (image.Frames.Count > 1) + { + encoder.EncodeHeader(image, stream, false, true); + + foreach (ImageFrame imageFrame in image.Frames) + { + using Vp8Encoder enc = new( + this.memoryAllocator, + this.configuration, + image.Width, + image.Height, + this.quality, + this.skipMetadata, + this.method, + this.entropyPasses, + this.filterStrength, + this.spatialNoiseShaping, + this.alphaCompression); + enc.EncodeAnimation(imageFrame, stream); + } + } + else + { + encoder.EncodeStatic(image, stream); + } + + encoder.EncodeFooter(image, stream); } } } diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index 6c5fa50ff6..4b100e854e 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -17,6 +17,13 @@ public class WebpEncoderTests { private static string TestImageLossyFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, Lossy.NoFilter06); + [Fact] + public void Encode_AnimatedLossy() + { + Image image = Image.Load(@"C:\Users\poker\Desktop\1.webp"); + image.SaveAsWebp(@"C:\Users\poker\Desktop\3.webp"); + } + [Theory] [WithFile(Flag, PixelTypes.Rgba32, WebpFileFormatType.Lossy)] // If its not a webp input image, it should default to lossy. [WithFile(Lossless.NoTransform1, PixelTypes.Rgba32, WebpFileFormatType.Lossless)] From 87de52141219f23a39559a8bae09d79c30e109d6 Mon Sep 17 00:00:00 2001 From: Poker Date: Mon, 23 Oct 2023 23:58:56 +0800 Subject: [PATCH 11/24] Implement Vp8L encoder --- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 87 +++++++++++++------ .../Formats/Webp/WebpEncoderCore.cs | 32 ++++++- .../Formats/WebP/WebpEncoderTests.cs | 7 +- 3 files changed, 97 insertions(+), 29 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 5859d8a872..d301df94f6 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -10,7 +10,6 @@ using SixLabors.ImageSharp.Formats.Webp.BitWriter; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; -using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; @@ -236,26 +235,59 @@ internal class Vp8LEncoder : IDisposable /// public Vp8LHashChain HashChain { get; } - /// - /// Encodes the image as lossless webp to the specified stream. - /// - /// The pixel format. - /// The to encode from. - /// The to encode the image data to. - public void Encode(Image image, Stream stream) + public void EncodeHeader(Image image, Stream stream, bool hasAnimation, uint background = 0, uint loopCount = 0) where TPixel : unmanaged, IPixel { - int width = image.Width; - int height = image.Height; - + // Write bytes from the bitwriter buffer to the stream. ImageMetadata metadata = image.Metadata; metadata.SyncProfiles(); ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; + BitWriterBase.WriteTrunksBeforeData( + stream, + (uint)image.Width, + (uint)image.Height, + exifProfile, + xmpProfile, + metadata.IccProfile, + false, + hasAnimation); + + if (hasAnimation) + { + BitWriterBase.WriteAnimationParameter(stream, background, (ushort)loopCount); + } + } + + public void EncodeFooter(Image image, Stream stream) + where TPixel : unmanaged, IPixel + { + // Write bytes from the bitwriter buffer to the stream. + ImageMetadata metadata = image.Metadata; + + ExifProfile exifProfile = this.skipMetadata ? null : metadata.ExifProfile; + XmpProfile xmpProfile = this.skipMetadata ? null : metadata.XmpProfile; + + BitWriterBase.WriteTrunksAfterData(stream, exifProfile, xmpProfile); + } + + /// + /// Encodes the image as lossless webp to the specified stream. + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// Flag indicating, if an animation parameter is present. + public void Encode(ImageFrame frame, Stream stream, bool hasAnimation) + where TPixel : unmanaged, IPixel + { + int width = frame.Width; + int height = frame.Height; + // Convert image pixels to bgra array. - bool hasAlpha = this.ConvertPixelsToBgra(image.Frames.RootFrame, width, height); + bool hasAlpha = this.ConvertPixelsToBgra(frame, width, height); // Write the image size. this.WriteImageSize(width, height); @@ -264,23 +296,28 @@ internal class Vp8LEncoder : IDisposable this.WriteAlphaAndVersion(hasAlpha); // Encode the main image stream. - this.EncodeStream(image.Frames.RootFrame); + this.EncodeStream(frame); this.bitWriter.Finish(); - BitWriterBase.WriteTrunksBeforeData( - stream, - (uint)width, - (uint)height, - exifProfile, - xmpProfile, - metadata.IccProfile, - false /*hasAlpha*/, - false); + + long prevPosition = 0; + + if (hasAnimation) + { + prevPosition = BitWriterBase.WriteAnimationFrame(stream, new() + { + Width = (uint)frame.Width, + Height = (uint)frame.Height + }); + } // Write bytes from the bitwriter buffer to the stream. this.bitWriter.WriteEncodedImageToStream(stream); - BitWriterBase.WriteTrunksAfterData(stream, exifProfile, xmpProfile); + if (hasAnimation) + { + BitWriterBase.OverwriteFrameSize(stream, prevPosition); + } } /// @@ -1843,9 +1880,9 @@ internal class Vp8LEncoder : IDisposable { this.Bgra.Dispose(); this.EncodedData.Dispose(); - this.BgraScratch.Dispose(); + this.BgraScratch?.Dispose(); this.Palette.Dispose(); - this.TransformData.Dispose(); + this.TransformData?.Dispose(); this.HashChain.Dispose(); } diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index 2751f99134..47712071bf 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -129,7 +129,7 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals if (lossless) { - using Vp8LEncoder enc = new( + using Vp8LEncoder encoder = new( this.memoryAllocator, this.configuration, image.Width, @@ -140,7 +140,34 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals this.transparentColorMode, this.nearLossless, this.nearLosslessQuality); - enc.Encode(image, stream); + + bool hasAnimation = image.Frames.Count > 1; + encoder.EncodeHeader(image, stream, hasAnimation); + if (hasAnimation) + { + foreach (ImageFrame imageFrame in image.Frames) + { + using Vp8LEncoder enc = new( + this.memoryAllocator, + this.configuration, + image.Width, + image.Height, + this.quality, + this.skipMetadata, + this.method, + this.transparentColorMode, + this.nearLossless, + this.nearLosslessQuality); + + enc.Encode(imageFrame, stream, true); + } + } + else + { + encoder.Encode(image.Frames.RootFrame, stream, false); + } + + encoder.EncodeFooter(image, stream); } else { @@ -174,6 +201,7 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals this.filterStrength, this.spatialNoiseShaping, this.alphaCompression); + enc.EncodeAnimation(imageFrame, stream); } } diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index 4b100e854e..1721cd938b 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -20,8 +20,11 @@ public class WebpEncoderTests [Fact] public void Encode_AnimatedLossy() { - Image image = Image.Load(@"C:\Users\poker\Desktop\1.webp"); - image.SaveAsWebp(@"C:\Users\poker\Desktop\3.webp"); + Image image = Image.Load(@"C:\WorkSpace\ImageSharp\tests\Images\Input\Webp\leo_animated_lossless.webp"); + image.SaveAsWebp(@"C:\Users\poker\Desktop\3.webp", new WebpEncoder() + { + FileFormat = WebpFileFormatType.Lossless + }); } [Theory] From 2c260b27bf273f7154a93a43bf1e5149776fbe5d Mon Sep 17 00:00:00 2001 From: Poker Date: Tue, 24 Oct 2023 00:09:10 +0800 Subject: [PATCH 12/24] add unit test and format --- .../Formats/Webp/BitWriter/Vp8LBitWriter.cs | 6 ++-- .../Formats/WebP/WebpEncoderTests.cs | 29 ++++++++++++++----- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs index bce77c9e5c..0b71a3ed0c 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -44,9 +44,6 @@ internal class Vp8LBitWriter : BitWriterBase { } - /// - public override int NumBytes => this.cur + ((this.used + 7) >> 3); - /// /// Initializes a new instance of the class. /// Used internally for cloning. @@ -59,6 +56,9 @@ internal class Vp8LBitWriter : BitWriterBase this.cur = cur; } + /// + public override int NumBytes => this.cur + ((this.used + 7) >> 3); + /// /// This function writes bits into bytes in increasing addresses (little endian), /// and within a byte least-significant-bit first. This function can write up to 32 bits in one go. diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index 1721cd938b..d81c9eb93a 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -17,14 +18,28 @@ public class WebpEncoderTests { private static string TestImageLossyFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, Lossy.NoFilter06); - [Fact] - public void Encode_AnimatedLossy() + [Theory] + [WithFile(Lossless.Animated, PixelTypes.Rgba32)] + public void Encode_AnimatedLossless(TestImageProvider provider) + where TPixel : unmanaged, IPixel { - Image image = Image.Load(@"C:\WorkSpace\ImageSharp\tests\Images\Input\Webp\leo_animated_lossless.webp"); - image.SaveAsWebp(@"C:\Users\poker\Desktop\3.webp", new WebpEncoder() - { - FileFormat = WebpFileFormatType.Lossless - }); + using Image image = provider.GetImage(); + using MemoryStream memStream = new(); + image.SaveAsWebp(memStream, new() { FileFormat = WebpFileFormatType.Lossless }); + + // TODO: DebugSave, VerifySimilarity + } + + [Theory] + [WithFile(Lossy.Animated, PixelTypes.Rgba32)] + public void Encode_AnimatedLossy(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + using MemoryStream memStream = new(); + image.SaveAsWebp(memStream, new()); + + // TODO: DebugSave, VerifySimilarity } [Theory] From 6fed95b5165f4216d9cb476d54fbd3e7faf5f59b Mon Sep 17 00:00:00 2001 From: Poker Date: Tue, 24 Oct 2023 09:03:23 +0800 Subject: [PATCH 13/24] remove unused scratchBuffer field --- src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index 4a9da3cbb1..89db7ed645 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -22,11 +22,6 @@ internal abstract class BitWriterBase /// private byte[] buffer; - /// - /// A scratch buffer to reduce allocations. - /// - private ScratchBuffer scratchBuffer; // mutable struct, don't make readonly - /// /// Initializes a new instance of the class. /// From dbb89603ee4ac639f8b6fa08fde747250b10d7b5 Mon Sep 17 00:00:00 2001 From: Poker Date: Tue, 24 Oct 2023 12:38:17 +0800 Subject: [PATCH 14/24] encode FrameDuration --- src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs | 3 ++- src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index d301df94f6..af472845ac 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -307,7 +307,8 @@ internal class Vp8LEncoder : IDisposable prevPosition = BitWriterBase.WriteAnimationFrame(stream, new() { Width = (uint)frame.Width, - Height = (uint)frame.Height + Height = (uint)frame.Height, + Duration = frame.Metadata.GetWebpMetadata().FrameDuration }); } diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index ccd7d8b6d5..40dbb90de6 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -477,7 +477,8 @@ internal class Vp8Encoder : IDisposable prevPosition = BitWriterBase.WriteAnimationFrame(stream, new() { Width = (uint)frame.Width, - Height = (uint)frame.Height + Height = (uint)frame.Height, + Duration = frame.Metadata.GetWebpMetadata().FrameDuration }); } From bd2d4550a998bb521cc8cce39bad9be42da201bf Mon Sep 17 00:00:00 2001 From: Poker Date: Wed, 25 Oct 2023 16:47:14 +0800 Subject: [PATCH 15/24] refactor to follow style rules --- src/ImageSharp/Formats/Webp/AlphaDecoder.cs | 2 +- src/ImageSharp/Formats/Webp/AlphaEncoder.cs | 17 ++---- .../Formats/Webp/AnimationFrameData.cs | 2 +- .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 4 +- .../Formats/Webp/BitWriter/Vp8LBitWriter.cs | 2 +- .../Webp/Lossless/BackwardReferenceEncoder.cs | 2 +- .../Formats/Webp/Lossless/CostManager.cs | 2 +- .../Formats/Webp/Lossless/PixOrCopy.cs | 7 ++- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 18 +++--- .../Webp/Lossless/WebpLosslessDecoder.cs | 4 +- .../Formats/Webp/Lossy/Vp8EncIterator.cs | 6 +- .../Formats/Webp/Lossy/Vp8Encoder.cs | 16 ++--- .../Formats/Webp/Lossy/WebpLossyDecoder.cs | 18 +++--- .../Formats/Webp/WebpAnimationDecoder.cs | 12 ++-- .../Formats/Webp/WebpAnimationEncoder.cs | 12 ---- .../Formats/Webp/WebpChunkParsingUtils.cs | 20 ++++--- src/ImageSharp/Formats/Webp/WebpDecoder.cs | 9 ++- .../Formats/Webp/WebpDecoderCore.cs | 27 +++++---- .../Formats/Webp/WebpDecoderOptions.cs | 2 +- src/ImageSharp/Formats/Webp/WebpEncoder.cs | 2 +- .../Formats/Webp/WebpEncoderCore.cs | 58 ++++--------------- src/ImageSharp/Formats/Webp/WebpFormat.cs | 6 +- 22 files changed, 99 insertions(+), 149 deletions(-) delete mode 100644 src/ImageSharp/Formats/Webp/WebpAnimationEncoder.cs diff --git a/src/ImageSharp/Formats/Webp/AlphaDecoder.cs b/src/ImageSharp/Formats/Webp/AlphaDecoder.cs index 289ebd35ca..63e6541354 100644 --- a/src/ImageSharp/Formats/Webp/AlphaDecoder.cs +++ b/src/ImageSharp/Formats/Webp/AlphaDecoder.cs @@ -59,7 +59,7 @@ internal class AlphaDecoder : IDisposable if (this.Compressed) { - Vp8LBitReader bitReader = new(data); + Vp8LBitReader bitReader = new Vp8LBitReader(data); this.LosslessDecoder = new WebpLosslessDecoder(bitReader, memoryAllocator, configuration); this.LosslessDecoder.DecodeImageStream(this.Vp8LDec, width, height, true); diff --git a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs index a18d44fde4..2084686969 100644 --- a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs +++ b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs @@ -43,17 +43,8 @@ internal static class AlphaEncoder { const WebpEncodingMethod effort = WebpEncodingMethod.Default; const int quality = 8 * (int)effort; - using Vp8LEncoder lossLessEncoder = new( - memoryAllocator, - configuration, - width, - height, - quality, - skipMetadata, - effort, - WebpTransparentColorMode.Preserve, - false, - 0); + using Vp8LEncoder lossLessEncoder = new Vp8LEncoder(memoryAllocator, configuration, width, height, quality, + skipMetadata, effort, WebpTransparentColorMode.Preserve, false, 0); // The transparency information will be stored in the green channel of the ARGB quadruplet. // The green channel is allowed extra transformation steps in the specification -- unlike the other channels, @@ -81,7 +72,7 @@ internal static class AlphaEncoder { int width = frame.Width; int height = frame.Height; - ImageFrame alphaAsFrame = new(Configuration.Default, width, height); + ImageFrame alphaAsFrame = new ImageFrame(Configuration.Default, width, height); for (int y = 0; y < height; y++) { @@ -91,7 +82,7 @@ internal static class AlphaEncoder for (int x = 0; x < width; x++) { // Leave A/R/B channels zero'd. - pixelRow[x] = new(0, alphaRow[x], 0, 0); + pixelRow[x] = new Rgba32(0, alphaRow[x], 0, 0); } } diff --git a/src/ImageSharp/Formats/Webp/AnimationFrameData.cs b/src/ImageSharp/Formats/Webp/AnimationFrameData.cs index 3400fef17d..27a1815fe3 100644 --- a/src/ImageSharp/Formats/Webp/AnimationFrameData.cs +++ b/src/ImageSharp/Formats/Webp/AnimationFrameData.cs @@ -62,7 +62,7 @@ internal struct AnimationFrameData { Span buffer = stackalloc byte[4]; - AnimationFrameData data = new() + AnimationFrameData data = new AnimationFrameData { DataSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer), diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index 923d2a69c4..81530706d6 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -399,7 +399,7 @@ internal class Vp8BitWriter : BitWriterBase int mbSize = this.enc.Mbw * this.enc.Mbh; int expectedSize = (int)((uint)mbSize * 7 / 8); - Vp8BitWriter bitWriterPartZero = new(expectedSize, this.enc); + Vp8BitWriter bitWriterPartZero = new Vp8BitWriter(expectedSize, this.enc); // Partition #0 with header and partition sizes. uint size0 = bitWriterPartZero.GeneratePartition0(); @@ -545,7 +545,7 @@ internal class Vp8BitWriter : BitWriterBase // Writes the partition #0 modes (that is: all intra modes) private void CodeIntraModes() { - Vp8EncIterator it = new(this.enc); + Vp8EncIterator it = new Vp8EncIterator(this.enc); int predsWidth = this.enc.PredsWidth; do diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs index 0b71a3ed0c..dc867fa85e 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -102,7 +102,7 @@ internal class Vp8LBitWriter : BitWriterBase { byte[] clonedBuffer = new byte[this.Buffer.Length]; System.Buffer.BlockCopy(this.Buffer, 0, clonedBuffer, 0, this.cur); - return new(clonedBuffer, this.bits, this.used, this.cur); + return new Vp8LBitWriter(clonedBuffer, this.bits, this.used, this.cur); } /// diff --git a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs index 61133142bf..03cb1990b9 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs @@ -775,7 +775,7 @@ internal static class BackwardReferenceEncoder private static void BackwardRefsWithLocalCache(ReadOnlySpan bgra, int cacheBits, Vp8LBackwardRefs refs) { int pixelIndex = 0; - ColorCache colorCache = new(cacheBits); + ColorCache colorCache = new ColorCache(cacheBits); for (int idx = 0; idx < refs.Refs.Count; idx++) { PixOrCopy v = refs.Refs[idx]; diff --git a/src/ImageSharp/Formats/Webp/Lossless/CostManager.cs b/src/ImageSharp/Formats/Webp/Lossless/CostManager.cs index e393c065ec..63ce9dbec6 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/CostManager.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/CostManager.cs @@ -17,7 +17,7 @@ internal sealed class CostManager : IDisposable private const int FreeIntervalsStartCount = 25; - private readonly Stack freeIntervals = new(FreeIntervalsStartCount); + private readonly Stack freeIntervals = new Stack(FreeIntervalsStartCount); public CostManager(MemoryAllocator memoryAllocator, IMemoryOwner distArray, int pixCount, CostModel costModel) { diff --git a/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs b/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs index 6a28e5b3fb..61804812d5 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs @@ -15,7 +15,7 @@ internal sealed class PixOrCopy public uint BgraOrDistance { get; set; } public static PixOrCopy CreateCacheIdx(int idx) => - new() + new PixOrCopy { Mode = PixOrCopyMode.CacheIdx, BgraOrDistance = (uint)idx, @@ -23,14 +23,15 @@ internal sealed class PixOrCopy }; public static PixOrCopy CreateLiteral(uint bgra) => - new() + new PixOrCopy { Mode = PixOrCopyMode.Literal, BgraOrDistance = bgra, Len = 1 }; - public static PixOrCopy CreateCopy(uint distance, ushort len) => new() + public static PixOrCopy CreateCopy(uint distance, ushort len) => + new PixOrCopy { Mode = PixOrCopyMode.Copy, BgraOrDistance = distance, diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index af472845ac..3da27229ab 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -304,7 +304,7 @@ internal class Vp8LEncoder : IDisposable if (hasAnimation) { - prevPosition = BitWriterBase.WriteAnimationFrame(stream, new() + prevPosition = BitWriterBase.WriteAnimationFrame(stream, new AnimationFrameData { Width = (uint)frame.Width, Height = (uint)frame.Height, @@ -547,7 +547,7 @@ internal class Vp8LEncoder : IDisposable EntropyIx entropyIdx = this.AnalyzeEntropy(bgra, width, height, usePalette, this.PaletteSize, this.TransformBits, out redAndBlueAlwaysZero); bool doNotCache = false; - List crunchConfigs = new(); + List crunchConfigs = new List(); if (this.method == WebpEncodingMethod.BestQuality && this.quality == 100) { @@ -641,8 +641,8 @@ internal class Vp8LEncoder : IDisposable Vp8LBackwardRefs refsTmp = this.Refs[refsBest.Equals(this.Refs[0]) ? 1 : 0]; this.bitWriter.Reset(bwInit); - Vp8LHistogram tmpHisto = new(cacheBits); - List histogramImage = new(histogramImageXySize); + Vp8LHistogram tmpHisto = new Vp8LHistogram(cacheBits); + List histogramImage = new List(histogramImageXySize); for (int i = 0; i < histogramImageXySize; i++) { histogramImage.Add(new Vp8LHistogram(cacheBits)); @@ -839,9 +839,9 @@ internal class Vp8LEncoder : IDisposable refsTmp1, refsTmp2); - List histogramImage = new() + List histogramImage = new List { - new(cacheBits) + new Vp8LHistogram(cacheBits) }; // Build histogram image and symbols from backward references. @@ -941,7 +941,7 @@ internal class Vp8LEncoder : IDisposable int i; byte[] codeLengthBitDepth = new byte[WebpConstants.CodeLengthCodes]; short[] codeLengthBitDepthSymbols = new short[WebpConstants.CodeLengthCodes]; - HuffmanTreeCode huffmanCode = new() + HuffmanTreeCode huffmanCode = new HuffmanTreeCode { NumSymbols = WebpConstants.CodeLengthCodes, CodeLengths = codeLengthBitDepth, @@ -1192,7 +1192,7 @@ internal class Vp8LEncoder : IDisposable histo[(int)HistoIx.HistoBluePred * 256]++; histo[(int)HistoIx.HistoAlphaPred * 256]++; - Vp8LBitEntropy bitEntropy = new(); + Vp8LBitEntropy bitEntropy = new Vp8LBitEntropy(); for (int j = 0; j < (int)HistoIx.HistoTotal; j++) { bitEntropy.Init(); @@ -1318,7 +1318,7 @@ internal class Vp8LEncoder : IDisposable /// The number of palette entries. private static int GetColorPalette(ReadOnlySpan bgra, int width, int height, Span palette) { - HashSet colors = new(); + HashSet colors = new HashSet(); for (int y = 0; y < height; y++) { ReadOnlySpan bgraRow = bgra.Slice(y * width, width); diff --git a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs index 19ea424199..54dd1d6ed1 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs @@ -95,7 +95,7 @@ internal sealed class WebpLosslessDecoder public void Decode(Buffer2D pixels, int width, int height) where TPixel : unmanaged, IPixel { - using (Vp8LDecoder decoder = new(width, height, this.memoryAllocator)) + using (Vp8LDecoder decoder = new Vp8LDecoder(width, height, this.memoryAllocator)) { this.DecodeImageStream(decoder, width, height, true); this.DecodeImageData(decoder, decoder.Pixels.Memory.Span); @@ -616,7 +616,7 @@ internal sealed class WebpLosslessDecoder private void ReadTransformation(int xSize, int ySize, Vp8LDecoder decoder) { Vp8LTransformType transformType = (Vp8LTransformType)this.bitReader.ReadValue(2); - Vp8LTransform transform = new(transformType, xSize, ySize); + Vp8LTransform transform = new Vp8LTransform(transformType, xSize, ySize); // Each transform is allowed to be used only once. foreach (Vp8LTransform decoderTransform in decoder.Transforms) diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs index a7c96edb7c..52c7e9703b 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs @@ -396,7 +396,7 @@ internal class Vp8EncIterator this.MakeLuma16Preds(); for (mode = 0; mode < maxMode; mode++) { - Vp8Histogram histo = new(); + Vp8Histogram histo = new Vp8Histogram(); histo.CollectHistogram(this.YuvIn.AsSpan(YOffEnc), this.YuvP.AsSpan(Vp8Encoding.Vp8I16ModeOffsets[mode]), 0, 16); int alpha = histo.GetAlpha(); if (alpha > bestAlpha) @@ -414,7 +414,7 @@ internal class Vp8EncIterator { Span modes = stackalloc byte[16]; const int maxMode = MaxIntra4Mode; - Vp8Histogram totalHisto = new(); + Vp8Histogram totalHisto = new Vp8Histogram(); int curHisto = 0; this.StartI4(); do @@ -467,7 +467,7 @@ internal class Vp8EncIterator this.MakeChroma8Preds(); for (mode = 0; mode < maxMode; ++mode) { - Vp8Histogram histo = new(); + Vp8Histogram histo = new Vp8Histogram(); histo.CollectHistogram(this.YuvIn.AsSpan(UOffEnc), this.YuvP.AsSpan(Vp8Encoding.Vp8UvModeOffsets[mode]), 16, 16 + 4 + 4); int alpha = histo.GetAlpha(); if (alpha > bestAlpha) diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 40dbb90de6..e62eb6cfc3 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -395,7 +395,7 @@ internal class Vp8Encoder : IDisposable int yStride = width; int uvStride = (yStride + 1) >> 1; - Vp8EncIterator it = new(this); + Vp8EncIterator it = new Vp8EncIterator(this); Span alphas = stackalloc int[WebpConstants.MaxAlpha + 1]; this.alpha = this.MacroBlockAnalysis(width, height, it, y, u, v, yStride, uvStride, alphas, out this.uvAlpha); int totalMb = this.Mbw * this.Mbw; @@ -416,8 +416,8 @@ internal class Vp8Encoder : IDisposable this.StatLoop(width, height, yStride, uvStride); it.Init(); Vp8EncIterator.InitFilter(); - Vp8ModeScore info = new(); - Vp8Residual residual = new(); + Vp8ModeScore info = new Vp8ModeScore(); + Vp8Residual residual = new Vp8Residual(); do { bool dontUseSkip = !this.Proba.UseSkipProba; @@ -474,7 +474,7 @@ internal class Vp8Encoder : IDisposable if (hasAnimation) { - prevPosition = BitWriterBase.WriteAnimationFrame(stream, new() + prevPosition = BitWriterBase.WriteAnimationFrame(stream, new AnimationFrameData { Width = (uint)frame.Width, Height = (uint)frame.Height, @@ -529,7 +529,7 @@ internal class Vp8Encoder : IDisposable Vp8RdLevel rdOpt = this.method >= WebpEncodingMethod.Level3 || doSearch ? Vp8RdLevel.RdOptBasic : Vp8RdLevel.RdOptNone; int nbMbs = this.Mbw * this.Mbh; - PassStats stats = new(targetSize, targetPsnr, QMin, QMax, this.quality); + PassStats stats = new PassStats(targetSize, targetPsnr, QMin, QMax, this.quality); this.Proba.ResetTokenStats(); // Fast mode: quick analysis pass over few mbs. Better than nothing. @@ -597,7 +597,7 @@ internal class Vp8Encoder : IDisposable Span y = this.Y.GetSpan(); Span u = this.U.GetSpan(); Span v = this.V.GetSpan(); - Vp8EncIterator it = new(this); + Vp8EncIterator it = new Vp8EncIterator(this); long size = 0; long sizeP0 = 0; long distortion = 0; @@ -605,7 +605,7 @@ internal class Vp8Encoder : IDisposable it.Init(); this.SetLoopParams(stats.Q); - Vp8ModeScore info = new(); + Vp8ModeScore info = new Vp8ModeScore(); do { info.Clear(); @@ -1167,7 +1167,7 @@ internal class Vp8Encoder : IDisposable private void RecordResiduals(Vp8EncIterator it, Vp8ModeScore rd) { int x, y, ch; - Vp8Residual residual = new(); + Vp8Residual residual = new Vp8Residual(); bool i16 = it.CurrentMacroBlockInfo.MacroBlockType == Vp8MacroBlockType.I16X16; it.NzToBytes(); diff --git a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs index 7952b15b44..4ac516f055 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs @@ -62,7 +62,7 @@ internal sealed class WebpLossyDecoder // Paragraph 9.2: color space and clamp type follow. sbyte colorSpace = (sbyte)this.bitReader.ReadValue(1); sbyte clampType = (sbyte)this.bitReader.ReadValue(1); - Vp8PictureHeader pictureHeader = new() + Vp8PictureHeader pictureHeader = new Vp8PictureHeader { Width = (uint)width, Height = (uint)height, @@ -73,10 +73,11 @@ internal sealed class WebpLossyDecoder }; // Paragraph 9.3: Parse the segment header. - Vp8Proba proba = new(); + Vp8Proba proba = new Vp8Proba(); Vp8SegmentHeader vp8SegmentHeader = this.ParseSegmentHeader(proba); - using (Vp8Decoder decoder = new(info.Vp8FrameHeader, pictureHeader, vp8SegmentHeader, proba, this.memoryAllocator)) + using (Vp8Decoder decoder = new Vp8Decoder(info.Vp8FrameHeader, pictureHeader, vp8SegmentHeader, proba, + this.memoryAllocator)) { Vp8Io io = InitializeVp8Io(decoder, pictureHeader); @@ -101,13 +102,8 @@ internal sealed class WebpLossyDecoder if (info.Features?.Alpha == true) { - using (AlphaDecoder alphaDecoder = new( - width, - height, - alphaData, - info.Features.AlphaChunkHeader, - this.memoryAllocator, - this.configuration)) + using (AlphaDecoder alphaDecoder = new AlphaDecoder(width, height, alphaData, + info.Features.AlphaChunkHeader, this.memoryAllocator, this.configuration)) { alphaDecoder.Decode(); DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels, alphaDecoder.Alpha); @@ -1062,7 +1058,7 @@ internal sealed class WebpLossyDecoder private Vp8SegmentHeader ParseSegmentHeader(Vp8Proba proba) { - Vp8SegmentHeader vp8SegmentHeader = new() + Vp8SegmentHeader vp8SegmentHeader = new Vp8SegmentHeader { UseSegment = this.bitReader.ReadBool() }; diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 81a7aebdf9..6922e37d6e 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -153,7 +153,7 @@ internal class WebpAnimationDecoder : IDisposable } WebpImageInfo? webpInfo = null; - WebpFeatures features = new(); + WebpFeatures features = new WebpFeatures(); switch (chunkType) { case WebpChunkType.Vp8: @@ -178,7 +178,7 @@ internal class WebpAnimationDecoder : IDisposable ImageFrame imageFrame; if (previousFrame is null) { - image = new(this.configuration, (int)width, (int)height, backgroundColor.ToPixel(), this.metadata); + image = new Image(this.configuration, (int)width, (int)height, backgroundColor.ToPixel(), this.metadata); SetFrameMetadata(image.Frames.RootFrame.Metadata, frameData.Duration); @@ -259,19 +259,21 @@ internal class WebpAnimationDecoder : IDisposable private Buffer2D DecodeImageData(AnimationFrameData frameData, WebpImageInfo webpInfo) where TPixel : unmanaged, IPixel { - Image decodedImage = new((int)frameData.Width, (int)frameData.Height); + Image decodedImage = new Image((int)frameData.Width, (int)frameData.Height); try { Buffer2D pixelBufferDecoded = decodedImage.GetRootFramePixelBuffer(); if (webpInfo.IsLossless) { - WebpLosslessDecoder losslessDecoder = new(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); + WebpLosslessDecoder losslessDecoder = + new WebpLosslessDecoder(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); losslessDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height); } else { - WebpLossyDecoder lossyDecoder = new(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration); + WebpLossyDecoder lossyDecoder = + new WebpLossyDecoder(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration); lossyDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height, webpInfo, this.alphaData); } diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationEncoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationEncoder.cs deleted file mode 100644 index bfa64b6797..0000000000 --- a/src/ImageSharp/Formats/Webp/WebpAnimationEncoder.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Webp; - -/// -/// Encoder for animated webp images. -/// -public class WebpAnimationEncoder -{ - // 可能不需要这个屌东西 -} diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs index 9e9f0f7f62..f4e40090cf 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs @@ -8,6 +8,8 @@ using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Webp; @@ -104,16 +106,16 @@ internal static class WebpChunkParsingUtils WebpThrowHelper.ThrowImageFormatException("bad partition length"); } - Vp8FrameHeader vp8FrameHeader = new() + Vp8FrameHeader vp8FrameHeader = new Vp8FrameHeader { KeyFrame = true, Profile = (sbyte)version, PartitionLength = partitionLength }; - Vp8BitReader bitReader = new(stream, remaining, memoryAllocator, partitionLength) { Remaining = remaining }; + Vp8BitReader bitReader = new Vp8BitReader(stream, remaining, memoryAllocator, partitionLength) { Remaining = remaining }; - return new() + return new WebpImageInfo { Width = width, Height = height, @@ -137,7 +139,7 @@ internal static class WebpChunkParsingUtils // VP8 data size. uint imageDataSize = ReadChunkSize(stream, buffer); - Vp8LBitReader bitReader = new(stream, imageDataSize, memoryAllocator); + Vp8LBitReader bitReader = new Vp8LBitReader(stream, imageDataSize, memoryAllocator); // One byte signature, should be 0x2f. uint signature = bitReader.ReadValue(8); @@ -166,7 +168,7 @@ internal static class WebpChunkParsingUtils WebpThrowHelper.ThrowNotSupportedException($"Unexpected version number {version} found in VP8L header"); } - return new() + return new WebpImageInfo { Width = width, Height = height, @@ -229,7 +231,7 @@ internal static class WebpChunkParsingUtils uint height = ReadUInt24LittleEndian(stream, buffer) + 1; // Read all the chunks in the order they occur. - WebpImageInfo info = new() + WebpImageInfo info = new WebpImageInfo { Width = width, Height = height, @@ -291,7 +293,7 @@ internal static class WebpChunkParsingUtils if (stream.Read(buffer) == 4) { uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer); - return (chunkSize % 2 == 0) ? chunkSize : chunkSize + 1; + return chunkSize % 2 == 0 ? chunkSize : chunkSize + 1; } throw new ImageFormatException("Invalid Webp data, could not read chunk size."); @@ -348,7 +350,7 @@ internal static class WebpChunkParsingUtils if (metadata.ExifProfile != null) { - metadata.ExifProfile = new(exifData); + metadata.ExifProfile = new ExifProfile(exifData); } break; @@ -362,7 +364,7 @@ internal static class WebpChunkParsingUtils if (metadata.XmpProfile != null) { - metadata.XmpProfile = new(xmpData); + metadata.XmpProfile = new XmpProfile(xmpData); } break; diff --git a/src/ImageSharp/Formats/Webp/WebpDecoder.cs b/src/ImageSharp/Formats/Webp/WebpDecoder.cs index e23b817ccd..dfbf4ef0e6 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoder.cs @@ -17,7 +17,7 @@ public sealed class WebpDecoder : SpecializedImageDecoder /// /// Gets the shared instance. /// - public static WebpDecoder Instance { get; } = new(); + public static WebpDecoder Instance { get; } = new WebpDecoder(); /// protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) @@ -25,7 +25,7 @@ public sealed class WebpDecoder : SpecializedImageDecoder Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); - using WebpDecoderCore decoder = new(new WebpDecoderOptions() { GeneralOptions = options }); + using WebpDecoderCore decoder = new WebpDecoderCore(new WebpDecoderOptions() { GeneralOptions = options }); return decoder.Identify(options.Configuration, stream, cancellationToken); } @@ -35,7 +35,7 @@ public sealed class WebpDecoder : SpecializedImageDecoder Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); - using WebpDecoderCore decoder = new(options); + using WebpDecoderCore decoder = new WebpDecoderCore(options); Image image = decoder.Decode(options.GeneralOptions.Configuration, stream, cancellationToken); ScaleToTargetSize(options.GeneralOptions, image); @@ -52,6 +52,5 @@ public sealed class WebpDecoder : SpecializedImageDecoder => this.Decode(options, stream, cancellationToken); /// - protected override WebpDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) - => new() { GeneralOptions = options }; + protected override WebpDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) => new WebpDecoderOptions { GeneralOptions = options }; } diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 63d3e1aead..bb54d99a04 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -8,7 +8,9 @@ using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Webp; @@ -71,7 +73,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable public DecoderOptions Options { get; } /// - public Size Dimensions => new((int)this.webImageInfo!.Width, (int)this.webImageInfo.Height); + public Size Dimensions => new Size((int)this.webImageInfo!.Width, (int)this.webImageInfo.Height); /// public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) @@ -80,7 +82,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable Image? image = null; try { - ImageMetadata metadata = new(); + ImageMetadata metadata = new ImageMetadata(); Span buffer = stackalloc byte[4]; uint fileSize = ReadImageHeader(stream, buffer); @@ -89,7 +91,8 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable { if (this.webImageInfo.Features is { Animation: true }) { - using WebpAnimationDecoder animationDecoder = new(this.memoryAllocator, this.configuration, this.maxFrames, this.backgroundColorHandling); + using WebpAnimationDecoder animationDecoder = new WebpAnimationDecoder(this.memoryAllocator, + this.configuration, this.maxFrames, this.backgroundColorHandling); return animationDecoder.Decode(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize); } @@ -97,12 +100,14 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable Buffer2D pixels = image.GetRootFramePixelBuffer(); if (this.webImageInfo.IsLossless) { - WebpLosslessDecoder losslessDecoder = new(this.webImageInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); + WebpLosslessDecoder losslessDecoder = new WebpLosslessDecoder(this.webImageInfo.Vp8LBitReader, + this.memoryAllocator, this.configuration); losslessDecoder.Decode(pixels, image.Width, image.Height); } else { - WebpLossyDecoder lossyDecoder = new(this.webImageInfo.Vp8BitReader, this.memoryAllocator, this.configuration); + WebpLossyDecoder lossyDecoder = new WebpLossyDecoder(this.webImageInfo.Vp8BitReader, + this.memoryAllocator, this.configuration); lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo, this.alphaData); } @@ -127,12 +132,12 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable { ReadImageHeader(stream, stackalloc byte[4]); - ImageMetadata metadata = new(); + ImageMetadata metadata = new ImageMetadata(); using (this.webImageInfo = this.ReadVp8Info(stream, metadata, true)) { return new ImageInfo( new PixelTypeInfo((int)this.webImageInfo.BitsPerPixel), - new((int)this.webImageInfo.Width, (int)this.webImageInfo.Height), + new Size((int)this.webImageInfo.Width, (int)this.webImageInfo.Height), metadata); } } @@ -173,7 +178,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable Span buffer = stackalloc byte[4]; WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); - WebpFeatures features = new(); + WebpFeatures features = new WebpFeatures(); switch (chunkType) { case WebpChunkType.Vp8: @@ -327,7 +332,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable return; } - metadata.ExifProfile = new(exifData); + metadata.ExifProfile = new ExifProfile(exifData); } } @@ -354,7 +359,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable return; } - metadata.XmpProfile = new(xmpData); + metadata.XmpProfile = new XmpProfile(xmpData); } } @@ -380,7 +385,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the iccp chunk"); } - IccProfile profile = new(iccpData); + IccProfile profile = new IccProfile(iccpData); if (profile.CheckIsValid()) { metadata.IccProfile = profile; diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs b/src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs index 6fb15acbb4..8840805b1f 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Webp; public sealed class WebpDecoderOptions : ISpecializedDecoderOptions { /// - public DecoderOptions GeneralOptions { get; init; } = new(); + public DecoderOptions GeneralOptions { get; init; } = new DecoderOptions(); /// /// Gets the flag to decide how to handle the background color Animation Chunk. diff --git a/src/ImageSharp/Formats/Webp/WebpEncoder.cs b/src/ImageSharp/Formats/Webp/WebpEncoder.cs index 29d0c9e3b0..13c9798dbb 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoder.cs @@ -82,7 +82,7 @@ public sealed class WebpEncoder : ImageEncoder /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { - WebpEncoderCore encoder = new(this, image.Configuration); + WebpEncoderCore encoder = new WebpEncoderCore(this, image.Configuration); encoder.Encode(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index 47712071bf..dcff53f3af 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -129,17 +129,9 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals if (lossless) { - using Vp8LEncoder encoder = new( - this.memoryAllocator, - this.configuration, - image.Width, - image.Height, - this.quality, - this.skipMetadata, - this.method, - this.transparentColorMode, - this.nearLossless, - this.nearLosslessQuality); + using Vp8LEncoder encoder = new Vp8LEncoder(this.memoryAllocator, this.configuration, image.Width, + image.Height, this.quality, this.skipMetadata, this.method, this.transparentColorMode, + this.nearLossless, this.nearLosslessQuality); bool hasAnimation = image.Frames.Count > 1; encoder.EncodeHeader(image, stream, hasAnimation); @@ -147,17 +139,9 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals { foreach (ImageFrame imageFrame in image.Frames) { - using Vp8LEncoder enc = new( - this.memoryAllocator, - this.configuration, - image.Width, - image.Height, - this.quality, - this.skipMetadata, - this.method, - this.transparentColorMode, - this.nearLossless, - this.nearLosslessQuality); + using Vp8LEncoder enc = new Vp8LEncoder(this.memoryAllocator, this.configuration, image.Width, + image.Height, this.quality, this.skipMetadata, this.method, this.transparentColorMode, + this.nearLossless, this.nearLosslessQuality); enc.Encode(imageFrame, stream, true); } @@ -171,36 +155,18 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals } else { - using Vp8Encoder encoder = new( - this.memoryAllocator, - this.configuration, - image.Width, - image.Height, - this.quality, - this.skipMetadata, - this.method, - this.entropyPasses, - this.filterStrength, - this.spatialNoiseShaping, - this.alphaCompression); + using Vp8Encoder encoder = new Vp8Encoder(this.memoryAllocator, this.configuration, image.Width, + image.Height, this.quality, this.skipMetadata, this.method, this.entropyPasses, this.filterStrength, + this.spatialNoiseShaping, this.alphaCompression); if (image.Frames.Count > 1) { encoder.EncodeHeader(image, stream, false, true); foreach (ImageFrame imageFrame in image.Frames) { - using Vp8Encoder enc = new( - this.memoryAllocator, - this.configuration, - image.Width, - image.Height, - this.quality, - this.skipMetadata, - this.method, - this.entropyPasses, - this.filterStrength, - this.spatialNoiseShaping, - this.alphaCompression); + using Vp8Encoder enc = new Vp8Encoder(this.memoryAllocator, this.configuration, image.Width, + image.Height, this.quality, this.skipMetadata, this.method, this.entropyPasses, + this.filterStrength, this.spatialNoiseShaping, this.alphaCompression); enc.EncodeAnimation(imageFrame, stream); } diff --git a/src/ImageSharp/Formats/Webp/WebpFormat.cs b/src/ImageSharp/Formats/Webp/WebpFormat.cs index 29c74b11bf..197041234e 100644 --- a/src/ImageSharp/Formats/Webp/WebpFormat.cs +++ b/src/ImageSharp/Formats/Webp/WebpFormat.cs @@ -15,7 +15,7 @@ public sealed class WebpFormat : IImageFormat /// /// Gets the shared instance. /// - public static WebpFormat Instance { get; } = new(); + public static WebpFormat Instance { get; } = new WebpFormat(); /// public string Name => "Webp"; @@ -30,8 +30,8 @@ public sealed class WebpFormat : IImageFormat public IEnumerable FileExtensions => WebpConstants.FileExtensions; /// - public WebpMetadata CreateDefaultFormatMetadata() => new(); + public WebpMetadata CreateDefaultFormatMetadata() => new WebpMetadata(); /// - public WebpFrameMetadata CreateDefaultFormatFrameMetadata() => new(); + public WebpFrameMetadata CreateDefaultFormatFrameMetadata() => new WebpFrameMetadata(); } From d4483217b623aa751f5591d7edc93a7c812a91b2 Mon Sep 17 00:00:00 2001 From: Poker Date: Wed, 25 Oct 2023 19:48:07 +0800 Subject: [PATCH 16/24] fix SA1117 --- src/ImageSharp/Formats/Webp/AlphaEncoder.cs | 13 ++++- .../Formats/Webp/Lossy/WebpLossyDecoder.cs | 15 ++++- .../Formats/Webp/WebpDecoderCore.cs | 19 ++++-- .../Formats/Webp/WebpEncoderCore.cs | 58 +++++++++++++++---- 4 files changed, 82 insertions(+), 23 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs index 2084686969..cbd2aa8e7f 100644 --- a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs +++ b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs @@ -43,8 +43,17 @@ internal static class AlphaEncoder { const WebpEncodingMethod effort = WebpEncodingMethod.Default; const int quality = 8 * (int)effort; - using Vp8LEncoder lossLessEncoder = new Vp8LEncoder(memoryAllocator, configuration, width, height, quality, - skipMetadata, effort, WebpTransparentColorMode.Preserve, false, 0); + using Vp8LEncoder lossLessEncoder = new( + memoryAllocator, + configuration, + width, + height, + quality, + skipMetadata, + effort, + WebpTransparentColorMode.Preserve, + false, + 0); // The transparency information will be stored in the green channel of the ARGB quadruplet. // The green channel is allowed extra transformation steps in the specification -- unlike the other channels, diff --git a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs index 4ac516f055..354bcdbb44 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs @@ -76,7 +76,11 @@ internal sealed class WebpLossyDecoder Vp8Proba proba = new Vp8Proba(); Vp8SegmentHeader vp8SegmentHeader = this.ParseSegmentHeader(proba); - using (Vp8Decoder decoder = new Vp8Decoder(info.Vp8FrameHeader, pictureHeader, vp8SegmentHeader, proba, + using (Vp8Decoder decoder = new Vp8Decoder( + info.Vp8FrameHeader, + pictureHeader, + vp8SegmentHeader, + proba, this.memoryAllocator)) { Vp8Io io = InitializeVp8Io(decoder, pictureHeader); @@ -102,8 +106,13 @@ internal sealed class WebpLossyDecoder if (info.Features?.Alpha == true) { - using (AlphaDecoder alphaDecoder = new AlphaDecoder(width, height, alphaData, - info.Features.AlphaChunkHeader, this.memoryAllocator, this.configuration)) + using (AlphaDecoder alphaDecoder = new AlphaDecoder( + width, + height, + alphaData, + info.Features.AlphaChunkHeader, + this.memoryAllocator, + this.configuration)) { alphaDecoder.Decode(); DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels, alphaDecoder.Alpha); diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index bb54d99a04..bc875c8890 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -91,8 +91,11 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable { if (this.webImageInfo.Features is { Animation: true }) { - using WebpAnimationDecoder animationDecoder = new WebpAnimationDecoder(this.memoryAllocator, - this.configuration, this.maxFrames, this.backgroundColorHandling); + using WebpAnimationDecoder animationDecoder = new WebpAnimationDecoder( + this.memoryAllocator, + this.configuration, + this.maxFrames, + this.backgroundColorHandling); return animationDecoder.Decode(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize); } @@ -100,14 +103,18 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable Buffer2D pixels = image.GetRootFramePixelBuffer(); if (this.webImageInfo.IsLossless) { - WebpLosslessDecoder losslessDecoder = new WebpLosslessDecoder(this.webImageInfo.Vp8LBitReader, - this.memoryAllocator, this.configuration); + WebpLosslessDecoder losslessDecoder = new WebpLosslessDecoder( + this.webImageInfo.Vp8LBitReader, + this.memoryAllocator, + this.configuration); losslessDecoder.Decode(pixels, image.Width, image.Height); } else { - WebpLossyDecoder lossyDecoder = new WebpLossyDecoder(this.webImageInfo.Vp8BitReader, - this.memoryAllocator, this.configuration); + WebpLossyDecoder lossyDecoder = new WebpLossyDecoder( + this.webImageInfo.Vp8BitReader, + this.memoryAllocator, + this.configuration); lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo, this.alphaData); } diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index dcff53f3af..d945cc3990 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -129,9 +129,17 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals if (lossless) { - using Vp8LEncoder encoder = new Vp8LEncoder(this.memoryAllocator, this.configuration, image.Width, - image.Height, this.quality, this.skipMetadata, this.method, this.transparentColorMode, - this.nearLossless, this.nearLosslessQuality); + using Vp8LEncoder encoder = new Vp8LEncoder( + this.memoryAllocator, + this.configuration, + image.Width, + image.Height, + this.quality, + this.skipMetadata, + this.method, + this.transparentColorMode, + this.nearLossless, + this.nearLosslessQuality); bool hasAnimation = image.Frames.Count > 1; encoder.EncodeHeader(image, stream, hasAnimation); @@ -139,9 +147,17 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals { foreach (ImageFrame imageFrame in image.Frames) { - using Vp8LEncoder enc = new Vp8LEncoder(this.memoryAllocator, this.configuration, image.Width, - image.Height, this.quality, this.skipMetadata, this.method, this.transparentColorMode, - this.nearLossless, this.nearLosslessQuality); + using Vp8LEncoder enc = new Vp8LEncoder( + this.memoryAllocator, + this.configuration, + image.Width, + image.Height, + this.quality, + this.skipMetadata, + this.method, + this.transparentColorMode, + this.nearLossless, + this.nearLosslessQuality); enc.Encode(imageFrame, stream, true); } @@ -155,18 +171,36 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals } else { - using Vp8Encoder encoder = new Vp8Encoder(this.memoryAllocator, this.configuration, image.Width, - image.Height, this.quality, this.skipMetadata, this.method, this.entropyPasses, this.filterStrength, - this.spatialNoiseShaping, this.alphaCompression); + using Vp8Encoder encoder = new Vp8Encoder( + this.memoryAllocator, + this.configuration, + image.Width, + image.Height, + this.quality, + this.skipMetadata, + this.method, + this.entropyPasses, + this.filterStrength, + this.spatialNoiseShaping, + this.alphaCompression); if (image.Frames.Count > 1) { encoder.EncodeHeader(image, stream, false, true); foreach (ImageFrame imageFrame in image.Frames) { - using Vp8Encoder enc = new Vp8Encoder(this.memoryAllocator, this.configuration, image.Width, - image.Height, this.quality, this.skipMetadata, this.method, this.entropyPasses, - this.filterStrength, this.spatialNoiseShaping, this.alphaCompression); + using Vp8Encoder enc = new Vp8Encoder( + this.memoryAllocator, + this.configuration, + image.Width, + image.Height, + this.quality, + this.skipMetadata, + this.method, + this.entropyPasses, + this.filterStrength, + this.spatialNoiseShaping, + this.alphaCompression); enc.EncodeAnimation(imageFrame, stream); } From b89bc54aa20b7868010d34734bd1a6ea88be92a2 Mon Sep 17 00:00:00 2001 From: Poker Date: Tue, 31 Oct 2023 18:27:35 +0800 Subject: [PATCH 17/24] Add new member to WebpFrameMetadata --- .../Formats/Webp/BitWriter/BitWriterBase.cs | 8 +++-- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 30 +++++++++++-------- .../Formats/Webp/Lossy/Vp8Encoder.cs | 26 +++++++++------- .../Formats/Webp/WebpAnimationDecoder.cs | 29 +++++++++--------- ...lendingMethod.cs => WebpBlendingMethod.cs} | 2 +- .../Formats/Webp/WebpDecoderCore.cs | 16 +++++----- ...isposalMethod.cs => WebpDisposalMethod.cs} | 2 +- src/ImageSharp/Formats/Webp/WebpEncoder.cs | 4 +-- .../Formats/Webp/WebpEncoderCore.cs | 8 ++--- ...AnimationFrameData.cs => WebpFrameData.cs} | 14 ++++----- .../Formats/Webp/WebpFrameMetadata.cs | 19 ++++++++++-- src/ImageSharp/Formats/Webp/WebpMetadata.cs | 9 ++++++ .../Formats/WebP/WebpDecoderTests.cs | 4 +-- 13 files changed, 102 insertions(+), 69 deletions(-) rename src/ImageSharp/Formats/Webp/{AnimationBlendingMethod.cs => WebpBlendingMethod.cs} (95%) rename src/ImageSharp/Formats/Webp/{AnimationDisposalMethod.cs => WebpDisposalMethod.cs} (94%) rename src/ImageSharp/Formats/Webp/{AnimationFrameData.cs => WebpFrameData.cs} (83%) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index 89db7ed645..c1860c9c59 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -231,14 +231,14 @@ internal abstract class BitWriterBase /// The background color is also used when the Disposal method is 1. /// /// The number of times to loop the animation. If it is 0, this means infinitely. - public static void WriteAnimationParameter(Stream stream, uint background, ushort loopCount) + public static void WriteAnimationParameter(Stream stream, Color background, ushort loopCount) { Span buf = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.AnimationParameter); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, sizeof(uint) + sizeof(ushort)); stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, background); + BinaryPrimitives.WriteUInt32LittleEndian(buf, background.ToRgba32().Rgba); stream.Write(buf); BinaryPrimitives.WriteUInt16LittleEndian(buf[..2], loopCount); stream.Write(buf[..2]); @@ -249,7 +249,7 @@ internal abstract class BitWriterBase /// /// The stream to write to. /// Animation frame data. - public static long WriteAnimationFrame(Stream stream, AnimationFrameData animation) + public static long WriteAnimationFrame(Stream stream, WebpFrameData animation) { Span buf = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Animation); @@ -262,6 +262,8 @@ internal abstract class BitWriterBase WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Width - 1); WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Height - 1); WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Duration); + + // TODO: If we can clip the indexed frame for transparent bounds we can set properties here. byte flag = (byte)(((int)animation.BlendingMethod << 1) | (int)animation.DisposalMethod); stream.WriteByte(flag); return position; diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 3da27229ab..9156d5bdf8 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -235,7 +235,7 @@ internal class Vp8LEncoder : IDisposable /// public Vp8LHashChain HashChain { get; } - public void EncodeHeader(Image image, Stream stream, bool hasAnimation, uint background = 0, uint loopCount = 0) + public void EncodeHeader(Image image, Stream stream, bool hasAnimation) where TPixel : unmanaged, IPixel { // Write bytes from the bitwriter buffer to the stream. @@ -257,7 +257,8 @@ internal class Vp8LEncoder : IDisposable if (hasAnimation) { - BitWriterBase.WriteAnimationParameter(stream, background, (ushort)loopCount); + WebpMetadata webpMetadata = metadata.GetWebpMetadata(); + BitWriterBase.WriteAnimationParameter(stream, webpMetadata.AnimationBackground, webpMetadata.AnimationLoopCount); } } @@ -304,11 +305,14 @@ internal class Vp8LEncoder : IDisposable if (hasAnimation) { - prevPosition = BitWriterBase.WriteAnimationFrame(stream, new AnimationFrameData + WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata(); + prevPosition = BitWriterBase.WriteAnimationFrame(stream, new WebpFrameData { Width = (uint)frame.Width, Height = (uint)frame.Height, - Duration = frame.Metadata.GetWebpMetadata().FrameDuration + Duration = frameMetadata.FrameDelay, + BlendingMethod = frameMetadata.BlendMethod, + DisposalMethod = frameMetadata.DisposalMethod }); } @@ -547,7 +551,7 @@ internal class Vp8LEncoder : IDisposable EntropyIx entropyIdx = this.AnalyzeEntropy(bgra, width, height, usePalette, this.PaletteSize, this.TransformBits, out redAndBlueAlwaysZero); bool doNotCache = false; - List crunchConfigs = new List(); + List crunchConfigs = new(); if (this.method == WebpEncodingMethod.BestQuality && this.quality == 100) { @@ -641,8 +645,8 @@ internal class Vp8LEncoder : IDisposable Vp8LBackwardRefs refsTmp = this.Refs[refsBest.Equals(this.Refs[0]) ? 1 : 0]; this.bitWriter.Reset(bwInit); - Vp8LHistogram tmpHisto = new Vp8LHistogram(cacheBits); - List histogramImage = new List(histogramImageXySize); + Vp8LHistogram tmpHisto = new(cacheBits); + List histogramImage = new(histogramImageXySize); for (int i = 0; i < histogramImageXySize; i++) { histogramImage.Add(new Vp8LHistogram(cacheBits)); @@ -839,7 +843,7 @@ internal class Vp8LEncoder : IDisposable refsTmp1, refsTmp2); - List histogramImage = new List + List histogramImage = new() { new Vp8LHistogram(cacheBits) }; @@ -941,7 +945,7 @@ internal class Vp8LEncoder : IDisposable int i; byte[] codeLengthBitDepth = new byte[WebpConstants.CodeLengthCodes]; short[] codeLengthBitDepthSymbols = new short[WebpConstants.CodeLengthCodes]; - HuffmanTreeCode huffmanCode = new HuffmanTreeCode + HuffmanTreeCode huffmanCode = new() { NumSymbols = WebpConstants.CodeLengthCodes, CodeLengths = codeLengthBitDepth, @@ -1192,7 +1196,7 @@ internal class Vp8LEncoder : IDisposable histo[(int)HistoIx.HistoBluePred * 256]++; histo[(int)HistoIx.HistoAlphaPred * 256]++; - Vp8LBitEntropy bitEntropy = new Vp8LBitEntropy(); + Vp8LBitEntropy bitEntropy = new(); for (int j = 0; j < (int)HistoIx.HistoTotal; j++) { bitEntropy.Init(); @@ -1318,7 +1322,7 @@ internal class Vp8LEncoder : IDisposable /// The number of palette entries. private static int GetColorPalette(ReadOnlySpan bgra, int width, int height, Span palette) { - HashSet colors = new HashSet(); + HashSet colors = new(); for (int y = 0; y < height; y++) { ReadOnlySpan bgraRow = bgra.Slice(y * width, width); @@ -1870,9 +1874,9 @@ internal class Vp8LEncoder : IDisposable /// public void ClearRefs() { - for (int i = 0; i < this.Refs.Length; i++) + foreach (Vp8LBackwardRefs t in this.Refs) { - this.Refs[i].Refs.Clear(); + t.Refs.Clear(); } } diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index e62eb6cfc3..3a6e9a2ccd 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -309,7 +309,7 @@ internal class Vp8Encoder : IDisposable /// private int MbHeaderLimit { get; } - public void EncodeHeader(Image image, Stream stream, bool hasAlpha, bool hasAnimation, uint background = 0, uint loopCount = 0) + public void EncodeHeader(Image image, Stream stream, bool hasAlpha, bool hasAnimation) where TPixel : unmanaged, IPixel { // Write bytes from the bitwriter buffer to the stream. @@ -331,7 +331,8 @@ internal class Vp8Encoder : IDisposable if (hasAnimation) { - BitWriterBase.WriteAnimationParameter(stream, background, (ushort)loopCount); + WebpMetadata webpMetadata = metadata.GetWebpMetadata(); + BitWriterBase.WriteAnimationParameter(stream, webpMetadata.AnimationBackground, webpMetadata.AnimationLoopCount); } } @@ -395,7 +396,7 @@ internal class Vp8Encoder : IDisposable int yStride = width; int uvStride = (yStride + 1) >> 1; - Vp8EncIterator it = new Vp8EncIterator(this); + Vp8EncIterator it = new(this); Span alphas = stackalloc int[WebpConstants.MaxAlpha + 1]; this.alpha = this.MacroBlockAnalysis(width, height, it, y, u, v, yStride, uvStride, alphas, out this.uvAlpha); int totalMb = this.Mbw * this.Mbw; @@ -416,8 +417,8 @@ internal class Vp8Encoder : IDisposable this.StatLoop(width, height, yStride, uvStride); it.Init(); Vp8EncIterator.InitFilter(); - Vp8ModeScore info = new Vp8ModeScore(); - Vp8Residual residual = new Vp8Residual(); + Vp8ModeScore info = new(); + Vp8Residual residual = new(); do { bool dontUseSkip = !this.Proba.UseSkipProba; @@ -474,11 +475,14 @@ internal class Vp8Encoder : IDisposable if (hasAnimation) { - prevPosition = BitWriterBase.WriteAnimationFrame(stream, new AnimationFrameData + WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata(); + prevPosition = BitWriterBase.WriteAnimationFrame(stream, new WebpFrameData { Width = (uint)frame.Width, Height = (uint)frame.Height, - Duration = frame.Metadata.GetWebpMetadata().FrameDuration + Duration = frameMetadata.FrameDelay, + BlendingMethod = frameMetadata.BlendMethod, + DisposalMethod = frameMetadata.DisposalMethod }); } @@ -529,7 +533,7 @@ internal class Vp8Encoder : IDisposable Vp8RdLevel rdOpt = this.method >= WebpEncodingMethod.Level3 || doSearch ? Vp8RdLevel.RdOptBasic : Vp8RdLevel.RdOptNone; int nbMbs = this.Mbw * this.Mbh; - PassStats stats = new PassStats(targetSize, targetPsnr, QMin, QMax, this.quality); + PassStats stats = new(targetSize, targetPsnr, QMin, QMax, this.quality); this.Proba.ResetTokenStats(); // Fast mode: quick analysis pass over few mbs. Better than nothing. @@ -597,7 +601,7 @@ internal class Vp8Encoder : IDisposable Span y = this.Y.GetSpan(); Span u = this.U.GetSpan(); Span v = this.V.GetSpan(); - Vp8EncIterator it = new Vp8EncIterator(this); + Vp8EncIterator it = new(this); long size = 0; long sizeP0 = 0; long distortion = 0; @@ -605,7 +609,7 @@ internal class Vp8Encoder : IDisposable it.Init(); this.SetLoopParams(stats.Q); - Vp8ModeScore info = new Vp8ModeScore(); + Vp8ModeScore info = new(); do { info.Clear(); @@ -1167,7 +1171,7 @@ internal class Vp8Encoder : IDisposable private void RecordResiduals(Vp8EncIterator it, Vp8ModeScore rd) { int x, y, ch; - Vp8Residual residual = new Vp8Residual(); + Vp8Residual residual = new(); bool i16 = it.CurrentMacroBlockInfo.MacroBlockType == Vp8MacroBlockType.I16X16; it.NzToBytes(); diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 6922e37d6e..87657dfabb 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -138,7 +138,7 @@ internal class WebpAnimationDecoder : IDisposable private uint ReadFrame(BufferedReadStream stream, ref Image? image, ref ImageFrame? previousFrame, uint width, uint height, Color backgroundColor) where TPixel : unmanaged, IPixel { - AnimationFrameData frameData = AnimationFrameData.Parse(stream); + WebpFrameData frameData = WebpFrameData.Parse(stream); long streamStartPosition = stream.Position; Span buffer = stackalloc byte[4]; @@ -153,7 +153,7 @@ internal class WebpAnimationDecoder : IDisposable } WebpImageInfo? webpInfo = null; - WebpFeatures features = new WebpFeatures(); + WebpFeatures features = new(); switch (chunkType) { case WebpChunkType.Vp8: @@ -180,7 +180,7 @@ internal class WebpAnimationDecoder : IDisposable { image = new Image(this.configuration, (int)width, (int)height, backgroundColor.ToPixel(), this.metadata); - SetFrameMetadata(image.Frames.RootFrame.Metadata, frameData.Duration); + SetFrameMetadata(image.Frames.RootFrame.Metadata, frameData); imageFrame = image.Frames.RootFrame; } @@ -188,7 +188,7 @@ internal class WebpAnimationDecoder : IDisposable { currentFrame = image!.Frames.AddFrame(previousFrame); // This clones the frame and adds it the collection. - SetFrameMetadata(currentFrame.Metadata, frameData.Duration); + SetFrameMetadata(currentFrame.Metadata, frameData); imageFrame = currentFrame; } @@ -199,7 +199,7 @@ internal class WebpAnimationDecoder : IDisposable int frameHeight = (int)frameData.Height; Rectangle regionRectangle = Rectangle.FromLTRB(frameX, frameY, frameX + frameWidth, frameY + frameHeight); - if (frameData.DisposalMethod is AnimationDisposalMethod.Dispose) + if (frameData.DisposalMethod is WebpDisposalMethod.Dispose) { this.RestoreToBackground(imageFrame, backgroundColor); } @@ -207,7 +207,7 @@ internal class WebpAnimationDecoder : IDisposable using Buffer2D decodedImage = this.DecodeImageData(frameData, webpInfo); DrawDecodedImageOnCanvas(decodedImage, imageFrame, frameX, frameY, frameWidth, frameHeight); - if (previousFrame != null && frameData.BlendingMethod is AnimationBlendingMethod.AlphaBlending) + if (previousFrame != null && frameData.BlendingMethod is WebpBlendingMethod.AlphaBlending) { this.AlphaBlend(previousFrame, imageFrame, frameX, frameY, frameWidth, frameHeight); } @@ -222,12 +222,13 @@ internal class WebpAnimationDecoder : IDisposable /// Sets the frames metadata. /// /// The metadata. - /// The frame duration. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SetFrameMetadata(ImageFrameMetadata meta, uint duration) + /// The frame data. + private static void SetFrameMetadata(ImageFrameMetadata meta, WebpFrameData frameData) { WebpFrameMetadata frameMetadata = meta.GetWebpMetadata(); - frameMetadata.FrameDuration = duration; + frameMetadata.FrameDelay = frameData.Duration; + frameMetadata.BlendMethod = frameData.BlendingMethod; + frameMetadata.DisposalMethod = frameData.DisposalMethod; } /// @@ -256,10 +257,10 @@ internal class WebpAnimationDecoder : IDisposable /// The frame data. /// The webp information. /// A decoded image. - private Buffer2D DecodeImageData(AnimationFrameData frameData, WebpImageInfo webpInfo) + private Buffer2D DecodeImageData(WebpFrameData frameData, WebpImageInfo webpInfo) where TPixel : unmanaged, IPixel { - Image decodedImage = new Image((int)frameData.Width, (int)frameData.Height); + Image decodedImage = new((int)frameData.Width, (int)frameData.Height); try { @@ -267,13 +268,13 @@ internal class WebpAnimationDecoder : IDisposable if (webpInfo.IsLossless) { WebpLosslessDecoder losslessDecoder = - new WebpLosslessDecoder(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); + new(webpInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); losslessDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height); } else { WebpLossyDecoder lossyDecoder = - new WebpLossyDecoder(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration); + new(webpInfo.Vp8BitReader, this.memoryAllocator, this.configuration); lossyDecoder.Decode(pixelBufferDecoded, (int)webpInfo.Width, (int)webpInfo.Height, webpInfo, this.alphaData); } diff --git a/src/ImageSharp/Formats/Webp/AnimationBlendingMethod.cs b/src/ImageSharp/Formats/Webp/WebpBlendingMethod.cs similarity index 95% rename from src/ImageSharp/Formats/Webp/AnimationBlendingMethod.cs rename to src/ImageSharp/Formats/Webp/WebpBlendingMethod.cs index 99b2462cea..cbd0e9a8cc 100644 --- a/src/ImageSharp/Formats/Webp/AnimationBlendingMethod.cs +++ b/src/ImageSharp/Formats/Webp/WebpBlendingMethod.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Webp; /// /// Indicates how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas. /// -internal enum AnimationBlendingMethod +public enum WebpBlendingMethod { /// /// Use alpha blending. After disposing of the previous frame, render the current frame on the canvas using alpha-blending. diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index bc875c8890..de188b137b 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -73,7 +73,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable public DecoderOptions Options { get; } /// - public Size Dimensions => new Size((int)this.webImageInfo!.Width, (int)this.webImageInfo.Height); + public Size Dimensions => new((int)this.webImageInfo!.Width, (int)this.webImageInfo.Height); /// public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) @@ -82,7 +82,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable Image? image = null; try { - ImageMetadata metadata = new ImageMetadata(); + ImageMetadata metadata = new(); Span buffer = stackalloc byte[4]; uint fileSize = ReadImageHeader(stream, buffer); @@ -91,7 +91,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable { if (this.webImageInfo.Features is { Animation: true }) { - using WebpAnimationDecoder animationDecoder = new WebpAnimationDecoder( + using WebpAnimationDecoder animationDecoder = new( this.memoryAllocator, this.configuration, this.maxFrames, @@ -103,7 +103,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable Buffer2D pixels = image.GetRootFramePixelBuffer(); if (this.webImageInfo.IsLossless) { - WebpLosslessDecoder losslessDecoder = new WebpLosslessDecoder( + WebpLosslessDecoder losslessDecoder = new( this.webImageInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); @@ -111,7 +111,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable } else { - WebpLossyDecoder lossyDecoder = new WebpLossyDecoder( + WebpLossyDecoder lossyDecoder = new( this.webImageInfo.Vp8BitReader, this.memoryAllocator, this.configuration); @@ -139,7 +139,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable { ReadImageHeader(stream, stackalloc byte[4]); - ImageMetadata metadata = new ImageMetadata(); + ImageMetadata metadata = new(); using (this.webImageInfo = this.ReadVp8Info(stream, metadata, true)) { return new ImageInfo( @@ -185,7 +185,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable Span buffer = stackalloc byte[4]; WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); - WebpFeatures features = new WebpFeatures(); + WebpFeatures features = new(); switch (chunkType) { case WebpChunkType.Vp8: @@ -392,7 +392,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the iccp chunk"); } - IccProfile profile = new IccProfile(iccpData); + IccProfile profile = new(iccpData); if (profile.CheckIsValid()) { metadata.IccProfile = profile; diff --git a/src/ImageSharp/Formats/Webp/AnimationDisposalMethod.cs b/src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs similarity index 94% rename from src/ImageSharp/Formats/Webp/AnimationDisposalMethod.cs rename to src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs index 23bc37c283..d409973a99 100644 --- a/src/ImageSharp/Formats/Webp/AnimationDisposalMethod.cs +++ b/src/ImageSharp/Formats/Webp/WebpDisposalMethod.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Webp; /// /// Indicates how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. /// -internal enum AnimationDisposalMethod +public enum WebpDisposalMethod { /// /// Do not dispose. Leave the canvas as is. diff --git a/src/ImageSharp/Formats/Webp/WebpEncoder.cs b/src/ImageSharp/Formats/Webp/WebpEncoder.cs index 13c9798dbb..bc93df3a5b 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoder.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Advanced; - namespace SixLabors.ImageSharp.Formats.Webp; /// @@ -82,7 +80,7 @@ public sealed class WebpEncoder : ImageEncoder /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { - WebpEncoderCore encoder = new WebpEncoderCore(this, image.Configuration); + WebpEncoderCore encoder = new(this, image.Configuration); encoder.Encode(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index d945cc3990..47712071bf 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -129,7 +129,7 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals if (lossless) { - using Vp8LEncoder encoder = new Vp8LEncoder( + using Vp8LEncoder encoder = new( this.memoryAllocator, this.configuration, image.Width, @@ -147,7 +147,7 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals { foreach (ImageFrame imageFrame in image.Frames) { - using Vp8LEncoder enc = new Vp8LEncoder( + using Vp8LEncoder enc = new( this.memoryAllocator, this.configuration, image.Width, @@ -171,7 +171,7 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals } else { - using Vp8Encoder encoder = new Vp8Encoder( + using Vp8Encoder encoder = new( this.memoryAllocator, this.configuration, image.Width, @@ -189,7 +189,7 @@ internal sealed class WebpEncoderCore : IImageEncoderInternals foreach (ImageFrame imageFrame in image.Frames) { - using Vp8Encoder enc = new Vp8Encoder( + using Vp8Encoder enc = new( this.memoryAllocator, this.configuration, image.Width, diff --git a/src/ImageSharp/Formats/Webp/AnimationFrameData.cs b/src/ImageSharp/Formats/Webp/WebpFrameData.cs similarity index 83% rename from src/ImageSharp/Formats/Webp/AnimationFrameData.cs rename to src/ImageSharp/Formats/Webp/WebpFrameData.cs index 27a1815fe3..e2bcfd7c36 100644 --- a/src/ImageSharp/Formats/Webp/AnimationFrameData.cs +++ b/src/ImageSharp/Formats/Webp/WebpFrameData.cs @@ -5,7 +5,7 @@ using SixLabors.ImageSharp.IO; namespace SixLabors.ImageSharp.Formats.Webp; -internal struct AnimationFrameData +internal struct WebpFrameData { /// /// The animation chunk size. @@ -46,23 +46,23 @@ internal struct AnimationFrameData /// /// Indicates how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas. /// - public AnimationBlendingMethod BlendingMethod; + public WebpBlendingMethod BlendingMethod; /// /// Indicates how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. /// - public AnimationDisposalMethod DisposalMethod; + public WebpDisposalMethod DisposalMethod; /// /// Reads the animation frame header. /// /// The stream to read from. /// Animation frame data. - public static AnimationFrameData Parse(BufferedReadStream stream) + public static WebpFrameData Parse(BufferedReadStream stream) { Span buffer = stackalloc byte[4]; - AnimationFrameData data = new AnimationFrameData + WebpFrameData data = new() { DataSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer), @@ -83,8 +83,8 @@ internal struct AnimationFrameData }; byte flags = (byte)stream.ReadByte(); - data.DisposalMethod = (flags & 1) == 1 ? AnimationDisposalMethod.Dispose : AnimationDisposalMethod.DoNotDispose; - data.BlendingMethod = (flags & (1 << 1)) != 0 ? AnimationBlendingMethod.DoNotBlend : AnimationBlendingMethod.AlphaBlending; + data.DisposalMethod = (flags & 1) == 1 ? WebpDisposalMethod.Dispose : WebpDisposalMethod.DoNotDispose; + data.BlendingMethod = (flags & (1 << 1)) != 0 ? WebpBlendingMethod.DoNotBlend : WebpBlendingMethod.AlphaBlending; return data; } diff --git a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs index bce1b09d6f..ef21d8b6fe 100644 --- a/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs +++ b/src/ImageSharp/Formats/Webp/WebpFrameMetadata.cs @@ -19,13 +19,28 @@ public class WebpFrameMetadata : IDeepCloneable /// Initializes a new instance of the class. /// /// The metadata to create an instance from. - private WebpFrameMetadata(WebpFrameMetadata other) => this.FrameDuration = other.FrameDuration; + private WebpFrameMetadata(WebpFrameMetadata other) + { + this.FrameDelay = other.FrameDelay; + this.DisposalMethod = other.DisposalMethod; + this.BlendMethod = other.BlendMethod; + } + + /// + /// Gets or sets how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas. + /// + public WebpBlendingMethod BlendMethod { get; set; } + + /// + /// Gets or sets how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. + /// + public WebpDisposalMethod DisposalMethod { get; set; } /// /// Gets or sets the frame duration. The time to wait before displaying the next frame, /// in 1 millisecond units. Note the interpretation of frame duration of 0 (and often smaller and equal to 10) is implementation defined. /// - public uint FrameDuration { get; set; } + public uint FrameDelay { get; set; } /// public IDeepCloneable DeepClone() => new WebpFrameMetadata(this); diff --git a/src/ImageSharp/Formats/Webp/WebpMetadata.cs b/src/ImageSharp/Formats/Webp/WebpMetadata.cs index 5d1051c751..a6bb0a7b80 100644 --- a/src/ImageSharp/Formats/Webp/WebpMetadata.cs +++ b/src/ImageSharp/Formats/Webp/WebpMetadata.cs @@ -23,6 +23,7 @@ public class WebpMetadata : IDeepCloneable { this.FileFormat = other.FileFormat; this.AnimationLoopCount = other.AnimationLoopCount; + this.AnimationBackground = other.AnimationBackground; } /// @@ -35,6 +36,14 @@ public class WebpMetadata : IDeepCloneable /// public ushort AnimationLoopCount { get; set; } = 1; + /// + /// Gets or sets the default background color of the canvas in [Blue, Green, Red, Alpha] byte order. + /// This color MAY be used to fill the unused space on the canvas around the frames, + /// as well as the transparent pixels of the first frame. + /// The background color is also used when the Disposal method is 1. + /// + public Color AnimationBackground { get; set; } + /// public IDeepCloneable DeepClone() => new WebpMetadata(this); } diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs index c0fc00b82d..c3a777c153 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs @@ -308,7 +308,7 @@ public class WebpDecoderTests image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact); Assert.Equal(0, webpMetaData.AnimationLoopCount); - Assert.Equal(150U, frameMetaData.FrameDuration); + Assert.Equal(150U, frameMetaData.FrameDelay); Assert.Equal(12, image.Frames.Count); } @@ -325,7 +325,7 @@ public class WebpDecoderTests image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Tolerant(0.04f)); Assert.Equal(0, webpMetaData.AnimationLoopCount); - Assert.Equal(150U, frameMetaData.FrameDuration); + Assert.Equal(150U, frameMetaData.FrameDelay); Assert.Equal(12, image.Frames.Count); } From b4e1b7f4e105360698fa155257c8d72c59bf52e2 Mon Sep 17 00:00:00 2001 From: Poker Date: Wed, 1 Nov 2023 22:46:20 +0800 Subject: [PATCH 18/24] fix --- .../Formats/Webp/BitWriter/BitWriterBase.cs | 1 - .../Formats/Webp/Lossless/Vp8LEncoder.cs | 4 + .../Webp/Lossless/WebpLosslessDecoder.cs | 115 +++++----- .../Formats/Webp/Lossy/Vp8Encoder.cs | 4 + .../Formats/Webp/Lossy/WebpLossyDecoder.cs | 198 +++++++++--------- .../Formats/Webp/WebpAnimationDecoder.cs | 53 ++--- src/ImageSharp/Formats/Webp/WebpFrameData.cs | 2 + 7 files changed, 188 insertions(+), 189 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index c1860c9c59..cbf96a91af 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -263,7 +263,6 @@ internal abstract class BitWriterBase WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Height - 1); WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Duration); - // TODO: If we can clip the indexed frame for transparent bounds we can set properties here. byte flag = (byte)(((int)animation.BlendingMethod << 1) | (int)animation.DisposalMethod); stream.WriteByte(flag); return position; diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 9156d5bdf8..42aa667ac5 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -306,8 +306,12 @@ internal class Vp8LEncoder : IDisposable if (hasAnimation) { WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata(); + + // TODO: If we can clip the indexed frame for transparent bounds we can set properties here. prevPosition = BitWriterBase.WriteAnimationFrame(stream, new WebpFrameData { + X = 0, + Y = 0, Width = (uint)frame.Width, Height = (uint)frame.Height, Duration = frameMetadata.FrameDelay, diff --git a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs index 54dd1d6ed1..e4c2a7ddf6 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs @@ -95,12 +95,10 @@ internal sealed class WebpLosslessDecoder public void Decode(Buffer2D pixels, int width, int height) where TPixel : unmanaged, IPixel { - using (Vp8LDecoder decoder = new Vp8LDecoder(width, height, this.memoryAllocator)) - { - this.DecodeImageStream(decoder, width, height, true); - this.DecodeImageData(decoder, decoder.Pixels.Memory.Span); - this.DecodePixelValues(decoder, pixels, width, height); - } + using Vp8LDecoder decoder = new(width, height, this.memoryAllocator); + this.DecodeImageStream(decoder, width, height, true); + this.DecodeImageData(decoder, decoder.Pixels.Memory.Span); + this.DecodePixelValues(decoder, pixels, width, height); } public IMemoryOwner DecodeImageStream(Vp8LDecoder decoder, int xSize, int ySize, bool isLevel0) @@ -616,15 +614,12 @@ internal sealed class WebpLosslessDecoder private void ReadTransformation(int xSize, int ySize, Vp8LDecoder decoder) { Vp8LTransformType transformType = (Vp8LTransformType)this.bitReader.ReadValue(2); - Vp8LTransform transform = new Vp8LTransform(transformType, xSize, ySize); + Vp8LTransform transform = new(transformType, xSize, ySize); // Each transform is allowed to be used only once. - foreach (Vp8LTransform decoderTransform in decoder.Transforms) + if (decoder.Transforms.Any(decoderTransform => decoderTransform.TransformType == transform.TransformType)) { - if (decoderTransform.TransformType == transform.TransformType) - { - WebpThrowHelper.ThrowImageFormatException("Each transform can only be present once"); - } + WebpThrowHelper.ThrowImageFormatException("Each transform can only be present once"); } switch (transformType) @@ -744,61 +739,69 @@ internal sealed class WebpLosslessDecoder this.bitReader.FillBitWindow(); int code = (int)this.ReadSymbol(htreeGroup[0].HTrees[HuffIndex.Green]); - if (code < WebpConstants.NumLiteralCodes) + switch (code) { - // Literal - data[pos] = (byte)code; - ++pos; - ++col; - - if (col >= width) + case < WebpConstants.NumLiteralCodes: { - col = 0; - ++row; - if (row <= lastRow && row % WebpConstants.NumArgbCacheRows == 0) + // Literal + data[pos] = (byte)code; + ++pos; + ++col; + + if (col >= width) { - dec.ExtractPalettedAlphaRows(row); + col = 0; + ++row; + if (row <= lastRow && row % WebpConstants.NumArgbCacheRows == 0) + { + dec.ExtractPalettedAlphaRows(row); + } } - } - } - else if (code < lenCodeLimit) - { - // Backward reference - int lengthSym = code - WebpConstants.NumLiteralCodes; - int length = this.GetCopyLength(lengthSym); - int distSymbol = (int)this.ReadSymbol(htreeGroup[0].HTrees[HuffIndex.Dist]); - this.bitReader.FillBitWindow(); - int distCode = this.GetCopyDistance(distSymbol); - int dist = PlaneCodeToDistance(width, distCode); - if (pos >= dist && end - pos >= length) - { - CopyBlock8B(data, pos, dist, length); - } - else - { - WebpThrowHelper.ThrowImageFormatException("error while decoding alpha data"); + + break; } - pos += length; - col += length; - while (col >= width) + case < lenCodeLimit: { - col -= width; - ++row; - if (row <= lastRow && row % WebpConstants.NumArgbCacheRows == 0) + // Backward reference + int lengthSym = code - WebpConstants.NumLiteralCodes; + int length = this.GetCopyLength(lengthSym); + int distSymbol = (int)this.ReadSymbol(htreeGroup[0].HTrees[HuffIndex.Dist]); + this.bitReader.FillBitWindow(); + int distCode = this.GetCopyDistance(distSymbol); + int dist = PlaneCodeToDistance(width, distCode); + if (pos >= dist && end - pos >= length) { - dec.ExtractPalettedAlphaRows(row); + CopyBlock8B(data, pos, dist, length); + } + else + { + WebpThrowHelper.ThrowImageFormatException("error while decoding alpha data"); } - } - if (pos < last && (col & mask) > 0) - { - htreeGroup = GetHTreeGroupForPos(hdr, col, row); + pos += length; + col += length; + while (col >= width) + { + col -= width; + ++row; + if (row <= lastRow && row % WebpConstants.NumArgbCacheRows == 0) + { + dec.ExtractPalettedAlphaRows(row); + } + } + + if (pos < last && (col & mask) > 0) + { + htreeGroup = GetHTreeGroupForPos(hdr, col, row); + } + + break; } - } - else - { - WebpThrowHelper.ThrowImageFormatException("bitstream error while parsing alpha data"); + + default: + WebpThrowHelper.ThrowImageFormatException("bitstream error while parsing alpha data"); + break; } this.bitReader.Eos = this.bitReader.IsEndOfStream(); diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 3a6e9a2ccd..3b73023062 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -476,8 +476,12 @@ internal class Vp8Encoder : IDisposable if (hasAnimation) { WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata(); + + // TODO: If we can clip the indexed frame for transparent bounds we can set properties here. prevPosition = BitWriterBase.WriteAnimationFrame(stream, new WebpFrameData { + X = 0, + Y = 0, Width = (uint)frame.Width, Height = (uint)frame.Height, Duration = frameMetadata.FrameDelay, diff --git a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs index 354bcdbb44..3eb03b1724 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs @@ -62,7 +62,7 @@ internal sealed class WebpLossyDecoder // Paragraph 9.2: color space and clamp type follow. sbyte colorSpace = (sbyte)this.bitReader.ReadValue(1); sbyte clampType = (sbyte)this.bitReader.ReadValue(1); - Vp8PictureHeader pictureHeader = new Vp8PictureHeader + Vp8PictureHeader pictureHeader = new() { Width = (uint)width, Height = (uint)height, @@ -73,55 +73,51 @@ internal sealed class WebpLossyDecoder }; // Paragraph 9.3: Parse the segment header. - Vp8Proba proba = new Vp8Proba(); + Vp8Proba proba = new(); Vp8SegmentHeader vp8SegmentHeader = this.ParseSegmentHeader(proba); - using (Vp8Decoder decoder = new Vp8Decoder( - info.Vp8FrameHeader, - pictureHeader, - vp8SegmentHeader, - proba, - this.memoryAllocator)) - { - Vp8Io io = InitializeVp8Io(decoder, pictureHeader); + using Vp8Decoder decoder = new( + info.Vp8FrameHeader, + pictureHeader, + vp8SegmentHeader, + proba, + this.memoryAllocator); + Vp8Io io = InitializeVp8Io(decoder, pictureHeader); - // Paragraph 9.4: Parse the filter specs. - this.ParseFilterHeader(decoder); - decoder.PrecomputeFilterStrengths(); + // Paragraph 9.4: Parse the filter specs. + this.ParseFilterHeader(decoder); + decoder.PrecomputeFilterStrengths(); - // Paragraph 9.5: Parse partitions. - this.ParsePartitions(decoder); + // Paragraph 9.5: Parse partitions. + this.ParsePartitions(decoder); - // Paragraph 9.6: Dequantization Indices. - this.ParseDequantizationIndices(decoder); + // Paragraph 9.6: Dequantization Indices. + this.ParseDequantizationIndices(decoder); - // Ignore the value of update probabilities. - this.bitReader.ReadBool(); + // Ignore the value of update probabilities. + this.bitReader.ReadBool(); - // Paragraph 13.4: Parse probabilities. - this.ParseProbabilities(decoder); + // Paragraph 13.4: Parse probabilities. + this.ParseProbabilities(decoder); - // Decode image data. - this.ParseFrame(decoder, io); + // Decode image data. + this.ParseFrame(decoder, io); - if (info.Features?.Alpha == true) - { - using (AlphaDecoder alphaDecoder = new AlphaDecoder( - width, - height, - alphaData, - info.Features.AlphaChunkHeader, - this.memoryAllocator, - this.configuration)) - { - alphaDecoder.Decode(); - DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels, alphaDecoder.Alpha); - } - } - else - { - this.DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels); - } + if (info.Features?.Alpha == true) + { + using AlphaDecoder alphaDecoder = new( + width, + height, + alphaData, + info.Features.AlphaChunkHeader, + this.memoryAllocator, + this.configuration); + alphaDecoder.Decode(); + DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels, alphaDecoder.Alpha); + } + else + { + this.DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels); } } @@ -199,8 +195,8 @@ internal sealed class WebpLossyDecoder { // Hardcoded tree parsing. block.Segment = this.bitReader.GetBit((int)dec.Probabilities.Segments[0]) == 0 - ? (byte)this.bitReader.GetBit((int)dec.Probabilities.Segments[1]) - : (byte)(this.bitReader.GetBit((int)dec.Probabilities.Segments[2]) + 2); + ? (byte)this.bitReader.GetBit((int)dec.Probabilities.Segments[1]) + : (byte)(this.bitReader.GetBit((int)dec.Probabilities.Segments[2]) + 2); } else { @@ -595,57 +591,65 @@ internal sealed class WebpLossyDecoder return; } - if (dec.Filter == LoopFilter.Simple) + switch (dec.Filter) { - int offset = dec.CacheYOffset + (mbx * 16); - if (mbx > 0) + case LoopFilter.Simple: { - LossyUtils.SimpleHFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4); - } + int offset = dec.CacheYOffset + (mbx * 16); + if (mbx > 0) + { + LossyUtils.SimpleHFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4); + } - if (filterInfo.UseInnerFiltering) - { - LossyUtils.SimpleHFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit); - } + if (filterInfo.UseInnerFiltering) + { + LossyUtils.SimpleHFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit); + } - if (mby > 0) - { - LossyUtils.SimpleVFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4); - } + if (mby > 0) + { + LossyUtils.SimpleVFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4); + } - if (filterInfo.UseInnerFiltering) - { - LossyUtils.SimpleVFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit); - } - } - else if (dec.Filter == LoopFilter.Complex) - { - int uvBps = dec.CacheUvStride; - int yOffset = dec.CacheYOffset + (mbx * 16); - int uvOffset = dec.CacheUvOffset + (mbx * 8); - int hevThresh = filterInfo.HighEdgeVarianceThreshold; - if (mbx > 0) - { - LossyUtils.HFilter16(dec.CacheY.Memory.Span, yOffset, yBps, limit + 4, iLevel, hevThresh); - LossyUtils.HFilter8(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit + 4, iLevel, hevThresh); - } + if (filterInfo.UseInnerFiltering) + { + LossyUtils.SimpleVFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit); + } - if (filterInfo.UseInnerFiltering) - { - LossyUtils.HFilter16i(dec.CacheY.Memory.Span, yOffset, yBps, limit, iLevel, hevThresh); - LossyUtils.HFilter8i(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit, iLevel, hevThresh); + break; } - if (mby > 0) + case LoopFilter.Complex: { - LossyUtils.VFilter16(dec.CacheY.Memory.Span, yOffset, yBps, limit + 4, iLevel, hevThresh); - LossyUtils.VFilter8(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit + 4, iLevel, hevThresh); - } + int uvBps = dec.CacheUvStride; + int yOffset = dec.CacheYOffset + (mbx * 16); + int uvOffset = dec.CacheUvOffset + (mbx * 8); + int hevThresh = filterInfo.HighEdgeVarianceThreshold; + if (mbx > 0) + { + LossyUtils.HFilter16(dec.CacheY.Memory.Span, yOffset, yBps, limit + 4, iLevel, hevThresh); + LossyUtils.HFilter8(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit + 4, iLevel, hevThresh); + } - if (filterInfo.UseInnerFiltering) - { - LossyUtils.VFilter16i(dec.CacheY.Memory.Span, yOffset, yBps, limit, iLevel, hevThresh); - LossyUtils.VFilter8i(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit, iLevel, hevThresh); + if (filterInfo.UseInnerFiltering) + { + LossyUtils.HFilter16i(dec.CacheY.Memory.Span, yOffset, yBps, limit, iLevel, hevThresh); + LossyUtils.HFilter8i(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit, iLevel, hevThresh); + } + + if (mby > 0) + { + LossyUtils.VFilter16(dec.CacheY.Memory.Span, yOffset, yBps, limit + 4, iLevel, hevThresh); + LossyUtils.VFilter8(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit + 4, iLevel, hevThresh); + } + + if (filterInfo.UseInnerFiltering) + { + LossyUtils.VFilter16i(dec.CacheY.Memory.Span, yOffset, yBps, limit, iLevel, hevThresh); + LossyUtils.VFilter8i(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit, iLevel, hevThresh); + } + + break; } } } @@ -1067,7 +1071,7 @@ internal sealed class WebpLossyDecoder private Vp8SegmentHeader ParseSegmentHeader(Vp8Proba proba) { - Vp8SegmentHeader vp8SegmentHeader = new Vp8SegmentHeader + Vp8SegmentHeader vp8SegmentHeader = new() { UseSegment = this.bitReader.ReadBool() }; @@ -1333,18 +1337,12 @@ internal sealed class WebpLossyDecoder private static uint NzCodeBits(uint nzCoeffs, int nz, int dcNz) { nzCoeffs <<= 2; - if (nz > 3) + nzCoeffs |= nz switch { - nzCoeffs |= 3; - } - else if (nz > 1) - { - nzCoeffs |= 2; - } - else - { - nzCoeffs |= (uint)dcNz; - } + > 3 => 3, + > 1 => 2, + _ => (uint)dcNz + }; return nzCoeffs; } @@ -1358,13 +1356,13 @@ internal sealed class WebpLossyDecoder if (mbx == 0) { return mby == 0 - ? 6 // B_DC_PRED_NOTOPLEFT - : 5; // B_DC_PRED_NOLEFT + ? 6 // B_DC_PRED_NOTOPLEFT + : 5; // B_DC_PRED_NOLEFT } return mby == 0 - ? 4 // B_DC_PRED_NOTOP - : 0; // B_DC_PRED + ? 4 // B_DC_PRED_NOTOP + : 0; // B_DC_PRED } return mode; diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 87657dfabb..fad6ca16cc 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.IO; @@ -193,11 +192,7 @@ internal class WebpAnimationDecoder : IDisposable imageFrame = currentFrame; } - int frameX = (int)(frameData.X * 2); - int frameY = (int)(frameData.Y * 2); - int frameWidth = (int)frameData.Width; - int frameHeight = (int)frameData.Height; - Rectangle regionRectangle = Rectangle.FromLTRB(frameX, frameY, frameX + frameWidth, frameY + frameHeight); + Rectangle regionRectangle = frameData.Bounds; if (frameData.DisposalMethod is WebpDisposalMethod.Dispose) { @@ -205,11 +200,11 @@ internal class WebpAnimationDecoder : IDisposable } using Buffer2D decodedImage = this.DecodeImageData(frameData, webpInfo); - DrawDecodedImageOnCanvas(decodedImage, imageFrame, frameX, frameY, frameWidth, frameHeight); + DrawDecodedImageOnCanvas(decodedImage, imageFrame, regionRectangle); if (previousFrame != null && frameData.BlendingMethod is WebpBlendingMethod.AlphaBlending) { - this.AlphaBlend(previousFrame, imageFrame, frameX, frameY, frameWidth, frameHeight); + this.AlphaBlend(previousFrame, imageFrame, regionRectangle); } previousFrame = currentFrame ?? image.Frames.RootFrame; @@ -245,7 +240,7 @@ internal class WebpAnimationDecoder : IDisposable byte alphaChunkHeader = (byte)stream.ReadByte(); Span alphaData = this.alphaData.GetSpan(); - stream.Read(alphaData, 0, alphaDataSize); + _ = stream.Read(alphaData, 0, alphaDataSize); return alphaChunkHeader; } @@ -260,11 +255,11 @@ internal class WebpAnimationDecoder : IDisposable private Buffer2D DecodeImageData(WebpFrameData frameData, WebpImageInfo webpInfo) where TPixel : unmanaged, IPixel { - Image decodedImage = new((int)frameData.Width, (int)frameData.Height); + ImageFrame decodedFrame = new(Configuration.Default, (int)frameData.Width, (int)frameData.Height); try { - Buffer2D pixelBufferDecoded = decodedImage.GetRootFramePixelBuffer(); + Buffer2D pixelBufferDecoded = decodedFrame.PixelBuffer; if (webpInfo.IsLossless) { WebpLosslessDecoder losslessDecoder = @@ -282,7 +277,7 @@ internal class WebpAnimationDecoder : IDisposable } catch { - decodedImage?.Dispose(); + decodedFrame?.Dispose(); throw; } finally @@ -297,20 +292,17 @@ internal class WebpAnimationDecoder : IDisposable /// The type of the pixel. /// The decoded image. /// The image frame to draw into. - /// The frame x coordinate. - /// The frame y coordinate. - /// The width of the frame. - /// The height of the frame. - private static void DrawDecodedImageOnCanvas(Buffer2D decodedImage, ImageFrame imageFrame, int frameX, int frameY, int frameWidth, int frameHeight) + /// The area of the frame. + private static void DrawDecodedImageOnCanvas(Buffer2D decodedImage, ImageFrame imageFrame, Rectangle restoreArea) where TPixel : unmanaged, IPixel { - Buffer2D imageFramePixels = imageFrame.PixelBuffer; + Buffer2DRegion imageFramePixels = imageFrame.PixelBuffer.GetRegion(restoreArea); int decodedRowIdx = 0; - for (int y = frameY; y < frameY + frameHeight; y++) + for (int y = 0; y < restoreArea.Height; y++) { Span framePixelRow = imageFramePixels.DangerousGetRowSpan(y); - Span decodedPixelRow = decodedImage.DangerousGetRowSpan(decodedRowIdx++)[..frameWidth]; - decodedPixelRow.TryCopyTo(framePixelRow[frameX..]); + Span decodedPixelRow = decodedImage.DangerousGetRowSpan(decodedRowIdx++)[..restoreArea.Width]; + decodedPixelRow.TryCopyTo(framePixelRow); } } @@ -321,22 +313,19 @@ internal class WebpAnimationDecoder : IDisposable /// The pixel format. /// The source image. /// The destination image. - /// The frame x coordinate. - /// The frame y coordinate. - /// The width of the frame. - /// The height of the frame. - private void AlphaBlend(ImageFrame src, ImageFrame dst, int frameX, int frameY, int frameWidth, int frameHeight) + /// The area of the frame. + private void AlphaBlend(ImageFrame src, ImageFrame dst, Rectangle restoreArea) where TPixel : unmanaged, IPixel { - Buffer2D srcPixels = src.PixelBuffer; - Buffer2D dstPixels = dst.PixelBuffer; + Buffer2DRegion srcPixels = src.PixelBuffer.GetRegion(restoreArea); + Buffer2DRegion dstPixels = dst.PixelBuffer.GetRegion(restoreArea); PixelBlender blender = PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); - for (int y = frameY; y < frameY + frameHeight; y++) + for (int y = 0; y < restoreArea.Height; y++) { - Span srcPixelRow = srcPixels.DangerousGetRowSpan(y).Slice(frameX, frameWidth); - Span dstPixelRow = dstPixels.DangerousGetRowSpan(y).Slice(frameX, frameWidth); + Span srcPixelRow = srcPixels.DangerousGetRowSpan(y); + Span dstPixelRow = dstPixels.DangerousGetRowSpan(y); - blender.Blend(this.configuration, dstPixelRow, srcPixelRow, dstPixelRow, 1.0f); + blender.Blend(this.configuration, dstPixelRow, srcPixelRow, dstPixelRow, 1f); } } diff --git a/src/ImageSharp/Formats/Webp/WebpFrameData.cs b/src/ImageSharp/Formats/Webp/WebpFrameData.cs index e2bcfd7c36..93c5d10dcd 100644 --- a/src/ImageSharp/Formats/Webp/WebpFrameData.cs +++ b/src/ImageSharp/Formats/Webp/WebpFrameData.cs @@ -53,6 +53,8 @@ internal struct WebpFrameData /// public WebpDisposalMethod DisposalMethod; + public readonly Rectangle Bounds => new((int)this.X * 2, (int)this.Y * 2, (int)this.Width, (int)this.Height); + /// /// Reads the animation frame header. /// From 296da738008b06ea511b4e0123d059aa4e660a89 Mon Sep 17 00:00:00 2001 From: Poker Date: Fri, 3 Nov 2023 14:26:50 +0800 Subject: [PATCH 19/24] add riif helper --- src/ImageSharp/Common/Helpers/RiffHelper.cs | 117 ++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 src/ImageSharp/Common/Helpers/RiffHelper.cs diff --git a/src/ImageSharp/Common/Helpers/RiffHelper.cs b/src/ImageSharp/Common/Helpers/RiffHelper.cs new file mode 100644 index 0000000000..6354ebd663 --- /dev/null +++ b/src/ImageSharp/Common/Helpers/RiffHelper.cs @@ -0,0 +1,117 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using System.Text; + +namespace SixLabors.ImageSharp.Common.Helpers; + +internal class RiffHelper +{ + /// + /// The header bytes identifying RIFF file. + /// + public static readonly uint RiffFourCc = 0x52_49_46_46; + + public static void WriteRiffFile(Stream stream, string formType, Action func) => + WriteChunk(stream, RiffFourCc, s => + { + s.Write(Encoding.ASCII.GetBytes(formType)); + func(s); + }); + + public static void WriteChunk(Stream stream, uint fourCc, Action func) + { + Span buffer = stackalloc byte[4]; + + // write the fourCC + BinaryPrimitives.WriteUInt32LittleEndian(buffer, fourCc); + stream.Write(buffer); + + long sizePosition = stream.Position; + stream.Position += 4; + + func(stream); + + long position = stream.Position; + stream.Position = sizePosition; + + uint dataSize = (uint)(position - sizePosition - 4); + + // padding + if (dataSize % 2 == 1) + { + stream.WriteByte(0); + position++; + } + + BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize); + stream.Write(buffer); + + stream.Position = position; + } + + public static void WriteChunk(Stream stream, uint fourCc, ReadOnlySpan data) + { + Span buffer = stackalloc byte[4]; + + // write the fourCC + BinaryPrimitives.WriteUInt32LittleEndian(buffer, fourCc); + stream.Write(buffer); + uint size = (uint)data.Length; + BinaryPrimitives.WriteUInt32LittleEndian(buffer, size); + stream.Write(buffer); + stream.Write(data); + + // padding + if (size % 2 == 1) + { + stream.WriteByte(0); + } + } + + public static unsafe void WriteChunk(Stream stream, uint fourCc, in TStruct chunk) + where TStruct : unmanaged + { + fixed (TStruct* ptr = &chunk) + { + WriteChunk(stream, fourCc, new Span(ptr, sizeof(TStruct))); + } + } + + public static long BeginWriteChunk(Stream stream, uint fourCc) + { + Span buffer = stackalloc byte[4]; + + // write the fourCC + BinaryPrimitives.WriteUInt32LittleEndian(buffer, fourCc); + stream.Write(buffer); + + long sizePosition = stream.Position; + stream.Position += 4; + + return sizePosition; + } + + public static void EndWriteChunk(Stream stream, long sizePosition) + { + Span buffer = stackalloc byte[4]; + + long position = stream.Position; + stream.Position = sizePosition; + + uint dataSize = (uint)(position - sizePosition - 4); + + // padding + if (dataSize % 2 == 1) + { + stream.WriteByte(0); + position++; + } + + BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize); + stream.Write(buffer); + + stream.Position = position; + } +} From 9dc0cda95e1c810f3bd56db1803fc0aa6cea4a09 Mon Sep 17 00:00:00 2001 From: Poker Date: Fri, 3 Nov 2023 15:06:57 +0800 Subject: [PATCH 20/24] add static --- src/ImageSharp/Common/Helpers/RiffHelper.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/RiffHelper.cs b/src/ImageSharp/Common/Helpers/RiffHelper.cs index 6354ebd663..0395d9a9c9 100644 --- a/src/ImageSharp/Common/Helpers/RiffHelper.cs +++ b/src/ImageSharp/Common/Helpers/RiffHelper.cs @@ -6,12 +6,12 @@ using System.Text; namespace SixLabors.ImageSharp.Common.Helpers; -internal class RiffHelper +internal static class RiffHelper { /// /// The header bytes identifying RIFF file. /// - public static readonly uint RiffFourCc = 0x52_49_46_46; + private const uint RiffFourCc = 0x52_49_46_46; public static void WriteRiffFile(Stream stream, string formType, Action func) => WriteChunk(stream, RiffFourCc, s => @@ -64,7 +64,7 @@ internal class RiffHelper stream.Write(data); // padding - if (size % 2 == 1) + if (size % 2 is 1) { stream.WriteByte(0); } @@ -103,7 +103,7 @@ internal class RiffHelper uint dataSize = (uint)(position - sizePosition - 4); // padding - if (dataSize % 2 == 1) + if (dataSize % 2 is 1) { stream.WriteByte(0); position++; @@ -114,4 +114,13 @@ internal class RiffHelper stream.Position = position; } + + public static long BeginWriteRiffFile(Stream stream, string formType) + { + long sizePosition = BeginWriteChunk(stream, RiffFourCc); + stream.Write(Encoding.ASCII.GetBytes(formType)); + return sizePosition; + } + + public static void EndWriteRiffFile(Stream stream, long sizePosition) => EndWriteChunk(stream, sizePosition); } From 4beafcf65586ff2631fefd2d6b1aedc1422ab4c8 Mon Sep 17 00:00:00 2001 From: Poker Date: Fri, 3 Nov 2023 17:16:44 +0800 Subject: [PATCH 21/24] fix FourCC bug --- src/ImageSharp/Common/Helpers/RiffHelper.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/RiffHelper.cs b/src/ImageSharp/Common/Helpers/RiffHelper.cs index 0395d9a9c9..8f06e5886f 100644 --- a/src/ImageSharp/Common/Helpers/RiffHelper.cs +++ b/src/ImageSharp/Common/Helpers/RiffHelper.cs @@ -25,7 +25,7 @@ internal static class RiffHelper Span buffer = stackalloc byte[4]; // write the fourCC - BinaryPrimitives.WriteUInt32LittleEndian(buffer, fourCc); + BinaryPrimitives.WriteUInt32BigEndian(buffer, fourCc); stream.Write(buffer); long sizePosition = stream.Position; @@ -34,7 +34,6 @@ internal static class RiffHelper func(stream); long position = stream.Position; - stream.Position = sizePosition; uint dataSize = (uint)(position - sizePosition - 4); @@ -46,8 +45,8 @@ internal static class RiffHelper } BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize); + stream.Position = sizePosition; stream.Write(buffer); - stream.Position = position; } @@ -56,7 +55,7 @@ internal static class RiffHelper Span buffer = stackalloc byte[4]; // write the fourCC - BinaryPrimitives.WriteUInt32LittleEndian(buffer, fourCc); + BinaryPrimitives.WriteUInt32BigEndian(buffer, fourCc); stream.Write(buffer); uint size = (uint)data.Length; BinaryPrimitives.WriteUInt32LittleEndian(buffer, size); @@ -84,7 +83,7 @@ internal static class RiffHelper Span buffer = stackalloc byte[4]; // write the fourCC - BinaryPrimitives.WriteUInt32LittleEndian(buffer, fourCc); + BinaryPrimitives.WriteUInt32BigEndian(buffer, fourCc); stream.Write(buffer); long sizePosition = stream.Position; @@ -98,7 +97,6 @@ internal static class RiffHelper Span buffer = stackalloc byte[4]; long position = stream.Position; - stream.Position = sizePosition; uint dataSize = (uint)(position - sizePosition - 4); @@ -110,8 +108,8 @@ internal static class RiffHelper } BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize); + stream.Position = sizePosition; stream.Write(buffer); - stream.Position = position; } From 1b0b877a147e81b9c3ff9d3e90de668b4e13cf1d Mon Sep 17 00:00:00 2001 From: Poker Date: Fri, 3 Nov 2023 19:18:56 +0800 Subject: [PATCH 22/24] introduce RiffHelper --- .../Formats/Webp/BitWriter/BitWriterBase.cs | 222 ++---------------- .../Webp/Chunks/WebpAnimationParameter.cs | 37 +++ .../Formats/Webp/Chunks/WebpFrameData.cs | 140 +++++++++++ .../Formats/Webp/Chunks/WebpVp8X.cs | 113 +++++++++ .../Formats/Webp/Lossless/Vp8LEncoder.cs | 23 +- .../Formats/Webp/Lossy/Vp8Encoder.cs | 23 +- .../Formats/Webp/WebpAnimationDecoder.cs | 3 +- .../Formats/Webp/WebpChunkParsingUtils.cs | 18 +- src/ImageSharp/Formats/Webp/WebpChunkType.cs | 2 +- src/ImageSharp/Formats/Webp/WebpConstants.cs | 10 +- src/ImageSharp/Formats/Webp/WebpFrameData.cs | 93 -------- 11 files changed, 349 insertions(+), 335 deletions(-) create mode 100644 src/ImageSharp/Formats/Webp/Chunks/WebpAnimationParameter.cs create mode 100644 src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs create mode 100644 src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs delete mode 100644 src/ImageSharp/Formats/Webp/WebpFrameData.cs diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index cbf96a91af..d502fd6063 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -1,8 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Buffers.Binary; -using System.Runtime.InteropServices; +using System.Diagnostics; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Webp.Chunks; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; @@ -15,8 +16,6 @@ internal abstract class BitWriterBase private const ulong MaxCanvasPixels = 4294967295ul; - protected const uint ExtendedFileChunkSize = WebpConstants.ChunkHeaderSize + WebpConstants.Vp8XChunkSize; - /// /// Buffer to write to. /// @@ -79,48 +78,6 @@ internal abstract class BitWriterBase Array.Resize(ref this.buffer, newSize); } - /// - /// Writes the RIFF header to the stream. - /// - /// The stream to write to. - /// The block length. - protected static void WriteRiffHeader(Stream stream, uint riffSize) - { - stream.Write(WebpConstants.RiffFourCc); - Span buf = stackalloc byte[4]; - BinaryPrimitives.WriteUInt32LittleEndian(buf, riffSize); - stream.Write(buf); - stream.Write(WebpConstants.WebpHeader); - } - - /// - /// Calculates the chunk size of EXIF, XMP or ICCP metadata. - /// - /// The metadata profile bytes. - /// The metadata chunk size in bytes. - protected static uint MetadataChunkSize(byte[] metadataBytes) - { - uint metaSize = (uint)metadataBytes.Length; - return WebpConstants.ChunkHeaderSize + metaSize + (metaSize & 1); - } - - /// - /// Calculates the chunk size of a alpha chunk. - /// - /// The alpha chunk bytes. - /// The alpha data chunk size in bytes. - protected static uint AlphaChunkSize(Span alphaBytes) - { - uint alphaSize = (uint)alphaBytes.Length + 1; - return WebpConstants.ChunkHeaderSize + alphaSize + (alphaSize & 1); - } - - /// - /// Overwrites ides the write file size. - /// - /// The stream to write to. - protected static void OverwriteFileSize(Stream stream) => OverwriteFrameSize(stream, 4); - /// /// Write the trunks before data trunk. /// @@ -143,7 +100,9 @@ internal abstract class BitWriterBase bool hasAnimation) { // Write file size later - WriteRiffHeader(stream, 0); + long pos = RiffHelper.BeginWriteRiffFile(stream, WebpConstants.WebpFourCc); + + Debug.Assert(pos is 4, "Stream should be written from position 0."); // Write VP8X, header if necessary. bool isVp8X = exifProfile != null || xmpProfile != null || iccProfile != null || hasAlpha || hasAnimation; @@ -153,7 +112,7 @@ internal abstract class BitWriterBase if (iccProfile != null) { - WriteColorProfile(stream, iccProfile.ToByteArray()); + RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Iccp, iccProfile.ToByteArray()); } } } @@ -177,49 +136,17 @@ internal abstract class BitWriterBase { if (exifProfile != null) { - WriteMetadataProfile(stream, exifProfile.ToByteArray(), WebpChunkType.Exif); + RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Exif, exifProfile.ToByteArray()); } if (xmpProfile != null) { - WriteMetadataProfile(stream, xmpProfile.Data, WebpChunkType.Xmp); + RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Xmp, xmpProfile.Data); } - OverwriteFileSize(stream); - } - - /// - /// Writes a metadata profile (EXIF or XMP) to the stream. - /// - /// The stream to write to. - /// The metadata profile's bytes. - /// The chuck type to write. - protected static void WriteMetadataProfile(Stream stream, byte[]? metadataBytes, WebpChunkType chunkType) - { - DebugGuard.NotNull(metadataBytes, nameof(metadataBytes)); - - uint size = (uint)metadataBytes.Length; - Span buf = stackalloc byte[4]; - BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)chunkType); - stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, size); - stream.Write(buf); - stream.Write(metadataBytes); - - // Add padding byte if needed. - if ((size & 1) == 1) - { - stream.WriteByte(0); - } + RiffHelper.EndWriteRiffFile(stream, 4); } - /// - /// Writes the color profile() to the stream. - /// - /// The stream to write to. - /// The color profile bytes. - protected static void WriteColorProfile(Stream stream, byte[] iccProfileBytes) => WriteMetadataProfile(stream, iccProfileBytes, WebpChunkType.Iccp); - /// /// Writes the animation parameter() to the stream. /// @@ -233,55 +160,8 @@ internal abstract class BitWriterBase /// The number of times to loop the animation. If it is 0, this means infinitely. public static void WriteAnimationParameter(Stream stream, Color background, ushort loopCount) { - Span buf = stackalloc byte[4]; - BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.AnimationParameter); - stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, sizeof(uint) + sizeof(ushort)); - stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, background.ToRgba32().Rgba); - stream.Write(buf); - BinaryPrimitives.WriteUInt16LittleEndian(buf[..2], loopCount); - stream.Write(buf[..2]); - } - - /// - /// Writes the animation frame() to the stream. - /// - /// The stream to write to. - /// Animation frame data. - public static long WriteAnimationFrame(Stream stream, WebpFrameData animation) - { - Span buf = stackalloc byte[4]; - BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Animation); - stream.Write(buf); - long position = stream.Position; - BinaryPrimitives.WriteUInt32BigEndian(buf, 0); - stream.Write(buf); - WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.X); - WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Y); - WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Width - 1); - WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Height - 1); - WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, animation.Duration); - - byte flag = (byte)(((int)animation.BlendingMethod << 1) | (int)animation.DisposalMethod); - stream.WriteByte(flag); - return position; - } - - /// - /// Overwrites ides the write frame size. - /// - /// The stream to write to. - /// Previous position. - public static void OverwriteFrameSize(Stream stream, long prevPosition) - { - uint position = (uint)stream.Position; - stream.Position = prevPosition; - byte[] buffer = new byte[4]; - - BinaryPrimitives.WriteUInt32LittleEndian(buffer, (uint)(position - prevPosition - 4)); - stream.Write(buffer); - stream.Position = position; + WebpAnimationParameter chunk = new(background.ToRgba32().Rgba, loopCount); + chunk.WriteTo(stream); } /// @@ -292,27 +172,17 @@ internal abstract class BitWriterBase /// Indicates, if the alpha channel data is compressed. public static void WriteAlphaChunk(Stream stream, Span dataBytes, bool alphaDataIsCompressed) { - uint size = (uint)dataBytes.Length + 1; - Span buf = stackalloc byte[4]; - BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Alpha); - stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, size); - stream.Write(buf); - + long pos = RiffHelper.BeginWriteChunk(stream, (uint)WebpChunkType.Alpha); byte flags = 0; if (alphaDataIsCompressed) { + // TODO: Filtering and preprocessing flags = 1; } stream.WriteByte(flags); stream.Write(dataBytes); - - // Add padding byte if needed. - if ((size & 1) == 1) - { - stream.WriteByte(0); - } + RiffHelper.EndWriteChunk(stream, pos); } /// @@ -328,66 +198,10 @@ internal abstract class BitWriterBase /// Flag indicating, if an animation parameter is present. protected static void WriteVp8XHeader(Stream stream, ExifProfile? exifProfile, XmpProfile? xmpProfile, IccProfile? iccProfile, uint width, uint height, bool hasAlpha, bool hasAnimation) { - if (width > MaxDimension || height > MaxDimension) - { - WebpThrowHelper.ThrowInvalidImageDimensions($"Image width or height exceeds maximum allowed dimension of {MaxDimension}"); - } + WebpVp8X chunk = new(hasAnimation, xmpProfile != null, exifProfile != null, hasAlpha, iccProfile != null, width, height); - // The spec states that the product of Canvas Width and Canvas Height MUST be at most 2^32 - 1. - if (width * height > MaxCanvasPixels) - { - WebpThrowHelper.ThrowInvalidImageDimensions("The product of image width and height MUST be at most 2^32 - 1"); - } - - uint flags = 0; - if (exifProfile != null) - { - // Set exif bit. - flags |= 8; - } - - if (hasAnimation) - { - // Set animated flag. - flags |= 2; - } - - if (xmpProfile != null) - { - // Set xmp bit. - flags |= 4; - } - - if (hasAlpha) - { - // Set alpha bit. - flags |= 16; - } - - if (iccProfile != null) - { - // Set iccp flag. - flags |= 32; - } - - Span buf = stackalloc byte[4]; - BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Vp8X); - stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, WebpConstants.Vp8XChunkSize); - stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, flags); - stream.Write(buf); - BinaryPrimitives.WriteUInt32LittleEndian(buf, width - 1); - stream.Write(buf[..3]); - BinaryPrimitives.WriteUInt32LittleEndian(buf, height - 1); - stream.Write(buf[..3]); - } - - private unsafe struct ScratchBuffer - { - private const int Size = 4; - private fixed byte scratch[Size]; + chunk.Validate(MaxDimension, MaxCanvasPixels); - public Span Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size); + chunk.WriteTo(stream); } } diff --git a/src/ImageSharp/Formats/Webp/Chunks/WebpAnimationParameter.cs b/src/ImageSharp/Formats/Webp/Chunks/WebpAnimationParameter.cs new file mode 100644 index 0000000000..3855a293c1 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Chunks/WebpAnimationParameter.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Webp.Chunks; + +internal readonly struct WebpAnimationParameter +{ + public WebpAnimationParameter(uint background, ushort loopCount) + { + this.Background = background; + this.LoopCount = loopCount; + } + + /// + /// Gets default background color of the canvas in [Blue, Green, Red, Alpha] byte order. + /// This color MAY be used to fill the unused space on the canvas around the frames, + /// as well as the transparent pixels of the first frame. + /// The background color is also used when the Disposal method is 1. + /// + public uint Background { get; } + + /// + /// Gets number of times to loop the animation. If it is 0, this means infinitely. + /// + public ushort LoopCount { get; } + + public void WriteTo(Stream stream) + { + Span buffer = stackalloc byte[6]; + BinaryPrimitives.WriteUInt32LittleEndian(buffer[..4], this.Background); + BinaryPrimitives.WriteUInt16LittleEndian(buffer[4..], this.LoopCount); + RiffHelper.WriteChunk(stream, (uint)WebpChunkType.AnimationParameter, buffer); + } +} diff --git a/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs b/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs new file mode 100644 index 0000000000..f22a3fd540 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs @@ -0,0 +1,140 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Webp.Chunks; + +internal readonly struct WebpFrameData +{ + /// + /// X(3) + Y(3) + Width(3) + Height(3) + Duration(3) + 1 byte for flags. + /// + public const uint HeaderSize = 16; + + public WebpFrameData(uint dataSize, uint x, uint y, uint width, uint height, uint duration, WebpBlendingMethod blendingMethod, WebpDisposalMethod disposalMethod) + { + this.DataSize = dataSize; + this.X = x; + this.Y = y; + this.Width = width; + this.Height = height; + this.Duration = duration; + this.DisposalMethod = disposalMethod; + this.BlendingMethod = blendingMethod; + } + + public WebpFrameData(uint dataSize, uint x, uint y, uint width, uint height, uint duration, int flags) + : this( + dataSize, + x, + y, + width, + height, + duration, + (flags & 2) != 0 ? WebpBlendingMethod.DoNotBlend : WebpBlendingMethod.AlphaBlending, + (flags & 1) == 1 ? WebpDisposalMethod.Dispose : WebpDisposalMethod.DoNotDispose) + { + } + + public WebpFrameData(uint x, uint y, uint width, uint height, uint duration, WebpBlendingMethod blendingMethod, WebpDisposalMethod disposalMethod) + : this(0, x, y, width, height, duration, blendingMethod, disposalMethod) + { + } + + /// + /// Gets the animation chunk size. + /// + public uint DataSize { get; } + + /// + /// Gets the X coordinate of the upper left corner of the frame is Frame X * 2. + /// + public uint X { get; } + + /// + /// Gets the Y coordinate of the upper left corner of the frame is Frame Y * 2. + /// + public uint Y { get; } + + /// + /// Gets the width of the frame. + /// + public uint Width { get; } + + /// + /// Gets the height of the frame. + /// + public uint Height { get; } + + /// + /// Gets the time to wait before displaying the next frame, in 1 millisecond units. + /// Note the interpretation of frame duration of 0 (and often smaller then 10) is implementation defined. + /// + public uint Duration { get; } + + /// + /// Gets how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas. + /// + public WebpBlendingMethod BlendingMethod { get; } + + /// + /// Gets how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. + /// + public WebpDisposalMethod DisposalMethod { get; } + + public Rectangle Bounds => new((int)this.X * 2, (int)this.Y * 2, (int)this.Width, (int)this.Height); + + /// + /// Writes the animation frame() to the stream. + /// + /// The stream to write to. + public long WriteHeaderTo(Stream stream) + { + byte flags = 0; + + if (this.BlendingMethod is WebpBlendingMethod.DoNotBlend) + { + // Set blending flag. + flags |= 2; + } + + if (this.DisposalMethod is WebpDisposalMethod.Dispose) + { + // Set disposal flag. + flags |= 1; + } + + long pos = RiffHelper.BeginWriteChunk(stream, (uint)WebpChunkType.FrameData); + + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.X); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Y); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Width - 1); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Height - 1); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Duration); + stream.WriteByte(flags); + + return pos; + } + + /// + /// Reads the animation frame header. + /// + /// The stream to read from. + /// Animation frame data. + public static WebpFrameData Parse(Stream stream) + { + Span buffer = stackalloc byte[4]; + + WebpFrameData data = new( + dataSize: WebpChunkParsingUtils.ReadChunkSize(stream, buffer), + x: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer), + y: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer), + width: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1, + height: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1, + duration: WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer), + flags: stream.ReadByte()); + + return data; + } +} diff --git a/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs b/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs new file mode 100644 index 0000000000..70d6870ce4 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs @@ -0,0 +1,113 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Webp.Chunks; + +internal readonly struct WebpVp8X +{ + public WebpVp8X(bool hasAnimation, bool hasXmp, bool hasExif, bool hasAlpha, bool hasIcc, uint width, uint height) + { + this.HasAnimation = hasAnimation; + this.HasXmp = hasXmp; + this.HasExif = hasExif; + this.HasAlpha = hasAlpha; + this.HasIcc = hasIcc; + this.Width = width; + this.Height = height; + } + + /// + /// Gets a value indicating whether this is an animated image. Data in 'ANIM' and 'ANMF' Chunks should be used to control the animation. + /// + public bool HasAnimation { get; } + + /// + /// Gets a value indicating whether the file contains XMP metadata. + /// + public bool HasXmp { get; } + + /// + /// Gets a value indicating whether the file contains Exif metadata. + /// + public bool HasExif { get; } + + /// + /// Gets a value indicating whether any of the frames of the image contain transparency information ("alpha"). + /// + public bool HasAlpha { get; } + + /// + /// Gets a value indicating whether the file contains an 'ICCP' Chunk. + /// + public bool HasIcc { get; } + + /// + /// Gets width of the canvas in pixels. (uint24) + /// + public uint Width { get; } + + /// + /// Gets height of the canvas in pixels. (uint24) + /// + public uint Height { get; } + + public void Validate(uint maxDimension, ulong maxCanvasPixels) + { + if (this.Width > maxDimension || this.Height > maxDimension) + { + WebpThrowHelper.ThrowInvalidImageDimensions($"Image width or height exceeds maximum allowed dimension of {maxDimension}"); + } + + // The spec states that the product of Canvas Width and Canvas Height MUST be at most 2^32 - 1. + if (this.Width * this.Height > maxCanvasPixels) + { + WebpThrowHelper.ThrowInvalidImageDimensions("The product of image width and height MUST be at most 2^32 - 1"); + } + } + + public void WriteTo(Stream stream) + { + byte flags = 0; + + if (this.HasAnimation) + { + // Set animated flag. + flags |= 2; + } + + if (this.HasXmp) + { + // Set xmp bit. + flags |= 4; + } + + if (this.HasExif) + { + // Set exif bit. + flags |= 8; + } + + if (this.HasAlpha) + { + // Set alpha bit. + flags |= 16; + } + + if (this.HasIcc) + { + // Set icc flag. + flags |= 32; + } + + long pos = RiffHelper.BeginWriteChunk(stream, (uint)WebpChunkType.Vp8X); + + stream.WriteByte(flags); + stream.Position += 3; // Reserved bytes + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Width - 1); + WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Height - 1); + + RiffHelper.EndWriteChunk(stream, pos); + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 42aa667ac5..fe0131a2aa 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -6,7 +6,9 @@ using System.Buffers; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Webp.BitWriter; +using SixLabors.ImageSharp.Formats.Webp.Chunks; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; @@ -308,16 +310,15 @@ internal class Vp8LEncoder : IDisposable WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata(); // TODO: If we can clip the indexed frame for transparent bounds we can set properties here. - prevPosition = BitWriterBase.WriteAnimationFrame(stream, new WebpFrameData - { - X = 0, - Y = 0, - Width = (uint)frame.Width, - Height = (uint)frame.Height, - Duration = frameMetadata.FrameDelay, - BlendingMethod = frameMetadata.BlendMethod, - DisposalMethod = frameMetadata.DisposalMethod - }); + prevPosition = new WebpFrameData( + 0, + 0, + (uint)frame.Width, + (uint)frame.Height, + frameMetadata.FrameDelay, + frameMetadata.BlendMethod, + frameMetadata.DisposalMethod) + .WriteHeaderTo(stream); } // Write bytes from the bitwriter buffer to the stream. @@ -325,7 +326,7 @@ internal class Vp8LEncoder : IDisposable if (hasAnimation) { - BitWriterBase.OverwriteFrameSize(stream, prevPosition); + RiffHelper.EndWriteChunk(stream, prevPosition); } } diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 3b73023062..98e50bb9c2 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -4,7 +4,9 @@ using System.Buffers; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Webp.BitWriter; +using SixLabors.ImageSharp.Formats.Webp.Chunks; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; @@ -478,16 +480,15 @@ internal class Vp8Encoder : IDisposable WebpFrameMetadata frameMetadata = frame.Metadata.GetWebpMetadata(); // TODO: If we can clip the indexed frame for transparent bounds we can set properties here. - prevPosition = BitWriterBase.WriteAnimationFrame(stream, new WebpFrameData - { - X = 0, - Y = 0, - Width = (uint)frame.Width, - Height = (uint)frame.Height, - Duration = frameMetadata.FrameDelay, - BlendingMethod = frameMetadata.BlendMethod, - DisposalMethod = frameMetadata.DisposalMethod - }); + prevPosition = new WebpFrameData( + 0, + 0, + (uint)frame.Width, + (uint)frame.Height, + frameMetadata.FrameDelay, + frameMetadata.BlendMethod, + frameMetadata.DisposalMethod) + .WriteHeaderTo(stream); } if (hasAlpha) @@ -501,7 +502,7 @@ internal class Vp8Encoder : IDisposable if (hasAnimation) { - BitWriterBase.OverwriteFrameSize(stream, prevPosition); + RiffHelper.EndWriteChunk(stream, prevPosition); } } finally diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index fad6ca16cc..f0e4093194 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; +using SixLabors.ImageSharp.Formats.Webp.Chunks; using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.IO; @@ -99,7 +100,7 @@ internal class WebpAnimationDecoder : IDisposable remainingBytes -= 4; switch (chunkType) { - case WebpChunkType.Animation: + case WebpChunkType.FrameData: Color backgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore ? new Color(new Bgra32(0, 0, 0, 0)) : features.AnimationBackgroundColor!.Value; diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs index f4e40090cf..80ffe8a996 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs @@ -106,14 +106,14 @@ internal static class WebpChunkParsingUtils WebpThrowHelper.ThrowImageFormatException("bad partition length"); } - Vp8FrameHeader vp8FrameHeader = new Vp8FrameHeader + Vp8FrameHeader vp8FrameHeader = new() { KeyFrame = true, Profile = (sbyte)version, PartitionLength = partitionLength }; - Vp8BitReader bitReader = new Vp8BitReader(stream, remaining, memoryAllocator, partitionLength) { Remaining = remaining }; + Vp8BitReader bitReader = new(stream, remaining, memoryAllocator, partitionLength) { Remaining = remaining }; return new WebpImageInfo { @@ -139,7 +139,7 @@ internal static class WebpChunkParsingUtils // VP8 data size. uint imageDataSize = ReadChunkSize(stream, buffer); - Vp8LBitReader bitReader = new Vp8LBitReader(stream, imageDataSize, memoryAllocator); + Vp8LBitReader bitReader = new(stream, imageDataSize, memoryAllocator); // One byte signature, should be 0x2f. uint signature = bitReader.ReadValue(8); @@ -231,7 +231,7 @@ internal static class WebpChunkParsingUtils uint height = ReadUInt24LittleEndian(stream, buffer) + 1; // Read all the chunks in the order they occur. - WebpImageInfo info = new WebpImageInfo + WebpImageInfo info = new() { Width = width, Height = height, @@ -247,7 +247,7 @@ internal static class WebpChunkParsingUtils /// The stream to read from. /// The buffer to store the read data into. /// A unsigned 24 bit integer. - public static uint ReadUInt24LittleEndian(BufferedReadStream stream, Span buffer) + public static uint ReadUInt24LittleEndian(Stream stream, Span buffer) { if (stream.Read(buffer, 0, 3) == 3) { @@ -286,14 +286,14 @@ internal static class WebpChunkParsingUtils /// The stream to read the data from. /// Buffer to store the data read from the stream. /// The chunk size in bytes. - public static uint ReadChunkSize(BufferedReadStream stream, Span buffer) + public static uint ReadChunkSize(Stream stream, Span buffer) { - DebugGuard.IsTrue(buffer.Length == 4, "buffer has wrong length"); + DebugGuard.IsTrue(buffer.Length is 4, "buffer has wrong length"); - if (stream.Read(buffer) == 4) + if (stream.Read(buffer) is 4) { uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer); - return chunkSize % 2 == 0 ? chunkSize : chunkSize + 1; + return chunkSize % 2 is 0 ? chunkSize : chunkSize + 1; } throw new ImageFormatException("Invalid Webp data, could not read chunk size."); diff --git a/src/ImageSharp/Formats/Webp/WebpChunkType.cs b/src/ImageSharp/Formats/Webp/WebpChunkType.cs index 5836dc6c09..12e3297775 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkType.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkType.cs @@ -61,5 +61,5 @@ internal enum WebpChunkType : uint /// For animated images, this chunk contains information about a single frame. If the Animation flag is not set, then this chunk SHOULD NOT be present. /// /// ANMF (Multiple) - Animation = 0x414E4D46, + FrameData = 0x414E4D46, } diff --git a/src/ImageSharp/Formats/Webp/WebpConstants.cs b/src/ImageSharp/Formats/Webp/WebpConstants.cs index 1433772757..818c843ea9 100644 --- a/src/ImageSharp/Formats/Webp/WebpConstants.cs +++ b/src/ImageSharp/Formats/Webp/WebpConstants.cs @@ -55,6 +55,11 @@ internal static class WebpConstants 0x50 // P }; + /// + /// The header bytes identifying a Webp. + /// + public const string WebpFourCc = "WEBP"; + /// /// 3 bits reserved for version. /// @@ -70,11 +75,6 @@ internal static class WebpConstants /// public const int Vp8FrameHeaderSize = 10; - /// - /// Size of a VP8X chunk in bytes. - /// - public const int Vp8XChunkSize = 10; - /// /// Size of a chunk header. /// diff --git a/src/ImageSharp/Formats/Webp/WebpFrameData.cs b/src/ImageSharp/Formats/Webp/WebpFrameData.cs deleted file mode 100644 index 93c5d10dcd..0000000000 --- a/src/ImageSharp/Formats/Webp/WebpFrameData.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.IO; - -namespace SixLabors.ImageSharp.Formats.Webp; - -internal struct WebpFrameData -{ - /// - /// The animation chunk size. - /// - public uint DataSize; - - /// - /// X(3) + Y(3) + Width(3) + Height(3) + Duration(3) + 1 byte for flags. - /// - public const uint HeaderSize = 16; - - /// - /// The X coordinate of the upper left corner of the frame is Frame X * 2. - /// - public uint X; - - /// - /// The Y coordinate of the upper left corner of the frame is Frame Y * 2. - /// - public uint Y; - - /// - /// The width of the frame. - /// - public uint Width; - - /// - /// The height of the frame. - /// - public uint Height; - - /// - /// The time to wait before displaying the next frame, in 1 millisecond units. - /// Note the interpretation of frame duration of 0 (and often smaller then 10) is implementation defined. - /// - public uint Duration; - - /// - /// Indicates how transparent pixels of the current frame are to be blended with corresponding pixels of the previous canvas. - /// - public WebpBlendingMethod BlendingMethod; - - /// - /// Indicates how the current frame is to be treated after it has been displayed (before rendering the next frame) on the canvas. - /// - public WebpDisposalMethod DisposalMethod; - - public readonly Rectangle Bounds => new((int)this.X * 2, (int)this.Y * 2, (int)this.Width, (int)this.Height); - - /// - /// Reads the animation frame header. - /// - /// The stream to read from. - /// Animation frame data. - public static WebpFrameData Parse(BufferedReadStream stream) - { - Span buffer = stackalloc byte[4]; - - WebpFrameData data = new() - { - DataSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer), - - // 3 bytes for the X coordinate of the upper left corner of the frame. - X = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer), - - // 3 bytes for the Y coordinate of the upper left corner of the frame. - Y = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer), - - // Frame width Minus One. - Width = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1, - - // Frame height Minus One. - Height = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) + 1, - - // Frame duration. - Duration = WebpChunkParsingUtils.ReadUInt24LittleEndian(stream, buffer) - }; - - byte flags = (byte)stream.ReadByte(); - data.DisposalMethod = (flags & 1) == 1 ? WebpDisposalMethod.Dispose : WebpDisposalMethod.DoNotDispose; - data.BlendingMethod = (flags & (1 << 1)) != 0 ? WebpBlendingMethod.DoNotBlend : WebpBlendingMethod.AlphaBlending; - - return data; - } -} From a477ac13bc358811cd4bb83c6ff0f19621f51ae5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 6 Nov 2023 21:21:11 +1000 Subject: [PATCH 23/24] Use correct alpha blending --- .../Formats/Webp/WebpAnimationDecoder.cs | 64 +++++++++---------- .../Formats/WebP/WebpDecoderTests.cs | 10 +++ tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Webp/landscape.webp | 3 + 4 files changed, 45 insertions(+), 33 deletions(-) create mode 100644 tests/Images/Input/Webp/landscape.webp diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index f0e4093194..66e69d9a43 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -200,13 +200,10 @@ internal class WebpAnimationDecoder : IDisposable this.RestoreToBackground(imageFrame, backgroundColor); } - using Buffer2D decodedImage = this.DecodeImageData(frameData, webpInfo); - DrawDecodedImageOnCanvas(decodedImage, imageFrame, regionRectangle); + using Buffer2D decodedImageFrame = this.DecodeImageFrameData(frameData, webpInfo); - if (previousFrame != null && frameData.BlendingMethod is WebpBlendingMethod.AlphaBlending) - { - this.AlphaBlend(previousFrame, imageFrame, regionRectangle); - } + bool blend = previousFrame != null && frameData.BlendingMethod == WebpBlendingMethod.AlphaBlending; + DrawDecodedImageFrameOnCanvas(decodedImageFrame, imageFrame, regionRectangle, blend); previousFrame = currentFrame ?? image.Frames.RootFrame; this.restoreArea = regionRectangle; @@ -253,7 +250,7 @@ internal class WebpAnimationDecoder : IDisposable /// The frame data. /// The webp information. /// A decoded image. - private Buffer2D DecodeImageData(WebpFrameData frameData, WebpImageInfo webpInfo) + private Buffer2D DecodeImageFrameData(WebpFrameData frameData, WebpImageInfo webpInfo) where TPixel : unmanaged, IPixel { ImageFrame decodedFrame = new(Configuration.Default, (int)frameData.Width, (int)frameData.Height); @@ -291,42 +288,43 @@ internal class WebpAnimationDecoder : IDisposable /// Draws the decoded image on canvas. The decoded image can be smaller the canvas. /// /// The type of the pixel. - /// The decoded image. + /// The decoded image. /// The image frame to draw into. /// The area of the frame. - private static void DrawDecodedImageOnCanvas(Buffer2D decodedImage, ImageFrame imageFrame, Rectangle restoreArea) + /// Whether to blend the decoded frame data onto the target frame. + private static void DrawDecodedImageFrameOnCanvas( + Buffer2D decodedImageFrame, + ImageFrame imageFrame, + Rectangle restoreArea, + bool blend) where TPixel : unmanaged, IPixel { + // Trim the destination frame to match the restore area. The source frame is already trimmed. Buffer2DRegion imageFramePixels = imageFrame.PixelBuffer.GetRegion(restoreArea); - int decodedRowIdx = 0; - for (int y = 0; y < restoreArea.Height; y++) + if (blend) { - Span framePixelRow = imageFramePixels.DangerousGetRowSpan(y); - Span decodedPixelRow = decodedImage.DangerousGetRowSpan(decodedRowIdx++)[..restoreArea.Width]; - decodedPixelRow.TryCopyTo(framePixelRow); + // The destination frame has already been prepopulated with the pixel data from the previous frame + // so blending will leave the desired result which takes into consideration restoration to the + // background color within the restore area. + PixelBlender blender = + PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); + + for (int y = 0; y < restoreArea.Height; y++) + { + Span framePixelRow = imageFramePixels.DangerousGetRowSpan(y); + Span decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y)[..restoreArea.Width]; + + blender.Blend(imageFrame.Configuration, framePixelRow, framePixelRow, decodedPixelRow, 1f); + } + + return; } - } - /// - /// After disposing of the previous frame, render the current frame on the canvas using alpha-blending. - /// If the current frame does not have an alpha channel, assume alpha value of 255, effectively replacing the rectangle. - /// - /// The pixel format. - /// The source image. - /// The destination image. - /// The area of the frame. - private void AlphaBlend(ImageFrame src, ImageFrame dst, Rectangle restoreArea) - where TPixel : unmanaged, IPixel - { - Buffer2DRegion srcPixels = src.PixelBuffer.GetRegion(restoreArea); - Buffer2DRegion dstPixels = dst.PixelBuffer.GetRegion(restoreArea); - PixelBlender blender = PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); for (int y = 0; y < restoreArea.Height; y++) { - Span srcPixelRow = srcPixels.DangerousGetRowSpan(y); - Span dstPixelRow = dstPixels.DangerousGetRowSpan(y); - - blender.Blend(this.configuration, dstPixelRow, srcPixelRow, dstPixelRow, 1f); + Span framePixelRow = imageFramePixels.DangerousGetRowSpan(y); + Span decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y)[..restoreArea.Width]; + decodedPixelRow.CopyTo(framePixelRow); } } diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs index c3a777c153..4b03671e16 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs @@ -357,6 +357,16 @@ public class WebpDecoderTests image.CompareToOriginal(provider, ReferenceDecoder); } + [Theory] + [WithFile(Lossy.AnimatedLandscape, PixelTypes.Rgba32)] + public void Decode_AnimatedLossy_AlphaBlending_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(WebpDecoder.Instance); + image.DebugSaveMultiFrame(provider); + image.CompareToOriginalMultiFrame(provider, ImageComparer.Exact); + } + [Theory] [WithFile(Lossless.LossLessCorruptImage1, PixelTypes.Rgba32)] [WithFile(Lossless.LossLessCorruptImage2, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 048b19dc5b..6ad93adfbd 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -681,6 +681,7 @@ public static class TestImages public static class Lossy { + public const string AnimatedLandscape = "Webp/landscape.webp"; public const string Earth = "Webp/earth_lossy.webp"; public const string WithExif = "Webp/exif_lossy.webp"; public const string WithExifNotEnoughData = "Webp/exif_lossy_not_enough_data.webp"; diff --git a/tests/Images/Input/Webp/landscape.webp b/tests/Images/Input/Webp/landscape.webp new file mode 100644 index 0000000000..5f1f31a055 --- /dev/null +++ b/tests/Images/Input/Webp/landscape.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e9f8b7ee87ecb59d8cee5e84320da7670eb5e274e1c0a7dd5f13fe3675be62a +size 26892 From f46137847bed3d6c63f1b1f29bf8538aa7a384b2 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 6 Nov 2023 21:42:20 +1000 Subject: [PATCH 24/24] Complete encoding tests --- .../Formats/WebP/WebpEncoderTests.cs | 32 +++++++++++++++---- ...Encode_AnimatedLossy_Rgba32_landscape.webp | 3 ++ ...imatedLossy_Rgba32_leo_animated_lossy.webp | 3 ++ 3 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_landscape.webp create mode 100644 tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_leo_animated_lossy.webp diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index d81c9eb93a..0ad684b277 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -24,22 +23,41 @@ public class WebpEncoderTests where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); - using MemoryStream memStream = new(); - image.SaveAsWebp(memStream, new() { FileFormat = WebpFileFormatType.Lossless }); + WebpEncoder encoder = new() + { + FileFormat = WebpFileFormatType.Lossless, + Quality = 100 + }; + + // Always save as we need to compare the encoded output. + provider.Utility.SaveTestOutputFile(image, "webp", encoder); - // TODO: DebugSave, VerifySimilarity + // Compare encoded result + image.VerifyEncoder(provider, "webp", string.Empty, encoder); } [Theory] [WithFile(Lossy.Animated, PixelTypes.Rgba32)] + [WithFile(Lossy.AnimatedLandscape, PixelTypes.Rgba32)] public void Encode_AnimatedLossy(TestImageProvider provider) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); - using MemoryStream memStream = new(); - image.SaveAsWebp(memStream, new()); + WebpEncoder encoder = new() + { + FileFormat = WebpFileFormatType.Lossy, + Quality = 100 + }; + + // Always save as we need to compare the encoded output. + provider.Utility.SaveTestOutputFile(image, "webp", encoder); - // TODO: DebugSave, VerifySimilarity + // Compare encoded result + // The reference decoder seems to produce differences up to 0.1% but the input/output have been + // checked to be correct. + string path = provider.Utility.GetTestOutputFileName("webp", null, true); + using Image encoded = Image.Load(path); + encoded.CompareToReferenceOutput(ImageComparer.Tolerant(0.01f), provider, null, "webp"); } [Theory] diff --git a/tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_landscape.webp b/tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_landscape.webp new file mode 100644 index 0000000000..2312cb8576 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_landscape.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9ece3c7acc6f40318e3cda6b0189607df6b9b60dd112212c72ec0f6aa26431d +size 409346 diff --git a/tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_leo_animated_lossy.webp b/tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_leo_animated_lossy.webp new file mode 100644 index 0000000000..8474504da7 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/WebpEncoderTests/Encode_AnimatedLossy_Rgba32_leo_animated_lossy.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71800dff476f50ebd2a3d0cf0b4f5bef427a1c2cd8732b415511f10d3d93f9a0 +size 126382