Developing a WebP Encoder in C#
Explains the transform, LZ77, and Huffman layers of a C# encoder that produces VP8L lossless WebP bitstreams directly. Scope differences from libwebp and benefits in web publishing are examined.
The source code I examined is neither a wrapper around the WebP format nor a P/Invoke call to the native libwebp library. It produces the VP8L lossless WebP bitstream inside a RIFF container directly in C#. It analyzes the ARGB pixels of the image, selects a suitable transformation path, reduces spatial and color repetition, finds LZ77 matches, constructs Huffman tables, and writes the result bits in the order required by the file format.
I developed this library by studying the source code and format definition of the official WebP encoder and adapting them to the actual needs of my personal website. My objective was not to reproduce every encoding option in libwebp. It was to build an independent C# core capable of producing lossless web images with their alpha channel intact, without requiring an external WebP executable or a platform-specific native library.
Using the library actively on my website required more than producing a formally valid WebP file. It also had to provide meaningful file size, compatibility, and processing time under real deployment conditions. In that sense, this work is not an experiment with a file format, but an image-processing component with a direct role in a production system.
The emergence of WebP
Google announced WebP on September 30, 2010, with the aim of reducing image transfer size on the web. The initial version focused on lossy compression and applied the intra-frame prediction techniques of the VP8 video codec to still images. Image data was stored in a RIFF-based container with a basic overhead of approximately 20 bytes. Google's first large-scale study, conducted on approximately one million images obtained from the web, showed that WebP could provide substantial size savings compared with JPEG encoding of the period.
Lossless compression and alpha-channel support were announced in November 2011. This addition made WebP useful not only as a JPEG alternative for photographs, but also for PNG-like graphics, screenshots, icons, and transparent images. The ability to use lossy RGB with a lossless alpha channel in the same container also became one of the format's significant design features.
Over time, WebP gained an extended container structure supporting animation, ICC color profiles, EXIF, and XMP metadata. RFC 9649, published in November 2024, describes the WebP container, lossy VP8 and lossless VP8L streams, and the image/webp media type in one document. The RFC is informational, but it consolidates the format's interoperability principles in a current and central reference.
My implementation occupies a specific part of this broader family:
It produces single-frame images.
It uses a lossless VP8L bitstream.
It preserves ARGB and alpha information.
It constructs the simple RIFF and VP8L layout.
It does not perform lossy VP8 encoding.
It does not produce a VP8X container for animation, ICC, EXIF, or XMP.
This scope is sufficient for the graphics and transparent images used on my website. Rather than adding features that would turn the library into a general-purpose libwebp alternative, I implemented the encoding path I needed in a clear and auditable form.
Entropy-based transform selection
Lossless WebP encoding does not convert an image directly into Huffman codes. It first transforms regularities in the pixel distribution into a representation that can be compressed more effectively. The official VP8L specification defines four reversible transforms:
- Spatial prediction
- Color transform
- Subtract-green transform
- Color indexing
These transforms aim to reduce Shannon entropy in the remaining symbols by decreasing correlation between neighboring pixels and color channels.
In my code, transform selection is based on histograms constructed from the image. In addition to the red, green, blue, and alpha channels, I separately measure differences between consecutive pixels, differences between red and green, differences between blue and green, and their counterparts in the difference image.
The entropy of a histogram is calculated as:
\[ H(X)=-\sum_i p_i\log_2p_i \]
Entropy decreases when the distribution is concentrated in a small number of values. It increases when symbols are spread across a wide range with similar frequencies. Low entropy creates favorable conditions for Huffman coding to represent frequent values with shorter bit sequences.
The encoder compares five primary paths:
\[ E_{\text{raw}}=H(R)+H(G)+H(B)+H(A) \]
\[ E_{\text{palette}}=H(P) \]
\[ E_{\text{green}}=H(R-G)+H(G)+H(B-G)+H(A) \]
\[ E_{\text{prediction}}=H(\Delta R)+H(\Delta G)+H(\Delta B)+H(\Delta A) \]
\[ E_{\text{combined}}= H(\Delta R-\Delta G)+H(\Delta G)+ H(\Delta B-\Delta G)+H(\Delta A) \]
The path with the lowest estimated entropy is selected. This decision does not predict the exact final file size. It does not include the precise bit cost of transform headers, Huffman tables, or LZ77 matches. It is nevertheless a fast estimate that can make a reasonable decision without fully encoding the image through every option.
This approach is a smaller and purpose-specific counterpart to the idea of testing different compression configurations and comparing their costs in the official encoder. The current libwebp lossless encoder also evaluates configurations including palette, subtract-green, spatial prediction, and color transforms. My C# implementation applies a selected subset of these through a more limited decision model.
Palette and color correlation
If the image contains at most 256 distinct ARGB values, a color table is created. Each unique pixel is converted into its index in that table. If the color count is very small, multiple indices are packed into the green channel of the same byte:
Eight pixels for at most 2 colors
Four pixels for at most 4 colors
Two pixels for at most 16 colors
One pixel for more colors
This layout matches the official WebP color-indexing transform. Packing indices into the same byte for small palettes does more than reduce the number of bits. By turning adjacent indices into a shared symbol, it gives entropy coding a joint-distribution advantage similar in effect to arithmetic coding.
The palette is not written as an independent list of raw colors. The first color is written directly, while subsequent colors are encoded as channel differences from the preceding color. In a table where similar colors occur consecutively, these differences can be concentrated in a small value range.
The subtract-green transform uses another image property. RGB channels in natural and synthetic images are often not independent. Red and blue values can move together with green. In that case, the encoder stores:
\[ R'=R-G \]
\[ B'=B-G \]
Green remains unchanged. During decoding, green is added back to red and blue. Because the operation uses modular byte arithmetic, it is fully reversible. The official WebP specification defines it as a separate and shorter transform because it requires no additional data, although it could also be expressed through the full color transform.
Block-based spatial prediction
One of the strongest redundancies in images is the relationship between neighboring pixels. Adjacent pixels on a flat background are identical or close in value. Along an edge, values change in a particular direction. Predicting a pixel from its neighbors and encoding only the difference can concentrate the symbol distribution around zero.
VP8L defines 14 prediction modes using the left, top, top-left, and top-right neighbors. These include constant black, left pixel, top pixel, various averages, selective prediction, and clamped linear predictions.
In my implementation, the image is divided into square regions. Processing begins with 16 x 16 blocks. For very large images, block size is increased so that the total number of blocks remains below approximately two thousand. This decision prevents prediction-mode search from growing without control on large images.
I test all 14 prediction modes for each block. For every candidate mode, the ARGB differences between the actual and predicted pixels are calculated, and the histogram entropy of the four channels is measured. The mode producing the lowest total entropy is selected for that block.
The evaluation is not limited to the local block histogram. Histograms accumulated from previously processed blocks are combined with the values of the candidate block. The choice therefore considers not only error within the block, but also the symbol distribution that will arise across the full stream. A prediction with slightly higher local error can be preferred if it makes the global Huffman distribution more regular.
Selected prediction modes are encoded as a separate low-resolution image. The actual pixels are retained as prediction residuals. An important WebP design property is visible here: side images carrying encoding parameters are compressed with the same lossless image-coding mechanism.
Because the number of prediction modes is fixed, the fundamental cost in terms of pixel count N is linear:
\[ T_{\text{prediction}}(N)=\Theta(14N)=\Theta(N) \]
The constant factor of 14 is nevertheless significant in practice. Each candidate requires channel differences, histogram copies, and logarithmic entropy calculations. Adaptive block sizing, which limits the number of blocks, keeps this fixed cost under control.
LZ77 and two-dimensional distance
The transformed image is still a pixel sequence. In the second stage, the encoder searches for pixel sequences that have appeared previously. When the same sequence is found, it emits a length and backward-distance pair rather than writing the colors again.
This is LZ77 dictionary compression applied to an image:
\[ (\text{length},\text{distance}) \]
In the official VP8L structure, match length is at most 4096 pixels. Distance values are not encoded only as linear-index differences. A two-dimensional distance mapping assigns small codes to locations in nearby rows and columns. This reflects the fact that repeated structures in images are often spatially close.
In my search core, I produce a custom hash from three consecutive pixels. Previous positions falling into the same hash bucket are retained in a linked chain. For each new position, at most 100 previous candidates are examined, and match length is limited to 4096 pixels.
This design has three important consequences:
The entire preceding image is not rescanned at every position.
The number of candidates does not grow without bound in highly repetitive data.
The upper bound of search time for an image remains predictable.
In general notation, with candidate limit C and match limit L, cost is:
\[ O(NCL) \]
Because the source code fixes C=100 and L=4096, asymptotic behavior is linear in image size. The constants can still be high. Actual performance depends on hash distribution and how early match comparisons terminate.
The encoder greedily selects the longest match it finds. It does not implement the complete cost-based backward-reference optimizations of the official libwebp encoder. It does not comprehensively examine cases in which a shorter but closer match might have lower bit cost. This choice gives up part of the possible compression density in exchange for a small and predictable search core.
Constructing the Huffman bitstream
After the LZ77 stage, the stream contains three types of elements:
Raw ARGB pixel
Length and distance reference
Color-cache index, when enabled
VP8L encodes these through five separate prefix codes:
- Green channel, match length, and color cache
- Red channel
- Blue channel
- Alpha channel
- Match distance
In my library, a histogram is built for each alphabet. A Huffman tree is constructed from the weights, and canonical codes are produced from the tree depths. With canonical representation, complete bit patterns do not need to be written to the file. Codes can be reconstructed from symbol order and code lengths.
Code lengths are not transmitted raw either. Repeated lengths and long runs of zero are run-length encoded with special symbols 16, 17, and 18. A second Huffman tree is then built for these code-length symbols. The definitions of the main compression tables are thus compressed as well.
This layer is where the source code goes beyond a simple image converter. Producing a valid WebP file requires more than calculating correct pixels. Bits must be written least-significant bit first, simple and normal Huffman tables must be distinguished, additional length bits must appear in the correct positions, and RIFF chunk sizes must be backfilled after encoding.
The library can write output to any target Stream. Because RIFF and VP8L lengths must later be written into their headers, output is first completed in a memory stream and then copied to targets that are not seekable or cannot be used directly. The API is stream-based, but the encoding process is not fully constant-memory streaming.
How it differs from the official encoder
My source code implements the fundamental layers of VP8L but does not have the same search space as the current libwebp lossless encoder. The official library includes different effort levels, cross-color transformation, variable histogram regions, meta-Huffman images, color-cache selection, advanced palette ordering, near-lossless options, and more comprehensive backward-reference cost analysis. The official project also provides the cwebp and dwebp tools alongside the WebP encoding and decoding library.
In my code:
The full cross-color transform is not applied.
A single global Huffman group is used.
No meta-Huffman image is produced.
A color-cache class and bitstream support exist, but active analysis selects a cache-bit count of zero.
LZ77 parsing uses a bounded hash chain and longest-match strategy.
The implementation focuses on the simple lossless VP8L container.
These omissions do not imply format incompatibility. WebP transforms and the color cache are optional. An encoder can produce a valid and fully decodable VP8L stream without using every compression opportunity. The difference lies not in file validity, but in the balance between the smallest attainable size and encoding time.
The correct position of the library is therefore not a complete rewrite of libwebp, but an independent managed lossless WebP encoder developed for web assets.
Benefits on the website
The first benefit of developing the WebP encoder in my own C# infrastructure was deployment control. Producing a WebP bitstream did not require loading a different native library for each operating system, managing P/Invoke signatures, or launching an external conversion process. Encoding behavior remained visible and auditable within the application's own source code.
The second benefit was the ability to choose according to target image usage. Palette transformation can be used for low-color graphics, subtract-green for images with correlated channels, and prediction transformation for images with spatial regularity. Instead of one fixed compression path, the encoder selects according to the statistical structure of the image.
The third benefit is transfer size. Google's general WebP data states that lossless WebP can be approximately 26 percent smaller than PNG on average. A separate study of twelve thousand transparent web images reported that WebP provided denser compression than size-optimized PNG in most samples. These ratios are not guaranteed for every image. Results depend on content type and encoder settings.
Smaller image files reduce the number of bytes transferred by a web page, server egress traffic, and the space occupied in client caches. On pages presenting many images simultaneously, such as article listings, a small saving per file becomes meaningful in the page total. The primary development objective of WebP is likewise to speed web delivery through smaller, richer images.
The absence of metadata in simple VP8L output also produces a clean result for web publication. Source information such as EXIF and XMP is not transferred automatically. This removes unnecessary metadata overhead, but it is an explicit limitation for professional workflows that must preserve a color profile or source metadata.
Lossy WebP can produce smaller results for photograph-heavy content. Because this library does not encode lossy VP8, it is not a universal solution for all image classes. Sharp-line graphics, images containing text, interface elements, diagrams, and transparent graphics are natural use cases for the lossless path.
From theory to a working bitstream
In the book Artificial Intelligence from Theory to Practice, published by Nobel Academic Publishing in December 2022, the chapter titled "Image and Audio Processing," which I wrote, examined the numerical representation of images, enhancement processes, and feature extraction in a theoretical framework. The publisher's record confirms the book, its ISBN, and my inclusion among its authors.
This WebP encoder is a lower-level implementation of the same approach. The image is not merely a pixel array. It is a data source with correlation between color channels, spatial dependency between neighboring pixels, repeated regions, and probability distributions of symbols.
The algorithm processes this structure in stages:
\[ \text{ARGB image} \rightarrow \text{statistical analysis} \rightarrow \text{reversible transform} \rightarrow \text{LZ77 references} \rightarrow \text{Huffman codes} \rightarrow \text{VP8L bitstream} \]
The library's main technical value does not come from its ability to write a file with a WebP extension. The data model and compression decisions behind an official encoder were examined, reconstructed in C#, and turned into a real software component used on my personal website for an extended period.
Without carrying the full complexity of the general-purpose official encoder, it combines purpose-specific transforms, bounded LZ77 search, and canonical Huffman coding. This limitation keeps the code understandable and allows the lossless WebP output required by my web-publishing pipeline to be produced without external dependencies.