From 95d36af396d5f57a9d79ca1e3a158c9b18e95a4a Mon Sep 17 00:00:00 2001 From: Poker Date: Mon, 23 Oct 2023 19:50:28 +0800 Subject: [PATCH] (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); }