diff --git a/ImageSharp.sln b/ImageSharp.sln index f1d4afef41..8fc463c720 100644 --- a/ImageSharp.sln +++ b/ImageSharp.sln @@ -332,6 +332,165 @@ Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "SharedInfrastructure", "sha EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp.Tests.ProfilingSandbox", "tests\ImageSharp.Tests.ProfilingSandbox\ImageSharp.Tests.ProfilingSandbox.csproj", "{FC527290-2F22-432C-B77B-6E815726B02C}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Webp", "Webp", "{983A31E2-5E26-4058-BD6E-03B4922D4BBF}" + ProjectSection(SolutionItems) = preProject + tests\Images\Input\WebP\alpha_color_cache.webp = tests\Images\Input\WebP\alpha_color_cache.webp + tests\Images\Input\WebP\alpha_filter_0_method_0.webp = tests\Images\Input\WebP\alpha_filter_0_method_0.webp + tests\Images\Input\WebP\alpha_filter_0_method_1.webp = tests\Images\Input\WebP\alpha_filter_0_method_1.webp + tests\Images\Input\WebP\alpha_filter_1.webp = tests\Images\Input\WebP\alpha_filter_1.webp + tests\Images\Input\WebP\alpha_filter_1_method_0.webp = tests\Images\Input\WebP\alpha_filter_1_method_0.webp + tests\Images\Input\WebP\alpha_filter_1_method_1.webp = tests\Images\Input\WebP\alpha_filter_1_method_1.webp + tests\Images\Input\WebP\alpha_filter_2.webp = tests\Images\Input\WebP\alpha_filter_2.webp + tests\Images\Input\WebP\alpha_filter_2_method_0.webp = tests\Images\Input\WebP\alpha_filter_2_method_0.webp + tests\Images\Input\WebP\alpha_filter_2_method_1.webp = tests\Images\Input\WebP\alpha_filter_2_method_1.webp + tests\Images\Input\WebP\alpha_filter_3.webp = tests\Images\Input\WebP\alpha_filter_3.webp + tests\Images\Input\WebP\alpha_filter_3_method_0.webp = tests\Images\Input\WebP\alpha_filter_3_method_0.webp + tests\Images\Input\WebP\alpha_filter_3_method_1.webp = tests\Images\Input\WebP\alpha_filter_3_method_1.webp + tests\Images\Input\WebP\alpha_no_compression.webp = tests\Images\Input\WebP\alpha_no_compression.webp + tests\Images\Input\WebP\animated-webp.webp = tests\Images\Input\WebP\animated-webp.webp + tests\Images\Input\WebP\animated2.webp = tests\Images\Input\WebP\animated2.webp + tests\Images\Input\WebP\animated3.webp = tests\Images\Input\WebP\animated3.webp + tests\Images\Input\WebP\animated_lossy.webp = tests\Images\Input\WebP\animated_lossy.webp + tests\Images\Input\WebP\bad_palette_index.webp = tests\Images\Input\WebP\bad_palette_index.webp + tests\Images\Input\WebP\big_endian_bug_393.webp = tests\Images\Input\WebP\big_endian_bug_393.webp + tests\Images\Input\WebP\bryce.webp = tests\Images\Input\WebP\bryce.webp + tests\Images\Input\WebP\bug3.webp = tests\Images\Input\WebP\bug3.webp + tests\Images\Input\WebP\color_cache_bits_11.webp = tests\Images\Input\WebP\color_cache_bits_11.webp + tests\Images\Input\WebP\grid.bmp = tests\Images\Input\WebP\grid.bmp + tests\Images\Input\WebP\grid.pam = tests\Images\Input\WebP\grid.pam + tests\Images\Input\WebP\grid.pgm = tests\Images\Input\WebP\grid.pgm + tests\Images\Input\WebP\grid.png = tests\Images\Input\WebP\grid.png + tests\Images\Input\WebP\grid.ppm = tests\Images\Input\WebP\grid.ppm + tests\Images\Input\WebP\grid.tiff = tests\Images\Input\WebP\grid.tiff + tests\Images\Input\WebP\libwebp_tests.md5 = tests\Images\Input\WebP\libwebp_tests.md5 + tests\Images\Input\WebP\lossless1.webp = tests\Images\Input\WebP\lossless1.webp + tests\Images\Input\WebP\lossless2.webp = tests\Images\Input\WebP\lossless2.webp + tests\Images\Input\WebP\lossless3.webp = tests\Images\Input\WebP\lossless3.webp + tests\Images\Input\WebP\lossless4.webp = tests\Images\Input\WebP\lossless4.webp + tests\Images\Input\WebP\lossless_big_random_alpha.webp = tests\Images\Input\WebP\lossless_big_random_alpha.webp + tests\Images\Input\WebP\lossless_color_transform.bmp = tests\Images\Input\WebP\lossless_color_transform.bmp + tests\Images\Input\WebP\lossless_color_transform.pam = tests\Images\Input\WebP\lossless_color_transform.pam + tests\Images\Input\WebP\lossless_color_transform.pgm = tests\Images\Input\WebP\lossless_color_transform.pgm + tests\Images\Input\WebP\lossless_color_transform.ppm = tests\Images\Input\WebP\lossless_color_transform.ppm + tests\Images\Input\WebP\lossless_color_transform.tiff = tests\Images\Input\WebP\lossless_color_transform.tiff + tests\Images\Input\WebP\lossless_color_transform.webp = tests\Images\Input\WebP\lossless_color_transform.webp + tests\Images\Input\WebP\lossless_vec_1_0.webp = tests\Images\Input\WebP\lossless_vec_1_0.webp + tests\Images\Input\WebP\lossless_vec_1_1.webp = tests\Images\Input\WebP\lossless_vec_1_1.webp + tests\Images\Input\WebP\lossless_vec_1_10.webp = tests\Images\Input\WebP\lossless_vec_1_10.webp + tests\Images\Input\WebP\lossless_vec_1_11.webp = tests\Images\Input\WebP\lossless_vec_1_11.webp + tests\Images\Input\WebP\lossless_vec_1_12.webp = tests\Images\Input\WebP\lossless_vec_1_12.webp + tests\Images\Input\WebP\lossless_vec_1_13.webp = tests\Images\Input\WebP\lossless_vec_1_13.webp + tests\Images\Input\WebP\lossless_vec_1_14.webp = tests\Images\Input\WebP\lossless_vec_1_14.webp + tests\Images\Input\WebP\lossless_vec_1_15.webp = tests\Images\Input\WebP\lossless_vec_1_15.webp + tests\Images\Input\WebP\lossless_vec_1_2.webp = tests\Images\Input\WebP\lossless_vec_1_2.webp + tests\Images\Input\WebP\lossless_vec_1_3.webp = tests\Images\Input\WebP\lossless_vec_1_3.webp + tests\Images\Input\WebP\lossless_vec_1_4.webp = tests\Images\Input\WebP\lossless_vec_1_4.webp + tests\Images\Input\WebP\lossless_vec_1_5.webp = tests\Images\Input\WebP\lossless_vec_1_5.webp + tests\Images\Input\WebP\lossless_vec_1_6.webp = tests\Images\Input\WebP\lossless_vec_1_6.webp + tests\Images\Input\WebP\lossless_vec_1_7.webp = tests\Images\Input\WebP\lossless_vec_1_7.webp + tests\Images\Input\WebP\lossless_vec_1_8.webp = tests\Images\Input\WebP\lossless_vec_1_8.webp + tests\Images\Input\WebP\lossless_vec_1_9.webp = tests\Images\Input\WebP\lossless_vec_1_9.webp + tests\Images\Input\WebP\lossless_vec_2_0.webp = tests\Images\Input\WebP\lossless_vec_2_0.webp + tests\Images\Input\WebP\lossless_vec_2_1.webp = tests\Images\Input\WebP\lossless_vec_2_1.webp + tests\Images\Input\WebP\lossless_vec_2_10.webp = tests\Images\Input\WebP\lossless_vec_2_10.webp + tests\Images\Input\WebP\lossless_vec_2_11.webp = tests\Images\Input\WebP\lossless_vec_2_11.webp + tests\Images\Input\WebP\lossless_vec_2_12.webp = tests\Images\Input\WebP\lossless_vec_2_12.webp + tests\Images\Input\WebP\lossless_vec_2_13.webp = tests\Images\Input\WebP\lossless_vec_2_13.webp + tests\Images\Input\WebP\lossless_vec_2_14.webp = tests\Images\Input\WebP\lossless_vec_2_14.webp + tests\Images\Input\WebP\lossless_vec_2_15.webp = tests\Images\Input\WebP\lossless_vec_2_15.webp + tests\Images\Input\WebP\lossless_vec_2_2.webp = tests\Images\Input\WebP\lossless_vec_2_2.webp + tests\Images\Input\WebP\lossless_vec_2_3.webp = tests\Images\Input\WebP\lossless_vec_2_3.webp + tests\Images\Input\WebP\lossless_vec_2_4.webp = tests\Images\Input\WebP\lossless_vec_2_4.webp + tests\Images\Input\WebP\lossless_vec_2_5.webp = tests\Images\Input\WebP\lossless_vec_2_5.webp + tests\Images\Input\WebP\lossless_vec_2_6.webp = tests\Images\Input\WebP\lossless_vec_2_6.webp + tests\Images\Input\WebP\lossless_vec_2_7.webp = tests\Images\Input\WebP\lossless_vec_2_7.webp + tests\Images\Input\WebP\lossless_vec_2_8.webp = tests\Images\Input\WebP\lossless_vec_2_8.webp + tests\Images\Input\WebP\lossless_vec_2_9.webp = tests\Images\Input\WebP\lossless_vec_2_9.webp + tests\Images\Input\WebP\lossless_vec_list.txt = tests\Images\Input\WebP\lossless_vec_list.txt + tests\Images\Input\WebP\lossy_alpha1.webp = tests\Images\Input\WebP\lossy_alpha1.webp + tests\Images\Input\WebP\lossy_alpha2.webp = tests\Images\Input\WebP\lossy_alpha2.webp + tests\Images\Input\WebP\lossy_alpha3.webp = tests\Images\Input\WebP\lossy_alpha3.webp + tests\Images\Input\WebP\lossy_alpha4.webp = tests\Images\Input\WebP\lossy_alpha4.webp + tests\Images\Input\WebP\lossy_extreme_probabilities.webp = tests\Images\Input\WebP\lossy_extreme_probabilities.webp + tests\Images\Input\WebP\lossy_q0_f100.webp = tests\Images\Input\WebP\lossy_q0_f100.webp + tests\Images\Input\WebP\near_lossless_75.webp = tests\Images\Input\WebP\near_lossless_75.webp + tests\Images\Input\WebP\peak.bmp = tests\Images\Input\WebP\peak.bmp + tests\Images\Input\WebP\peak.pam = tests\Images\Input\WebP\peak.pam + tests\Images\Input\WebP\peak.pgm = tests\Images\Input\WebP\peak.pgm + tests\Images\Input\WebP\peak.png = tests\Images\Input\WebP\peak.png + tests\Images\Input\WebP\peak.ppm = tests\Images\Input\WebP\peak.ppm + tests\Images\Input\WebP\peak.tiff = tests\Images\Input\WebP\peak.tiff + tests\Images\Input\WebP\segment01.webp = tests\Images\Input\WebP\segment01.webp + tests\Images\Input\WebP\segment02.webp = tests\Images\Input\WebP\segment02.webp + tests\Images\Input\WebP\segment03.webp = tests\Images\Input\WebP\segment03.webp + tests\Images\Input\WebP\small_13x1.webp = tests\Images\Input\WebP\small_13x1.webp + tests\Images\Input\WebP\small_1x1.webp = tests\Images\Input\WebP\small_1x1.webp + tests\Images\Input\WebP\small_1x13.webp = tests\Images\Input\WebP\small_1x13.webp + tests\Images\Input\WebP\small_31x13.webp = tests\Images\Input\WebP\small_31x13.webp + tests\Images\Input\WebP\test-nostrong.webp = tests\Images\Input\WebP\test-nostrong.webp + tests\Images\Input\WebP\test.webp = tests\Images\Input\WebP\test.webp + tests\Images\Input\WebP\test_cwebp.sh = tests\Images\Input\WebP\test_cwebp.sh + tests\Images\Input\WebP\test_dwebp.sh = tests\Images\Input\WebP\test_dwebp.sh + tests\Images\Input\WebP\test_lossless.sh = tests\Images\Input\WebP\test_lossless.sh + tests\Images\Input\WebP\very_short.webp = tests\Images\Input\WebP\very_short.webp + tests\Images\Input\WebP\vp80-00-comprehensive-001.webp = tests\Images\Input\WebP\vp80-00-comprehensive-001.webp + tests\Images\Input\WebP\vp80-00-comprehensive-002.webp = tests\Images\Input\WebP\vp80-00-comprehensive-002.webp + tests\Images\Input\WebP\vp80-00-comprehensive-003.webp = tests\Images\Input\WebP\vp80-00-comprehensive-003.webp + tests\Images\Input\WebP\vp80-00-comprehensive-004.webp = tests\Images\Input\WebP\vp80-00-comprehensive-004.webp + tests\Images\Input\WebP\vp80-00-comprehensive-005.webp = tests\Images\Input\WebP\vp80-00-comprehensive-005.webp + tests\Images\Input\WebP\vp80-00-comprehensive-006.webp = tests\Images\Input\WebP\vp80-00-comprehensive-006.webp + tests\Images\Input\WebP\vp80-00-comprehensive-007.webp = tests\Images\Input\WebP\vp80-00-comprehensive-007.webp + tests\Images\Input\WebP\vp80-00-comprehensive-008.webp = tests\Images\Input\WebP\vp80-00-comprehensive-008.webp + tests\Images\Input\WebP\vp80-00-comprehensive-009.webp = tests\Images\Input\WebP\vp80-00-comprehensive-009.webp + tests\Images\Input\WebP\vp80-00-comprehensive-010.webp = tests\Images\Input\WebP\vp80-00-comprehensive-010.webp + tests\Images\Input\WebP\vp80-00-comprehensive-011.webp = tests\Images\Input\WebP\vp80-00-comprehensive-011.webp + tests\Images\Input\WebP\vp80-00-comprehensive-012.webp = tests\Images\Input\WebP\vp80-00-comprehensive-012.webp + tests\Images\Input\WebP\vp80-00-comprehensive-013.webp = tests\Images\Input\WebP\vp80-00-comprehensive-013.webp + tests\Images\Input\WebP\vp80-00-comprehensive-014.webp = tests\Images\Input\WebP\vp80-00-comprehensive-014.webp + tests\Images\Input\WebP\vp80-00-comprehensive-015.webp = tests\Images\Input\WebP\vp80-00-comprehensive-015.webp + tests\Images\Input\WebP\vp80-00-comprehensive-016.webp = tests\Images\Input\WebP\vp80-00-comprehensive-016.webp + tests\Images\Input\WebP\vp80-00-comprehensive-017.webp = tests\Images\Input\WebP\vp80-00-comprehensive-017.webp + tests\Images\Input\WebP\vp80-01-intra-1400.webp = tests\Images\Input\WebP\vp80-01-intra-1400.webp + tests\Images\Input\WebP\vp80-01-intra-1411.webp = tests\Images\Input\WebP\vp80-01-intra-1411.webp + tests\Images\Input\WebP\vp80-01-intra-1416.webp = tests\Images\Input\WebP\vp80-01-intra-1416.webp + tests\Images\Input\WebP\vp80-01-intra-1417.webp = tests\Images\Input\WebP\vp80-01-intra-1417.webp + tests\Images\Input\WebP\vp80-02-inter-1402.webp = tests\Images\Input\WebP\vp80-02-inter-1402.webp + tests\Images\Input\WebP\vp80-02-inter-1412.webp = tests\Images\Input\WebP\vp80-02-inter-1412.webp + tests\Images\Input\WebP\vp80-02-inter-1418.webp = tests\Images\Input\WebP\vp80-02-inter-1418.webp + tests\Images\Input\WebP\vp80-02-inter-1424.webp = tests\Images\Input\WebP\vp80-02-inter-1424.webp + tests\Images\Input\WebP\vp80-03-segmentation-1401.webp = tests\Images\Input\WebP\vp80-03-segmentation-1401.webp + tests\Images\Input\WebP\vp80-03-segmentation-1403.webp = tests\Images\Input\WebP\vp80-03-segmentation-1403.webp + tests\Images\Input\WebP\vp80-03-segmentation-1407.webp = tests\Images\Input\WebP\vp80-03-segmentation-1407.webp + tests\Images\Input\WebP\vp80-03-segmentation-1408.webp = tests\Images\Input\WebP\vp80-03-segmentation-1408.webp + tests\Images\Input\WebP\vp80-03-segmentation-1409.webp = tests\Images\Input\WebP\vp80-03-segmentation-1409.webp + tests\Images\Input\WebP\vp80-03-segmentation-1410.webp = tests\Images\Input\WebP\vp80-03-segmentation-1410.webp + tests\Images\Input\WebP\vp80-03-segmentation-1413.webp = tests\Images\Input\WebP\vp80-03-segmentation-1413.webp + tests\Images\Input\WebP\vp80-03-segmentation-1414.webp = tests\Images\Input\WebP\vp80-03-segmentation-1414.webp + tests\Images\Input\WebP\vp80-03-segmentation-1415.webp = tests\Images\Input\WebP\vp80-03-segmentation-1415.webp + tests\Images\Input\WebP\vp80-03-segmentation-1425.webp = tests\Images\Input\WebP\vp80-03-segmentation-1425.webp + tests\Images\Input\WebP\vp80-03-segmentation-1426.webp = tests\Images\Input\WebP\vp80-03-segmentation-1426.webp + tests\Images\Input\WebP\vp80-03-segmentation-1427.webp = tests\Images\Input\WebP\vp80-03-segmentation-1427.webp + tests\Images\Input\WebP\vp80-03-segmentation-1432.webp = tests\Images\Input\WebP\vp80-03-segmentation-1432.webp + tests\Images\Input\WebP\vp80-03-segmentation-1435.webp = tests\Images\Input\WebP\vp80-03-segmentation-1435.webp + tests\Images\Input\WebP\vp80-03-segmentation-1436.webp = tests\Images\Input\WebP\vp80-03-segmentation-1436.webp + tests\Images\Input\WebP\vp80-03-segmentation-1437.webp = tests\Images\Input\WebP\vp80-03-segmentation-1437.webp + tests\Images\Input\WebP\vp80-03-segmentation-1441.webp = tests\Images\Input\WebP\vp80-03-segmentation-1441.webp + tests\Images\Input\WebP\vp80-03-segmentation-1442.webp = tests\Images\Input\WebP\vp80-03-segmentation-1442.webp + tests\Images\Input\WebP\vp80-04-partitions-1404.webp = tests\Images\Input\WebP\vp80-04-partitions-1404.webp + tests\Images\Input\WebP\vp80-04-partitions-1405.webp = tests\Images\Input\WebP\vp80-04-partitions-1405.webp + tests\Images\Input\WebP\vp80-04-partitions-1406.webp = tests\Images\Input\WebP\vp80-04-partitions-1406.webp + tests\Images\Input\WebP\vp80-05-sharpness-1428.webp = tests\Images\Input\WebP\vp80-05-sharpness-1428.webp + tests\Images\Input\WebP\vp80-05-sharpness-1429.webp = tests\Images\Input\WebP\vp80-05-sharpness-1429.webp + tests\Images\Input\WebP\vp80-05-sharpness-1430.webp = tests\Images\Input\WebP\vp80-05-sharpness-1430.webp + tests\Images\Input\WebP\vp80-05-sharpness-1431.webp = tests\Images\Input\WebP\vp80-05-sharpness-1431.webp + tests\Images\Input\WebP\vp80-05-sharpness-1433.webp = tests\Images\Input\WebP\vp80-05-sharpness-1433.webp + tests\Images\Input\WebP\vp80-05-sharpness-1434.webp = tests\Images\Input\WebP\vp80-05-sharpness-1434.webp + tests\Images\Input\WebP\vp80-05-sharpness-1438.webp = tests\Images\Input\WebP\vp80-05-sharpness-1438.webp + tests\Images\Input\WebP\vp80-05-sharpness-1439.webp = tests\Images\Input\WebP\vp80-05-sharpness-1439.webp + tests\Images\Input\WebP\vp80-05-sharpness-1440.webp = tests\Images\Input\WebP\vp80-05-sharpness-1440.webp + tests\Images\Input\WebP\vp80-05-sharpness-1443.webp = tests\Images\Input\WebP\vp80-05-sharpness-1443.webp + EndProjectSection +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{2aa31a1f-142c-43f4-8687-09abca4b3a26}*SharedItemsImports = 5 @@ -420,6 +579,7 @@ Global {C0D7754B-5277-438E-ABEB-2BA34401B5A7} = {1799C43E-5C54-4A8F-8D64-B1475241DB0D} {68A8CC40-6AED-4E96-B524-31B1158FDEEA} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} {FC527290-2F22-432C-B77B-6E815726B02C} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC} + {983A31E2-5E26-4058-BD6E-03B4922D4BBF} = {9DA226A1-8656-49A8-A58A-A8B5C081AD66} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5F8B9D1F-CD8B-4CC5-8216-D531E25BD795} diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs index 47c7c54ea0..15a3ed40fd 100644 --- a/src/ImageSharp/Configuration.cs +++ b/src/ImageSharp/Configuration.cs @@ -9,6 +9,7 @@ using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Tga; +using SixLabors.ImageSharp.Formats.WebP; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Processing; @@ -162,7 +163,8 @@ internal static Configuration CreateDefaultInstance() new JpegConfigurationModule(), new GifConfigurationModule(), new BmpConfigurationModule(), - new TgaConfigurationModule()); + new TgaConfigurationModule(), + new WebPConfigurationModule()); } } } diff --git a/src/ImageSharp/Formats/WebP/AlphaDecoder.cs b/src/ImageSharp/Formats/WebP/AlphaDecoder.cs new file mode 100644 index 0000000000..5d0ba360a7 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/AlphaDecoder.cs @@ -0,0 +1,309 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Collections.Generic; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Implements decoding for lossy alpha chunks which may be compressed. + /// + internal class AlphaDecoder : IDisposable + { + /// + /// Initializes a new instance of the class. + /// + /// The width of the image. + /// The height of the image. + /// The (maybe compressed) alpha data. + /// The first byte of the alpha image stream contains information on ow to decode the stream. + /// Used for allocating memory during decoding. + /// The configuration. + public AlphaDecoder(int width, int height, byte[] data, byte alphaChunkHeader, MemoryAllocator memoryAllocator, Configuration configuration) + { + this.Width = width; + this.Height = height; + this.Data = data; + this.LastRow = 0; + + // Compression method: Either 0 (no compression) or 1 (Compressed using the WebP lossless format) + int method = alphaChunkHeader & 0x03; + if (method < 0 || method > 1) + { + WebPThrowHelper.ThrowImageFormatException($"unexpected alpha compression method {method} found"); + } + + this.Compressed = !(method is 0); + + // The filtering method used. Only values between 0 and 3 are valid. + int filter = (alphaChunkHeader >> 2) & 0x03; + if (filter < 0 || filter > 3) + { + WebPThrowHelper.ThrowImageFormatException($"unexpected alpha filter method {filter} found"); + } + + this.AlphaFilterType = (WebPAlphaFilterType)filter; + + // These INFORMATIVE bits are used to signal the pre-processing that has been performed during compression. + // The decoder can use this information to e.g. dither the values or smooth the gradients prior to display. + // 0: no pre-processing, 1: level reduction + this.PreProcessing = (alphaChunkHeader >> 4) & 0x03; + + this.Vp8LDec = new Vp8LDecoder(width, height, memoryAllocator); + + this.Alpha = memoryAllocator.Allocate(width * height); + + if (this.Compressed) + { + var bitReader = new Vp8LBitReader(data); + this.LosslessDecoder = new WebPLosslessDecoder(bitReader, memoryAllocator, configuration); + this.LosslessDecoder.DecodeImageStream(this.Vp8LDec, width, height, true); + } + } + + /// + /// Gets the the width of the image. + /// + public int Width { get; } + + /// + /// Gets the height of the image. + /// + public int Height { get; } + + /// + /// Gets the used filter type. + /// + public WebPAlphaFilterType AlphaFilterType { get; } + + /// + /// Gets or sets the last decoded row. + /// + public int LastRow { get; set; } + + /// + /// Gets or sets the row before the last decoded row. + /// + public int PrevRow { get; set; } + + /// + /// Gets information for decoding Vp8L compressed alpha data. + /// + public Vp8LDecoder Vp8LDec { get; } + + /// + /// Gets the decoded alpha data. + /// + public IMemoryOwner Alpha { get; } + + public int CropTop { get; } + + /// + /// Gets a value indicating whether pre-processing was used during compression. + /// 0: no pre-processing, 1: level reduction. + /// + private int PreProcessing { get; } + + /// + /// Gets a value indicating whether the alpha channel uses compression. + /// + private bool Compressed { get; } + + /// + /// Gets the (maybe compressed) alpha data. + /// + private byte[] Data { get; } + + /// + /// Gets the Vp8L decoder which is used to de compress the alpha channel, if needed. + /// + private WebPLosslessDecoder LosslessDecoder { get; } + + /// + /// Gets or sets a value indicating whether the decoding needs 1 byte per pixel for decoding. + /// Although Alpha Channel requires only 1 byte per pixel, sometimes Vp8LDecoder may need to allocate + /// 4 bytes per pixel internally during decode. + /// + public bool Use8BDecode { get; set; } + + /// + /// Decodes and filters the maybe compressed alpha data. + /// + public void Decode() + { + if (this.Compressed is false) + { + if (this.Data.Length < (this.Width * this.Height)) + { + WebPThrowHelper.ThrowImageFormatException("not enough data in the ALPH chunk"); + } + + Span alphaSpan = this.Alpha.Memory.Span; + if (this.AlphaFilterType == WebPAlphaFilterType.None) + { + this.Data.AsSpan(0, this.Width * this.Height).CopyTo(alphaSpan); + return; + } + + Span deltas = this.Data.AsSpan(); + Span dst = alphaSpan; + Span prev = null; + for (int y = 0; y < this.Height; ++y) + { + switch (this.AlphaFilterType) + { + case WebPAlphaFilterType.Horizontal: + HorizontalUnfilter(prev, deltas, dst, this.Width); + break; + case WebPAlphaFilterType.Vertical: + VerticalUnfilter(prev, deltas, dst, this.Width); + break; + case WebPAlphaFilterType.Gradient: + GradientUnfilter(prev, deltas, dst, this.Width); + break; + } + + prev = dst; + deltas = deltas.Slice(this.Width); + dst = dst.Slice(this.Width); + } + } + else + { + this.LosslessDecoder.DecodeAlphaData(this); + } + } + + /// + /// Applies filtering to a set of rows. + /// + /// The first row index to start filtering. + /// The last row index for filtering. + /// The destination to store the filtered data. + /// The stride to use. + public void AlphaApplyFilter(int firstRow, int lastRow, Span dst, int stride) + { + if (this.AlphaFilterType is WebPAlphaFilterType.None) + { + return; + } + + Span alphaSpan = this.Alpha.Memory.Span; + Span prev = this.PrevRow == 0 ? null : alphaSpan.Slice(this.Width * this.PrevRow); + for (int y = firstRow; y < lastRow; ++y) + { + switch (this.AlphaFilterType) + { + case WebPAlphaFilterType.Horizontal: + HorizontalUnfilter(prev, dst, dst, this.Width); + break; + case WebPAlphaFilterType.Vertical: + VerticalUnfilter(prev, dst, dst, this.Width); + break; + case WebPAlphaFilterType.Gradient: + GradientUnfilter(prev, dst, dst, this.Width); + break; + } + + prev = dst; + dst = dst.Slice(stride); + } + + this.PrevRow = lastRow - 1; + } + + private static void HorizontalUnfilter(Span prev, Span input, Span dst, int width) + { + byte pred = (byte)(prev == null ? 0 : prev[0]); + + for (int i = 0; i < width; ++i) + { + dst[i] = (byte)(pred + input[i]); + pred = dst[i]; + } + } + + private static void VerticalUnfilter(Span prev, Span input, Span dst, int width) + { + if (prev == null) + { + HorizontalUnfilter(null, input, dst, width); + } + else + { + for (int i = 0; i < width; ++i) + { + dst[i] = (byte)(prev[i] + input[i]); + } + } + } + + private static void GradientUnfilter(Span prev, Span input, Span dst, int width) + { + if (prev == null) + { + HorizontalUnfilter(null, input, dst, width); + } + else + { + byte top = prev[0]; + byte topLeft = top; + byte left = top; + for (int i = 0; i < width; ++i) + { + top = prev[i]; + left = (byte)(input[i] + GradientPredictor(left, top, topLeft)); + topLeft = top; + dst[i] = left; + } + } + } + + private static bool Is8bOptimizable(Vp8LMetadata hdr) + { + if (hdr.ColorCacheSize > 0) + { + return false; + } + + // When the Huffman tree contains only one symbol, we can skip the + // call to ReadSymbol() for red/blue/alpha channels. + for (int i = 0; i < hdr.NumHTreeGroups; ++i) + { + List htrees = hdr.HTreeGroups[i].HTrees; + if (htrees[HuffIndex.Red][0].Value > 0) + { + return false; + } + + if (htrees[HuffIndex.Blue][0].Value > 0) + { + return false; + } + + if (htrees[HuffIndex.Alpha][0].Value > 0) + { + return false; + } + } + + return true; + } + + private static int GradientPredictor(byte a, byte b, byte c) + { + int g = a + b - c; + return ((g & ~0xff) is 0) ? g : (g < 0) ? 0 : 255; // clip to 8bit + } + + /// + public void Dispose() + { + this.Vp8LDec?.Dispose(); + this.Alpha?.Dispose(); + } + } +} diff --git a/src/ImageSharp/Formats/WebP/BitReaderBase.cs b/src/ImageSharp/Formats/WebP/BitReaderBase.cs new file mode 100644 index 0000000000..3a5bf4f4ae --- /dev/null +++ b/src/ImageSharp/Formats/WebP/BitReaderBase.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Base class for VP8 and VP8L bitreader. + /// + internal abstract class BitReaderBase + { + /// + /// Gets or sets the raw encoded image data. + /// + public byte[] Data { get; set; } + + /// + /// Copies the raw encoded image data from the stream into a byte array. + /// + /// The input stream. + /// Number of bytes to read as indicated from the chunk size. + /// Used for allocating memory during reading data from the stream. + protected void ReadImageDataFromStream(Stream input, int bytesToRead, MemoryAllocator memoryAllocator) + { + using (var ms = new MemoryStream()) + using (IManagedByteBuffer buffer = memoryAllocator.AllocateManagedByteBuffer(4096)) + { + Span bufferSpan = buffer.GetSpan(); + int read; + while (bytesToRead > 0 && (read = input.Read(buffer.Array, 0, Math.Min(bufferSpan.Length, bytesToRead))) > 0) + { + ms.Write(buffer.Array, 0, read); + bytesToRead -= read; + } + + if (bytesToRead > 0) + { + WebPThrowHelper.ThrowImageFormatException("image file has insufficient data"); + } + + this.Data = ms.ToArray(); + } + } + } +} diff --git a/src/ImageSharp/Formats/WebP/ColorCache.cs b/src/ImageSharp/Formats/WebP/ColorCache.cs new file mode 100644 index 0000000000..d5834a4c80 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/ColorCache.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// A small hash-addressed array to store recently used colors, to be able to recall them with shorter codes. + /// + internal class ColorCache + { + private const uint HashMul = 0x1e35a7bdu; + + /// + /// Gets the color entries. + /// + public uint[] Colors { get; private set; } + + /// + /// Gets the hash shift: 32 - hashBits. + /// + public int HashShift { get; private set; } + + /// + /// Gets the hash bits. + /// + public int HashBits { get; private set; } + + /// + /// Initializes a new color cache. + /// + /// The hashBits determine the size of cache. It will be 1 left shifted by hashBits. + public void Init(int hashBits) + { + int hashSize = 1 << hashBits; + this.Colors = new uint[hashSize]; + this.HashBits = hashBits; + this.HashShift = 32 - hashBits; + } + + /// + /// Inserts a new color into the cache. + /// + /// The color to insert. + public void Insert(uint argb) + { + int key = this.HashPix(argb, this.HashShift); + this.Colors[key] = argb; + } + + public uint Lookup(int key) + { + return this.Colors[key]; + } + + private int HashPix(uint argb, int shift) + { + return (int)((argb * HashMul) >> shift); + } + } +} diff --git a/src/ImageSharp/Formats/WebP/HTreeGroup.cs b/src/ImageSharp/Formats/WebP/HTreeGroup.cs new file mode 100644 index 0000000000..c311601bb5 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/HTreeGroup.cs @@ -0,0 +1,59 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Huffman table group. + /// Includes special handling for the following cases: + /// - IsTrivialLiteral: one common literal base for RED/BLUE/ALPHA (not GREEN) + /// - IsTrivialCode: only 1 code (no bit is read from the bitstream) + /// - UsePackedTable: few enough literal symbols, so all the bit codes can fit into a small look-up table PackedTable[] + /// The common literal base, if applicable, is stored in 'LiteralArb'. + /// + internal class HTreeGroup + { + public HTreeGroup(uint packedTableSize) + { + this.HTrees = new List(WebPConstants.HuffmanCodesPerMetaCode); + this.PackedTable = new HuffmanCode[packedTableSize]; + for (int i = 0; i < packedTableSize; i++) + { + this.PackedTable[i] = new HuffmanCode(); + } + } + + /// + /// Gets the Huffman trees. This has a maximum of HuffmanCodesPerMetaCode (5) entry's. + /// + public List HTrees { get; } + + /// + /// Gets or sets a value indicating whether huffman trees for Red, Blue and Alpha Symbols are trivial (have a single code). + /// + public bool IsTrivialLiteral { get; set; } + + /// + /// Gets or sets a the literal argb value of the pixel. + /// If IsTrivialLiteral is true, this is the ARGB value of the pixel, with Green channel being set to zero. + /// + public uint LiteralArb { get; set; } + + /// + /// Gets or sets a value indicating whether there is only one code. + /// + public bool IsTrivialCode { get; set; } + + /// + /// Gets or sets a value indicating whether to use packed table below for short literal code. + /// + public bool UsePackedTable { get; set; } + + /// + /// Gets or sets table mapping input bits to packed values, or escape case to literal code. + /// + public HuffmanCode[] PackedTable { get; set; } + } +} diff --git a/src/ImageSharp/Formats/WebP/HuffIndex.cs b/src/ImageSharp/Formats/WebP/HuffIndex.cs new file mode 100644 index 0000000000..7e2b58a8e5 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/HuffIndex.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Five Huffman codes are used at each meta code. + /// + public static class HuffIndex + { + /// + /// Green + length prefix codes + color cache codes. + /// + public const int Green = 0; + + /// + /// Red. + /// + public const int Red = 1; + + /// + /// Blue. + /// + public const int Blue = 2; + + /// + /// Alpha. + /// + public const int Alpha = 3; + + /// + /// Distance prefix codes. + /// + public const int Dist = 4; + } +} diff --git a/src/ImageSharp/Formats/WebP/HuffmanCode.cs b/src/ImageSharp/Formats/WebP/HuffmanCode.cs new file mode 100644 index 0000000000..ac6c5bec46 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/HuffmanCode.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// A classic way to do entropy coding where a smaller number of bits are used for more frequent codes. + /// + [DebuggerDisplay("BitsUsed: {BitsUsed}, Value: {Value}")] + internal class HuffmanCode + { + /// + /// Gets or sets the number of bits used for this symbol. + /// + public int BitsUsed { get; set; } + + /// + /// Gets or sets the symbol value or table offset. + /// + public uint Value { get; set; } + } +} diff --git a/src/ImageSharp/Formats/WebP/HuffmanUtils.cs b/src/ImageSharp/Formats/WebP/HuffmanUtils.cs new file mode 100644 index 0000000000..3cab68dd12 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/HuffmanUtils.cs @@ -0,0 +1,216 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Utility functions related to creating the huffman tables. + /// + internal static class HuffmanUtils + { + public const int HuffmanTableBits = 8; + + public const int HuffmanPackedBits = 6; + + public const int HuffmanTableMask = (1 << HuffmanTableBits) - 1; + + public const uint HuffmanPackedTableSize = 1u << HuffmanPackedBits; + + public static int BuildHuffmanTable(Span table, int rootBits, int[] codeLengths, int codeLengthsSize) + { + Guard.MustBeGreaterThan(rootBits, 0, nameof(rootBits)); + Guard.NotNull(codeLengths, nameof(codeLengths)); + Guard.MustBeGreaterThan(codeLengthsSize, 0, nameof(codeLengthsSize)); + + // sorted[codeLengthsSize] is a pre-allocated array for sorting symbols by code length. + var sorted = new int[codeLengthsSize]; + int totalSize = 1 << rootBits; // total size root table + 2nd level table. + int len; // current code length. + int symbol; // symbol index in original or sorted table. + var count = new int[WebPConstants.MaxAllowedCodeLength + 1]; // number of codes of each length. + var offset = new int[WebPConstants.MaxAllowedCodeLength + 1]; // offsets in sorted table for each length. + + // Build histogram of code lengths. + for (symbol = 0; symbol < codeLengthsSize; ++symbol) + { + if (codeLengths[symbol] > WebPConstants.MaxAllowedCodeLength) + { + return 0; + } + + count[codeLengths[symbol]]++; + } + + // Error, all code lengths are zeros. + if (count[0] == codeLengthsSize) + { + return 0; + } + + // Generate offsets into sorted symbol table by code length. + offset[1] = 0; + for (len = 1; len < WebPConstants.MaxAllowedCodeLength; ++len) + { + if (count[len] > (1 << len)) + { + return 0; + } + + offset[len + 1] = offset[len] + count[len]; + } + + // Sort symbols by length, by symbol order within each length. + for (symbol = 0; symbol < codeLengthsSize; ++symbol) + { + int symbolCodeLength = codeLengths[symbol]; + if (codeLengths[symbol] > 0) + { + sorted[offset[symbolCodeLength]++] = symbol; + } + } + + // Special case code with only one value. + if (offset[WebPConstants.MaxAllowedCodeLength] is 1) + { + var huffmanCode = new HuffmanCode() + { + BitsUsed = 0, + Value = (uint)sorted[0] + }; + ReplicateValue(table, 1, totalSize, huffmanCode); + return totalSize; + } + + int step; // step size to replicate values in current table + int low = -1; // low bits for current root entry + int mask = totalSize - 1; // mask for low bits + int key = 0; // reversed prefix code + int numNodes = 1; // number of Huffman tree nodes + int numOpen = 1; // number of open branches in current tree level + int tableBits = rootBits; // key length of current table + int tableSize = 1 << tableBits; // size of current table + symbol = 0; + + // Fill in root table. + for (len = 1, step = 2; len <= rootBits; ++len, step <<= 1) + { + numOpen <<= 1; + numNodes += numOpen; + numOpen -= count[len]; + if (numOpen < 0) + { + return 0; + } + + for (; count[len] > 0; --count[len]) + { + var huffmanCode = new HuffmanCode() + { + BitsUsed = len, + Value = (uint)sorted[symbol++] + }; + ReplicateValue(table.Slice(key), step, tableSize, huffmanCode); + key = GetNextKey(key, len); + } + } + + // Fill in 2nd level tables and add pointers to root table. + Span tableSpan = table; + int tablePos = 0; + for (len = rootBits + 1, step = 2; len <= WebPConstants.MaxAllowedCodeLength; ++len, step <<= 1) + { + numOpen <<= 1; + numNodes += numOpen; + numOpen -= count[len]; + if (numOpen < 0) + { + return 0; + } + + for (; count[len] > 0; --count[len]) + { + if ((key & mask) != low) + { + tableSpan = tableSpan.Slice(tableSize); + tablePos += tableSize; + tableBits = NextTableBitSize(count, len, rootBits); + tableSize = 1 << tableBits; + totalSize += tableSize; + low = key & mask; + uint v = (uint)(tablePos - low); + table[low] = new HuffmanCode + { + BitsUsed = tableBits + rootBits, + Value = (uint)(tablePos - low) + }; + } + + var huffmanCode = new HuffmanCode + { + BitsUsed = len - rootBits, + Value = (uint)sorted[symbol++] + }; + ReplicateValue(tableSpan.Slice(key >> rootBits), step, tableSize, huffmanCode); + key = GetNextKey(key, len); + } + } + + return totalSize; + } + + /// + /// Returns the table width of the next 2nd level table. count is the histogram of bit lengths for the remaining symbols, + /// len is the code length of the next processed symbol. + /// + private static int NextTableBitSize(int[] count, int len, int rootBits) + { + int left = 1 << (len - rootBits); + while (len < WebPConstants.MaxAllowedCodeLength) + { + left -= count[len]; + if (left <= 0) + { + break; + } + + ++len; + left <<= 1; + } + + return len - rootBits; + } + + /// + /// Stores code in table[0], table[step], table[2*step], ..., table[end]. + /// Assumes that end is an integer multiple of step. + /// + private static void ReplicateValue(Span table, int step, int end, HuffmanCode code) + { + Guard.IsTrue(end % step == 0, nameof(end), "end must be a multiple of step"); + + do + { + end -= step; + table[end] = code; + } + while (end > 0); + } + + /// + /// Returns reverse(reverse(key, len) + 1, len), where reverse(key, len) is the + /// bit-wise reversal of the len least significant bits of key. + /// + private static int GetNextKey(int key, int len) + { + int step = 1 << (len - 1); + while ((key & step) != 0) + { + step >>= 1; + } + + return step != 0 ? (key & (step - 1)) + step : key; + } + } +} diff --git a/src/ImageSharp/Formats/WebP/IWebPDecoderOptions.cs b/src/ImageSharp/Formats/WebP/IWebPDecoderOptions.cs new file mode 100644 index 0000000000..1ddbdbdc6d --- /dev/null +++ b/src/ImageSharp/Formats/WebP/IWebPDecoderOptions.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Image decoder for generating an image out of a webp stream. + /// + internal interface IWebPDecoderOptions + { + /// + /// Gets a value indicating whether the metadata should be ignored when the image is being decoded. + /// + bool IgnoreMetadata { get; } + } +} diff --git a/src/ImageSharp/Formats/WebP/LoopFilter.cs b/src/ImageSharp/Formats/WebP/LoopFilter.cs new file mode 100644 index 0000000000..8330ca593f --- /dev/null +++ b/src/ImageSharp/Formats/WebP/LoopFilter.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Enum for the different loop filters used. VP8 supports two types of loop filters. + /// + internal enum LoopFilter + { + /// + /// No filter is used. + /// + None = 0, + + /// + /// Simple loop filter. + /// + Simple = 1, + + /// + /// Complex loop filter. + /// + Complex = 2, + } +} diff --git a/src/ImageSharp/Formats/WebP/LosslessUtils.cs b/src/ImageSharp/Formats/WebP/LosslessUtils.cs new file mode 100644 index 0000000000..600338e8ea --- /dev/null +++ b/src/ImageSharp/Formats/WebP/LosslessUtils.cs @@ -0,0 +1,656 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.InteropServices; + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Utility functions for the lossless decoder. + /// + internal static class LosslessUtils + { + /// + /// Add green to blue and red channels (i.e. perform the inverse transform of 'subtract green'). + /// + /// The pixel data to apply the transformation. + public static void AddGreenToBlueAndRed(Span pixelData) + { + for (int i = 0; i < pixelData.Length; i++) + { + uint argb = pixelData[i]; + uint green = (argb >> 8) & 0xff; + uint redBlue = argb & 0x00ff00ffu; + redBlue += (green << 16) | green; + redBlue &= 0x00ff00ffu; + pixelData[i] = (argb & 0xff00ff00u) | redBlue; + } + } + + /// + /// If there are not many unique pixel values, it may be more efficient to create a color index array and replace the pixel values by the array's indices. + /// This will reverse the color index transform. + /// + /// The transform data contains color table size and the entries in the color table. + /// The pixel data to apply the reverse transform on. + public static void ColorIndexInverseTransform(Vp8LTransform transform, Span pixelData) + { + int bitsPerPixel = 8 >> transform.Bits; + int width = transform.XSize; + int height = transform.YSize; + Span colorMap = transform.Data.GetSpan(); + int decodedPixels = 0; + if (bitsPerPixel < 8) + { + int pixelsPerByte = 1 << transform.Bits; + int countMask = pixelsPerByte - 1; + int bitMask = (1 << bitsPerPixel) - 1; + + var decodedPixelData = new uint[width * height]; + int pixelDataPos = 0; + for (int y = 0; y < height; ++y) + { + uint packedPixels = 0; + for (int x = 0; x < width; ++x) + { + // We need to load fresh 'packed_pixels' once every + // 'pixelsPerByte' increments of x. Fortunately, pixelsPerByte + // is a power of 2, so can just use a mask for that, instead of + // decrementing a counter. + if ((x & countMask) is 0) + { + packedPixels = GetArgbIndex(pixelData[pixelDataPos++]); + } + + decodedPixelData[decodedPixels++] = colorMap[(int)(packedPixels & bitMask)]; + packedPixels >>= bitsPerPixel; + } + } + + decodedPixelData.AsSpan().CopyTo(pixelData); + + return; + } + + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + uint colorMapIndex = GetArgbIndex(pixelData[decodedPixels]); + pixelData[decodedPixels] = colorMap[(int)colorMapIndex]; + decodedPixels++; + } + } + } + + /// + /// The goal of the color transform is to de-correlate the R, G and B values of each pixel. + /// Color transform keeps the green (G) value as it is, transforms red (R) based on green and transforms blue (B) based on green and then based on red. + /// + /// The transform data. + /// The pixel data to apply the inverse transform on. + public static void ColorSpaceInverseTransform(Vp8LTransform transform, Span pixelData) + { + int width = transform.XSize; + int yEnd = transform.YSize; + int tileWidth = 1 << transform.Bits; + int mask = tileWidth - 1; + int safeWidth = width & ~mask; + int remainingWidth = width - safeWidth; + int tilesPerRow = SubSampleSize(width, transform.Bits); + int y = 0; + int predRowIdxStart = (y >> transform.Bits) * tilesPerRow; + Span transformData = transform.Data.GetSpan(); + + int pixelPos = 0; + while (y < yEnd) + { + int predRowIdx = predRowIdxStart; + Vp8LMultipliers m = default(Vp8LMultipliers); + int srcSafeEnd = pixelPos + safeWidth; + int srcEnd = pixelPos + width; + while (pixelPos < srcSafeEnd) + { + uint colorCode = transformData[predRowIdx++]; + ColorCodeToMultipliers(colorCode, ref m); + TransformColorInverse(m, pixelData, pixelPos, tileWidth); + pixelPos += tileWidth; + } + + if (pixelPos < srcEnd) + { + uint colorCode = transformData[predRowIdx]; + ColorCodeToMultipliers(colorCode, ref m); + TransformColorInverse(m, pixelData, pixelPos, remainingWidth); + pixelPos += remainingWidth; + } + + ++y; + if ((y & mask) is 0) + { + predRowIdxStart += tilesPerRow; + } + } + } + + /// + /// Reverses the color space transform. + /// + /// The color transform element. + /// The pixel data to apply the inverse transform on. + /// The start index of reverse transform. + /// The number of pixels to apply the transform. + public static void TransformColorInverse(Vp8LMultipliers m, Span pixelData, int start, int numPixels) + { + int end = start + numPixels; + for (int i = start; i < end; i++) + { + uint argb = pixelData[i]; + sbyte green = (sbyte)(argb >> 8); + uint red = argb >> 16; + int newRed = (int)(red & 0xff); + int newBlue = (int)argb & 0xff; + newRed += ColorTransformDelta((sbyte)m.GreenToRed, (sbyte)green); + newRed &= 0xff; + newBlue += ColorTransformDelta((sbyte)m.GreenToBlue, (sbyte)green); + newBlue += ColorTransformDelta((sbyte)m.RedToBlue, (sbyte)newRed); + newBlue &= 0xff; + + pixelData[i] = (argb & 0xff00ff00u) | ((uint)newRed << 16) | (uint)newBlue; + } + } + + /// + /// This will reverse the predictor transform. + /// The predictor transform can be used to reduce entropy by exploiting the fact that neighboring pixels are often correlated. + /// In the predictor transform, the current pixel value is predicted from the pixels already decoded (in scan-line order) and only the residual value (actual - predicted) is encoded. + /// The prediction mode determines the type of prediction to use. The image is divided into squares and all the pixels in a square use same prediction mode. + /// + /// The transform data. + /// The pixel data to apply the inverse transform. + /// The resulting pixel data with the reversed transformation data. + public static void PredictorInverseTransform(Vp8LTransform transform, Span pixelData, Span output) + { + int processedPixels = 0; + int yStart = 0; + int width = transform.XSize; + Span transformData = transform.Data.GetSpan(); + + // First Row follows the L (mode=1) mode. + PredictorAdd0(pixelData, processedPixels, 1, output); + PredictorAdd1(pixelData, 1, width - 1, output); + processedPixels += width; + yStart++; + + int y = yStart; + int yEnd = transform.YSize; + int tileWidth = 1 << transform.Bits; + int mask = tileWidth - 1; + int tilesPerRow = SubSampleSize(width, transform.Bits); + int predictorModeIdxBase = (y >> transform.Bits) * tilesPerRow; + while (y < yEnd) + { + int predictorModeIdx = predictorModeIdxBase; + int x = 1; + + // First pixel follows the T (mode=2) mode. + PredictorAdd2(pixelData, processedPixels, 1, width, output); + + while (x < width) + { + uint predictorMode = (transformData[predictorModeIdx++] >> 8) & 0xf; + int xEnd = (x & ~mask) + tileWidth; + if (xEnd > width) + { + xEnd = width; + } + + // There are 14 different prediction modes. + // In each prediction mode, the current pixel value is predicted from one or more neighboring pixels whose values are already known. + int startIdx = processedPixels + x; + int numberOfPixels = xEnd - x; + switch (predictorMode) + { + case 0: + PredictorAdd0(pixelData, startIdx, numberOfPixels, output); + break; + case 1: + PredictorAdd1(pixelData, startIdx, numberOfPixels, output); + break; + case 2: + PredictorAdd2(pixelData, startIdx, numberOfPixels, width, output); + break; + case 3: + PredictorAdd3(pixelData, startIdx, numberOfPixels, width, output); + break; + case 4: + PredictorAdd4(pixelData, startIdx, numberOfPixels, width, output); + break; + case 5: + PredictorAdd5(pixelData, startIdx, numberOfPixels, width, output); + break; + case 6: + PredictorAdd6(pixelData, startIdx, numberOfPixels, width, output); + break; + case 7: + PredictorAdd7(pixelData, startIdx, numberOfPixels, width, output); + break; + case 8: + PredictorAdd8(pixelData, startIdx, numberOfPixels, width, output); + break; + case 9: + PredictorAdd9(pixelData, startIdx, numberOfPixels, width, output); + break; + case 10: + PredictorAdd10(pixelData, startIdx, numberOfPixels, width, output); + break; + case 11: + PredictorAdd11(pixelData, startIdx, numberOfPixels, width, output); + break; + case 12: + PredictorAdd12(pixelData, startIdx, numberOfPixels, width, output); + break; + case 13: + PredictorAdd13(pixelData, startIdx, numberOfPixels, width, output); + break; + } + + x = xEnd; + } + + processedPixels += width; + ++y; + if ((y & mask) is 0) + { + // Use the same mask, since tiles are squares. + predictorModeIdxBase += tilesPerRow; + } + } + + output.CopyTo(pixelData); + } + + private static void PredictorAdd0(Span input, int startIdx, int numberOfPixels, Span output) + { + int endIdx = startIdx + numberOfPixels; + for (int x = startIdx; x < endIdx; ++x) + { + uint pred = Predictor0(); + output[x] = AddPixels(input[x], pred); + } + } + + private static void PredictorAdd1(Span input, int startIdx, int numberOfPixels, Span output) + { + int endIdx = startIdx + numberOfPixels; + uint left = output[startIdx - 1]; + for (int x = startIdx; x < endIdx; ++x) + { + output[x] = left = AddPixels(input[x], left); + } + } + + private static void PredictorAdd2(Span input, int startIdx, int numberOfPixels, int width, Span output) + { + int endIdx = startIdx + numberOfPixels; + int offset = 0; + for (int x = startIdx; x < endIdx; ++x) + { + uint pred = Predictor2(output[x - 1], output, startIdx - width + offset++); + output[x] = AddPixels(input[x], pred); + } + } + + private static void PredictorAdd3(Span input, int startIdx, int numberOfPixels, int width, Span output) + { + int endIdx = startIdx + numberOfPixels; + int offset = 0; + for (int x = startIdx; x < endIdx; ++x) + { + uint pred = Predictor3(output[x - 1], output, startIdx - width + offset++); + output[x] = AddPixels(input[x], pred); + } + } + + private static void PredictorAdd4(Span input, int startIdx, int numberOfPixels, int width, Span output) + { + int endIdx = startIdx + numberOfPixels; + int offset = 0; + for (int x = startIdx; x < endIdx; ++x) + { + uint pred = Predictor4(output[x - 1], output, startIdx - width + offset++); + output[x] = AddPixels(input[x], pred); + } + } + + private static void PredictorAdd5(Span input, int startIdx, int numberOfPixels, int width, Span output) + { + int endIdx = startIdx + numberOfPixels; + int offset = 0; + for (int x = startIdx; x < endIdx; ++x) + { + uint pred = Predictor5(output[x - 1], output, startIdx - width + offset++); + output[x] = AddPixels(input[x], pred); + } + } + + private static void PredictorAdd6(Span input, int startIdx, int numberOfPixels, int width, Span output) + { + int endIdx = startIdx + numberOfPixels; + int offset = 0; + for (int x = startIdx; x < endIdx; ++x) + { + uint pred = Predictor6(output[x - 1], output, startIdx - width + offset++); + output[x] = AddPixels(input[x], pred); + } + } + + private static void PredictorAdd7(Span input, int startIdx, int numberOfPixels, int width, Span output) + { + int endIdx = startIdx + numberOfPixels; + int offset = 0; + for (int x = startIdx; x < endIdx; ++x) + { + uint pred = Predictor7(output[x - 1], output, startIdx - width + offset++); + output[x] = AddPixels(input[x], pred); + } + } + + private static void PredictorAdd8(Span input, int startIdx, int numberOfPixels, int width, Span output) + { + int endIdx = startIdx + numberOfPixels; + int offset = 0; + for (int x = startIdx; x < endIdx; ++x) + { + uint pred = Predictor8(output[x - 1], output, startIdx - width + offset++); + output[x] = AddPixels(input[x], pred); + } + } + + private static void PredictorAdd9(Span input, int startIdx, int numberOfPixels, int width, Span output) + { + int endIdx = startIdx + numberOfPixels; + int offset = 0; + for (int x = startIdx; x < endIdx; ++x) + { + uint pred = Predictor9(output[x - 1], output, startIdx - width + offset++); + output[x] = AddPixels(input[x], pred); + } + } + + private static void PredictorAdd10(Span input, int startIdx, int numberOfPixels, int width, Span output) + { + int endIdx = startIdx + numberOfPixels; + int offset = 0; + for (int x = startIdx; x < endIdx; ++x) + { + uint pred = Predictor10(output[x - 1], output, startIdx - width + offset++); + output[x] = AddPixels(input[x], pred); + } + } + + private static void PredictorAdd11(Span input, int startIdx, int numberOfPixels, int width, Span output) + { + int endIdx = startIdx + numberOfPixels; + int offset = 0; + for (int x = startIdx; x < endIdx; ++x) + { + uint pred = Predictor11(output[x - 1], output, startIdx - width + offset++); + output[x] = AddPixels(input[x], pred); + } + } + + private static void PredictorAdd12(Span input, int startIdx, int numberOfPixels, int width, Span output) + { + int endIdx = startIdx + numberOfPixels; + int offset = 0; + for (int x = startIdx; x < endIdx; ++x) + { + uint pred = Predictor12(output[x - 1], output, startIdx - width + offset++); + output[x] = AddPixels(input[x], pred); + } + } + + private static void PredictorAdd13(Span input, int startIdx, int numberOfPixels, int width, Span output) + { + int endIdx = startIdx + numberOfPixels; + int offset = 0; + for (int x = startIdx; x < endIdx; ++x) + { + uint pred = Predictor13(output[x - 1], output, startIdx - width + offset++); + output[x] = AddPixels(input[x], pred); + } + } + + private static uint Predictor0() + { + return WebPConstants.ArgbBlack; + } + + private static uint Predictor1(uint left, Span top) + { + return left; + } + + private static uint Predictor2(uint left, Span top, int idx) + { + return top[idx]; + } + + private static uint Predictor3(uint left, Span top, int idx) + { + return top[idx + 1]; + } + + private static uint Predictor4(uint left, Span top, int idx) + { + return top[idx - 1]; + } + + private static uint Predictor5(uint left, Span top, int idx) + { + uint pred = Average3(left, top[idx], top[idx + 1]); + return pred; + } + + private static uint Predictor6(uint left, Span top, int idx) + { + uint pred = Average2(left, top[idx - 1]); + return pred; + } + + private static uint Predictor7(uint left, Span top, int idx) + { + uint pred = Average2(left, top[idx]); + return pred; + } + + private static uint Predictor8(uint left, Span top, int idx) + { + uint pred = Average2(top[idx - 1], top[idx]); + return pred; + } + + private static uint Predictor9(uint left, Span top, int idx) + { + uint pred = Average2(top[idx], top[idx + 1]); + return pred; + } + + private static uint Predictor10(uint left, Span top, int idx) + { + uint pred = Average4(left, top[idx - 1], top[idx], top[idx + 1]); + return pred; + } + + private static uint Predictor11(uint left, Span top, int idx) + { + uint pred = Select(top[idx], left, top[idx - 1]); + return pred; + } + + private static uint Predictor12(uint left, Span top, int idx) + { + uint pred = ClampedAddSubtractFull(left, top[idx], top[idx - 1]); + return pred; + } + + private static uint Predictor13(uint left, Span top, int idx) + { + uint pred = ClampedAddSubtractHalf(left, top[idx], top[idx - 1]); + return pred; + } + + private static uint ClampedAddSubtractFull(uint c0, uint c1, uint c2) + { + int a = AddSubtractComponentFull((int)(c0 >> 24), (int)(c1 >> 24), (int)(c2 >> 24)); + int r = AddSubtractComponentFull( + (int)((c0 >> 16) & 0xff), + (int)((c1 >> 16) & 0xff), + (int)((c2 >> 16) & 0xff)); + int g = AddSubtractComponentFull( + (int)((c0 >> 8) & 0xff), + (int)((c1 >> 8) & 0xff), + (int)((c2 >> 8) & 0xff)); + int b = AddSubtractComponentFull((int)(c0 & 0xff), (int)(c1 & 0xff), (int)(c2 & 0xff)); + return ((uint)a << 24) | ((uint)r << 16) | ((uint)g << 8) | (uint)b; + } + + private static uint ClampedAddSubtractHalf(uint c0, uint c1, uint c2) + { + uint ave = Average2(c0, c1); + int a = AddSubtractComponentHalf((int)(ave >> 24), (int)(c2 >> 24)); + int r = AddSubtractComponentHalf((int)((ave >> 16) & 0xff), (int)((c2 >> 16) & 0xff)); + int g = AddSubtractComponentHalf((int)((ave >> 8) & 0xff), (int)((c2 >> 8) & 0xff)); + int b = AddSubtractComponentHalf((int)(ave & 0xff), (int)(c2 & 0xff)); + return ((uint)a << 24) | ((uint)r << 16) | ((uint)g << 8) | (uint)b; + } + + private static int AddSubtractComponentHalf(int a, int b) + { + return (int)Clip255((uint)(a + ((a - b) / 2))); + } + + private static int AddSubtractComponentFull(int a, int b, int c) + { + return (int)Clip255((uint)(a + b - c)); + } + + private static uint Clip255(uint a) + { + if (a < 256) + { + return a; + } + + return ~a >> 24; + } + + private static uint Select(uint a, uint b, uint c) + { + int paMinusPb = + Sub3((int)(a >> 24), (int)(b >> 24), (int)(c >> 24)) + + Sub3((int)((a >> 16) & 0xff), (int)((b >> 16) & 0xff), (int)((c >> 16) & 0xff)) + + Sub3((int)((a >> 8) & 0xff), (int)((b >> 8) & 0xff), (int)((c >> 8) & 0xff)) + + Sub3((int)(a & 0xff), (int)(b & 0xff), (int)(c & 0xff)); + return (paMinusPb <= 0) ? a : b; + } + + private static int Sub3(int a, int b, int c) + { + int pb = b - c; + int pa = a - c; + return Math.Abs(pb) - Math.Abs(pa); + } + + private static uint Average2(uint a0, uint a1) + { + return (((a0 ^ a1) & 0xfefefefeu) >> 1) + (a0 & a1); + } + + private static uint Average3(uint a0, uint a1, uint a2) + { + return Average2(Average2(a0, a2), a1); + } + + private static uint Average4(uint a0, uint a1, uint a2, uint a3) + { + return Average2(Average2(a0, a1), Average2(a2, a3)); + } + + /// + /// Computes sampled size of 'size' when sampling using 'sampling bits'. + /// + public static int SubSampleSize(int size, int samplingBits) + { + return (size + (1 << samplingBits) - 1) >> samplingBits; + } + + /// + /// Sum of each component, mod 256. + /// + private static uint AddPixels(uint a, uint b) + { + uint alphaAndGreen = (a & 0xff00ff00u) + (b & 0xff00ff00u); + uint redAndBlue = (a & 0x00ff00ffu) + (b & 0x00ff00ffu); + return (alphaAndGreen & 0xff00ff00u) | (redAndBlue & 0x00ff00ffu); + } + + /// + /// Difference of each component, mod 256. + /// + private static uint SubPixels(uint a, uint b) + { + uint alphaAndGreen = 0x00ff00ffu + (a & 0xff00ff00u) - (b & 0xff00ff00u); + uint redAndBlue = 0xff00ff00u + (a & 0x00ff00ffu) - (b & 0x00ff00ffu); + return (alphaAndGreen & 0xff00ff00u) | (redAndBlue & 0x00ff00ffu); + } + + private static uint GetArgbIndex(uint idx) + { + return (idx >> 8) & 0xff; + } + + public static void ExpandColorMap(int numColors, Span transformData, Span newColorMap) + { + newColorMap[0] = transformData[0]; + Span data = MemoryMarshal.Cast(transformData); + Span newData = MemoryMarshal.Cast(newColorMap); + int i; + for (i = 4; i < 4 * numColors; ++i) + { + // Equivalent to AddPixelEq(), on a byte-basis. + newData[i] = (byte)((data[i] + newData[i - 4]) & 0xff); + } + + for (; i < 4 * newColorMap.Length; ++i) + { + newData[i] = 0; // black tail. + } + } + + private static int ColorTransformDelta(sbyte colorPred, sbyte color) + { + return ((int)colorPred * color) >> 5; + } + + private static void ColorCodeToMultipliers(uint colorCode, ref Vp8LMultipliers m) + { + m.GreenToRed = (byte)(colorCode & 0xff); + m.GreenToBlue = (byte)((colorCode >> 8) & 0xff); + m.RedToBlue = (byte)((colorCode >> 16) & 0xff); + } + + internal struct Vp8LMultipliers + { + public byte GreenToRed; + + public byte GreenToBlue; + + public byte RedToBlue; + } + } +} diff --git a/src/ImageSharp/Formats/WebP/LossyUtils.cs b/src/ImageSharp/Formats/WebP/LossyUtils.cs new file mode 100644 index 0000000000..7222cfa7b3 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/LossyUtils.cs @@ -0,0 +1,912 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers.Binary; + +namespace SixLabors.ImageSharp.Formats.WebP +{ + internal static class LossyUtils + { + private static void Put16(int v, Span dst) + { + for (int j = 0; j < 16; ++j) + { + Memset(dst.Slice(j * WebPConstants.Bps), (byte)v, 0, 16); + } + } + + public static void DC16(Span dst, Span yuv, int offset) + { + int dc = 16; + for (int j = 0; j < 16; ++j) + { + // DC += dst[-1 + j * BPS] + dst[j - BPS]; + dc += yuv[offset - 1 + (j * WebPConstants.Bps)] + yuv[offset + j - WebPConstants.Bps]; + } + + Put16(dc >> 5, dst); + } + + public static void TM16(Span dst, Span yuv, int offset) + { + TrueMotion(dst, yuv, offset, 16); + } + + public static void VE16(Span dst, Span yuv, int offset) + { + // vertical + Span src = yuv.Slice(offset - WebPConstants.Bps, 16); + for (int j = 0; j < 16; ++j) + { + // memcpy(dst + j * BPS, dst - BPS, 16); + src.CopyTo(dst.Slice(j * WebPConstants.Bps)); + } + } + + public static void HE16(Span dst, Span yuv, int offset) + { + // horizontal + for (int j = 16; j > 0; --j) + { + // memset(dst, dst[-1], 16); + byte v = yuv[offset - 1]; + Memset(dst, v, 0, 16); + offset += WebPConstants.Bps; + dst = dst.Slice(WebPConstants.Bps); + } + } + + public static void DC16NoTop(Span dst, Span yuv, int offset) + { + // DC with top samples not available. + int dc = 8; + for (int j = 0; j < 16; ++j) + { + // DC += dst[-1 + j * BPS]; + dc += yuv[-1 + (j * WebPConstants.Bps) + offset]; + } + + Put16(dc >> 4, dst); + } + + public static void DC16NoLeft(Span dst, Span yuv, int offset) + { + // DC with left samples not available. + int dc = 8; + for (int i = 0; i < 16; ++i) + { + // DC += dst[i - BPS]; + dc += yuv[i - WebPConstants.Bps + offset]; + } + + Put16(dc >> 4, dst); + } + + public static void DC16NoTopLeft(Span dst) + { + // DC with no top and left samples. + Put16(0x80, dst); + } + + public static void DC8uv(Span dst, Span yuv, int offset) + { + int dc0 = 8; + for (int i = 0; i < 8; ++i) + { + // dc0 += dst[i - BPS] + dst[-1 + i * BPS]; + dc0 += yuv[offset + i - WebPConstants.Bps] + yuv[offset - 1 + (i * WebPConstants.Bps)]; + } + + Put8x8uv((byte)(dc0 >> 4), dst); + } + + public static void TM8uv(Span dst, Span yuv, int offset) + { + // TrueMotion + TrueMotion(dst, yuv, offset, 8); + } + + public static void VE8uv(Span dst, Span yuv, int offset) + { + // vertical + Span src = yuv.Slice(offset - WebPConstants.Bps, 8); + + for (int j = 0; j < 8; ++j) + { + // memcpy(dst + j * BPS, dst - BPS, 8); + src.CopyTo(dst.Slice(j * WebPConstants.Bps)); + } + } + + public static void HE8uv(Span dst, Span yuv, int offset) + { + // horizontal + for (int j = 0; j < 8; ++j) + { + // memset(dst, dst[-1], 8); + // dst += BPS; + byte v = yuv[offset - 1]; + Memset(dst, v, 0, 8); + dst = dst.Slice(WebPConstants.Bps); + offset += WebPConstants.Bps; + } + } + + public static void DC8uvNoTop(Span dst, Span yuv, int offset) + { + // DC with no top samples. + int dc0 = 4; + for (int i = 0; i < 8; ++i) + { + // dc0 += dst[-1 + i * BPS]; + dc0 += yuv[offset - 1 + (i * WebPConstants.Bps)]; + } + + Put8x8uv((byte)(dc0 >> 3), dst); + } + + public static void DC8uvNoLeft(Span dst, Span yuv, int offset) + { + // DC with no left samples. + int dc0 = 4; + for (int i = 0; i < 8; ++i) + { + // dc0 += dst[i - BPS]; + dc0 += yuv[offset + i - WebPConstants.Bps]; + } + + Put8x8uv((byte)(dc0 >> 3), dst); + } + + public static void DC8uvNoTopLeft(Span dst) + { + // DC with nothing. + Put8x8uv(0x80, dst); + } + + public static void DC4(Span dst, Span yuv, int offset) + { + int dc = 4; + for (int i = 0; i < 4; ++i) + { + dc += yuv[offset + i - WebPConstants.Bps] + yuv[offset - 1 + (i * WebPConstants.Bps)]; + } + + dc >>= 3; + for (int i = 0; i < 4; ++i) + { + Memset(dst, (byte)dc, i * WebPConstants.Bps, 4); + } + } + + public static void TM4(Span dst, Span yuv, int offset) + { + TrueMotion(dst, yuv, offset, 4); + } + + public static void VE4(Span dst, Span yuv, int offset) + { + // vertical + int topOffset = offset - WebPConstants.Bps; + byte[] vals = + { + Avg3(yuv[topOffset - 1], yuv[topOffset], yuv[topOffset + 1]), + Avg3(yuv[topOffset], yuv[topOffset + 1], yuv[topOffset + 2]), + Avg3(yuv[topOffset + 1], yuv[topOffset + 2], yuv[topOffset + 3]), + Avg3(yuv[topOffset + 2], yuv[topOffset + 3], yuv[topOffset + 4]) + }; + + for (int i = 0; i < 4; ++i) + { + vals.CopyTo(dst.Slice(i * WebPConstants.Bps)); + } + } + + public static void HE4(Span dst, Span yuv, int offset) + { + // horizontal + byte a = yuv[offset - 1 - WebPConstants.Bps]; + byte b = yuv[offset - 1]; + byte c = yuv[offset - 1 + WebPConstants.Bps]; + byte d = yuv[offset - 1 + (2 * WebPConstants.Bps)]; + byte e = yuv[offset - 1 + (3 * WebPConstants.Bps)]; + uint val = 0x01010101U * Avg3(a, b, c); + BinaryPrimitives.WriteUInt32BigEndian(dst, val); + val = 0x01010101U * Avg3(b, c, d); + BinaryPrimitives.WriteUInt32BigEndian(dst.Slice(WebPConstants.Bps), val); + val = 0x01010101U * Avg3(c, d, e); + BinaryPrimitives.WriteUInt32BigEndian(dst.Slice(2 * WebPConstants.Bps), val); + val = 0x01010101U * Avg3(d, e, e); + BinaryPrimitives.WriteUInt32BigEndian(dst.Slice(3 * WebPConstants.Bps), val); + } + + public static void RD4(Span dst, Span yuv, int offset) + { + // Down-right + byte i = yuv[offset - 1]; + byte j = yuv[offset - 1 + (1 * WebPConstants.Bps)]; + byte k = yuv[offset - 1 + (2 * WebPConstants.Bps)]; + byte l = yuv[offset - 1 + (3 * WebPConstants.Bps)]; + byte x = yuv[offset - 1 - WebPConstants.Bps]; + byte a = yuv[offset - WebPConstants.Bps]; + byte b = yuv[offset + 1 - WebPConstants.Bps]; + byte c = yuv[offset + 2 - WebPConstants.Bps]; + byte d = yuv[offset + 3 - WebPConstants.Bps]; + + Dst(dst, 0, 3, Avg3(j, k, l)); + byte ijk = Avg3(i, j, k); + Dst(dst, 1, 3, ijk); + Dst(dst, 0, 2, ijk); + byte xij = Avg3(x, i, j); + Dst(dst, 2, 3, xij); + Dst(dst, 1, 2, xij); + Dst(dst, 0, 1, xij); + byte axi = Avg3(a, x, i); + Dst(dst, 3, 3, axi); + Dst(dst, 2, 2, axi); + Dst(dst, 1, 1, axi); + Dst(dst, 0, 0, axi); + byte bax = Avg3(b, a, x); + Dst(dst, 3, 2, bax); + Dst(dst, 2, 1, bax); + Dst(dst, 1, 0, bax); + byte cba = Avg3(c, b, a); + Dst(dst, 3, 1, cba); + Dst(dst, 2, 0, cba); + Dst(dst, 3, 0, Avg3(d, c, b)); + } + + public static void VR4(Span dst, Span yuv, int offset) + { + // Vertical-Right + byte i = yuv[offset - 1]; + byte j = yuv[offset - 1 + (1 * WebPConstants.Bps)]; + byte k = yuv[offset - 1 + (2 * WebPConstants.Bps)]; + byte x = yuv[offset - 1 - WebPConstants.Bps]; + byte a = yuv[offset - WebPConstants.Bps]; + byte b = yuv[offset + 1 - WebPConstants.Bps]; + byte c = yuv[offset + 2 - WebPConstants.Bps]; + byte d = yuv[offset + 3 - WebPConstants.Bps]; + + byte xa = Avg2(x, a); + Dst(dst, 0, 0, xa); + Dst(dst, 1, 2, xa); + byte ab = Avg2(a, b); + Dst(dst, 1, 0, ab); + Dst(dst, 2, 2, ab); + byte bc = Avg2(b, c); + Dst(dst, 2, 0, bc); + Dst(dst, 3, 2, bc); + Dst(dst, 3, 0, Avg2(c, d)); + Dst(dst, 0, 3, Avg3(k, j, i)); + Dst(dst, 0, 2, Avg3(j, i, x)); + byte ixa = Avg3(i, x, a); + Dst(dst, 0, 1, ixa); + Dst(dst, 1, 3, ixa); + byte xab = Avg3(x, a, b); + Dst(dst, 1, 1, xab); + Dst(dst, 2, 3, xab); + byte abc = Avg3(a, b, c); + Dst(dst, 2, 1, abc); + Dst(dst, 3, 3, abc); + Dst(dst, 3, 1, Avg3(b, c, d)); + } + + public static void LD4(Span dst, Span yuv, int offset) + { + // Down-Left + byte a = yuv[offset - WebPConstants.Bps]; + byte b = yuv[offset + 1 - WebPConstants.Bps]; + byte c = yuv[offset + 2 - WebPConstants.Bps]; + byte d = yuv[offset + 3 - WebPConstants.Bps]; + byte e = yuv[offset + 4 - WebPConstants.Bps]; + byte f = yuv[offset + 5 - WebPConstants.Bps]; + byte g = yuv[offset + 6 - WebPConstants.Bps]; + byte h = yuv[offset + 7 - WebPConstants.Bps]; + + Dst(dst, 0, 0, Avg3(a, b, c)); + byte bcd = Avg3(b, c, d); + Dst(dst, 1, 0, bcd); + Dst(dst, 0, 1, bcd); + byte cde = Avg3(c, d, e); + Dst(dst, 2, 0, cde); + Dst(dst, 1, 1, cde); + Dst(dst, 0, 2, cde); + byte def = Avg3(d, e, f); + Dst(dst, 3, 0, def); + Dst(dst, 2, 1, def); + Dst(dst, 1, 2, def); + Dst(dst, 0, 3, def); + byte efg = Avg3(e, f, g); + Dst(dst, 3, 1, efg); + Dst(dst, 2, 2, efg); + Dst(dst, 1, 3, efg); + byte fgh = Avg3(f, g, h); + Dst(dst, 3, 2, fgh); + Dst(dst, 2, 3, fgh); + Dst(dst, 3, 3, Avg3(g, h, h)); + } + + public static void VL4(Span dst, Span yuv, int offset) + { + // Vertical-Left + byte a = yuv[offset - WebPConstants.Bps]; + byte b = yuv[offset + 1 - WebPConstants.Bps]; + byte c = yuv[offset + 2 - WebPConstants.Bps]; + byte d = yuv[offset + 3 - WebPConstants.Bps]; + byte e = yuv[offset + 4 - WebPConstants.Bps]; + byte f = yuv[offset + 5 - WebPConstants.Bps]; + byte g = yuv[offset + 6 - WebPConstants.Bps]; + byte h = yuv[offset + 7 - WebPConstants.Bps]; + + Dst(dst, 0, 0, Avg2(a, b)); + byte bc = Avg2(b, c); + Dst(dst, 1, 0, bc); + Dst(dst, 0, 2, bc); + byte cd = Avg2(c, d); + Dst(dst, 2, 0, cd); + Dst(dst, 1, 2, cd); + byte de = Avg2(d, e); + Dst(dst, 3, 0, de); + Dst(dst, 2, 2, de); + Dst(dst, 0, 1, Avg3(a, b, c)); + byte bcd = Avg3(b, c, d); + Dst(dst, 1, 1, bcd); + Dst(dst, 0, 3, bcd); + byte cde = Avg3(c, d, e); + Dst(dst, 2, 1, cde); + Dst(dst, 1, 3, cde); + byte def = Avg3(d, e, f); + Dst(dst, 3, 1, def); + Dst(dst, 2, 3, def); + Dst(dst, 3, 2, Avg3(e, f, g)); + Dst(dst, 3, 3, Avg3(f, g, h)); + } + + public static void HD4(Span dst, Span yuv, int offset) + { + // Horizontal-Down + byte i = yuv[offset - 1]; + byte j = yuv[offset - 1 + (1 * WebPConstants.Bps)]; + byte k = yuv[offset - 1 + (2 * WebPConstants.Bps)]; + byte l = yuv[offset - 1 + (3 * WebPConstants.Bps)]; + byte x = yuv[offset - 1 - WebPConstants.Bps]; + byte a = yuv[offset - WebPConstants.Bps]; + byte b = yuv[offset + 1 - WebPConstants.Bps]; + byte c = yuv[offset + 2 - WebPConstants.Bps]; + + byte ix = Avg2(i, x); + Dst(dst, 0, 0, ix); + Dst(dst, 2, 1, ix); + byte ji = Avg2(j, i); + Dst(dst, 0, 1, ji); + Dst(dst, 2, 2, ji); + byte kj = Avg2(k, j); + Dst(dst, 0, 2, kj); + Dst(dst, 2, 3, kj); + Dst(dst, 0, 3, Avg2(l, k)); + Dst(dst, 3, 0, Avg3(a, b, c)); + Dst(dst, 2, 0, Avg3(x, a, b)); + byte ixa = Avg3(i, x, a); + Dst(dst, 1, 0, ixa); + Dst(dst, 3, 1, ixa); + byte jix = Avg3(j, i, x); + Dst(dst, 1, 1, jix); + Dst(dst, 3, 2, jix); + byte kji = Avg3(k, j, i); + Dst(dst, 1, 2, kji); + Dst(dst, 3, 3, kji); + Dst(dst, 1, 3, Avg3(l, k, j)); + } + + public static void HU4(Span dst, Span yuv, int offset) + { + // Horizontal-Up + byte i = yuv[offset - 1]; + byte j = yuv[offset - 1 + (1 * WebPConstants.Bps)]; + byte k = yuv[offset - 1 + (2 * WebPConstants.Bps)]; + byte l = yuv[offset - 1 + (3 * WebPConstants.Bps)]; + + Dst(dst, 0, 0, Avg2(i, j)); + byte jk = Avg2(j, k); + Dst(dst, 2, 0, jk); + Dst(dst, 0, 1, jk); + byte kl = Avg2(k, l); + Dst(dst, 2, 1, kl); + Dst(dst, 0, 2, kl); + Dst(dst, 1, 0, Avg3(i, j, k)); + byte jkl = Avg3(j, k, l); + Dst(dst, 3, 0, jkl); + Dst(dst, 1, 1, jkl); + byte kll = Avg3(k, l, l); + Dst(dst, 3, 1, kll); + Dst(dst, 1, 2, kll); + Dst(dst, 3, 2, l); + Dst(dst, 2, 2, l); + Dst(dst, 0, 3, l); + Dst(dst, 1, 3, l); + Dst(dst, 2, 3, l); + Dst(dst, 3, 3, l); + } + + public static void Transform(Span src, Span dst, bool doTwo) + { + TransformOne(src, dst); + if (doTwo) + { + TransformOne(src.Slice(16), dst.Slice(4)); + } + } + + public static void TransformOne(Span src, Span dst) + { + var tmp = new int[4 * 4]; + int tmpOffset = 0; + int srcOffset = 0; + for (int i = 0; i < 4; ++i) + { + // vertical pass + int a = src[srcOffset] + src[srcOffset + 8]; + int b = src[srcOffset] - src[srcOffset + 8]; + int c = Mul2(src[srcOffset + 4]) - Mul1(src[srcOffset + 12]); + int d = Mul1(src[srcOffset + 4]) + Mul2(src[srcOffset + 12]); + tmp[tmpOffset] = a + d; + tmp[tmpOffset + 1] = b + c; + tmp[tmpOffset + 2] = b - c; + tmp[tmpOffset + 3] = a - d; + tmpOffset += 4; + srcOffset++; + } + + // Each pass is expanding the dynamic range by ~3.85 (upper bound). + // The exact value is (2. + (20091 + 35468) / 65536). + // After the second pass, maximum interval is [-3794, 3794], assuming + // an input in [-2048, 2047] interval. We then need to add a dst value in the [0, 255] range. + // In the worst case scenario, the input to clip_8b() can be as large as [-60713, 60968]. + tmpOffset = 0; + for (int i = 0; i < 4; ++i) + { + // horizontal pass + int dc = tmp[tmpOffset] + 4; + int a = dc + tmp[tmpOffset + 8]; + int b = dc - tmp[tmpOffset + 8]; + int c = Mul2(tmp[tmpOffset + 4]) - Mul1(tmp[tmpOffset + 12]); + int d = Mul1(tmp[tmpOffset + 4]) + Mul2(tmp[tmpOffset + 12]); + Store(dst, 0, 0, a + d); + Store(dst, 1, 0, b + c); + Store(dst, 2, 0, b - c); + Store(dst, 3, 0, a - d); + tmpOffset++; + dst = dst.Slice(WebPConstants.Bps); + } + } + + public static void TransformDc(Span src, Span dst) + { + int dc = src[0] + 4; + for (int j = 0; j < 4; ++j) + { + for (int i = 0; i < 4; ++i) + { + Store(dst, i, j, dc); + } + } + } + + // Simplified transform when only src[0], src[1] and src[4] are non-zero + public static void TransformAc3(Span src, Span dst) + { + int a = src[0] + 4; + int c4 = Mul2(src[4]); + int d4 = Mul1(src[4]); + int c1 = Mul2(src[1]); + int d1 = Mul1(src[1]); + Store2(dst, 0, a + d4, d1, c1); + Store2(dst, 1, a + c4, d1, c1); + Store2(dst, 2, a - c4, d1, c1); + Store2(dst, 3, a - d4, d1, c1); + } + + public static void TransformUv(Span src, Span dst) + { + Transform(src.Slice(0 * 16), dst, true); + Transform(src.Slice(2 * 16), dst.Slice(4 * WebPConstants.Bps), true); + } + + public static void TransformDcuv(Span src, Span dst) + { + if (src[0 * 16] != 0) + { + TransformDc(src.Slice(0 * 16), dst); + } + + if (src[1 * 16] != 0) + { + TransformDc(src.Slice(1 * 16), dst.Slice(4)); + } + + if (src[2 * 16] != 0) + { + TransformDc(src.Slice(2 * 16), dst.Slice(4 * WebPConstants.Bps)); + } + + if (src[3 * 16] != 0) + { + TransformDc(src.Slice(3 * 16), dst.Slice((4 * WebPConstants.Bps) + 4)); + } + } + + private static void TrueMotion(Span dst, Span yuv, int offset, int size) + { + // For information about how true motion works, see rfc6386, page 52. ff and section 20.14. + int topOffset = offset - WebPConstants.Bps; + Span top = yuv.Slice(topOffset); + byte p = yuv[topOffset - 1]; + int leftOffset = offset - 1; + byte left = yuv[leftOffset]; + for (int y = 0; y < size; ++y) + { + for (int x = 0; x < size; ++x) + { + dst[x] = (byte)Clamp255(left + top[x] - p); + } + + leftOffset += WebPConstants.Bps; + left = yuv[leftOffset]; + dst = dst.Slice(WebPConstants.Bps); + } + } + + // Simple In-loop filtering (Paragraph 15.2) + public static void SimpleVFilter16(Span p, int offset, int stride, int thresh) + { + int thresh2 = (2 * thresh) + 1; + for (int i = 0; i < 16; ++i) + { + if (NeedsFilter(p, offset + i, stride, thresh2)) + { + DoFilter2(p, offset + i, stride); + } + } + } + + public static void SimpleHFilter16(Span p, int offset, int stride, int thresh) + { + int thresh2 = (2 * thresh) + 1; + for (int i = 0; i < 16; ++i) + { + if (NeedsFilter(p, offset + (i * stride), 1, thresh2)) + { + DoFilter2(p, offset + (i * stride), 1); + } + } + } + + public static void SimpleVFilter16i(Span p, int offset, int stride, int thresh) + { + for (int k = 3; k > 0; --k) + { + offset += 4 * stride; + SimpleVFilter16(p, offset, stride, thresh); + } + } + + public static void SimpleHFilter16i(Span p, int offset, int stride, int thresh) + { + for (int k = 3; k > 0; --k) + { + offset += 4; + SimpleHFilter16(p, offset, stride, thresh); + } + } + + public static void VFilter16(Span p, int offset, int stride, int thresh, int ithresh, int hevThresh) + { + FilterLoop26(p, offset, stride, 1, 16, thresh, ithresh, hevThresh); + } + + public static void HFilter16(Span p, int offset, int stride, int thresh, int ithresh, int hevThresh) + { + FilterLoop26(p, offset, 1, stride, 16, thresh, ithresh, hevThresh); + } + + public static void VFilter16i(Span p, int offset, int stride, int thresh, int ithresh, int hevThresh) + { + for (int k = 3; k > 0; --k) + { + offset += 4 * stride; + FilterLoop24(p, offset, stride, 1, 16, thresh, ithresh, hevThresh); + } + } + + public static void HFilter16i(Span p, int offset, int stride, int thresh, int ithresh, int hevThresh) + { + for (int k = 3; k > 0; --k) + { + offset += 4; + FilterLoop24(p, offset, 1, stride, 16, thresh, ithresh, hevThresh); + } + } + + // 8-pixels wide variant, for chroma filtering. + public static void VFilter8(Span u, Span v, int offset, int stride, int thresh, int ithresh, int hevThresh) + { + FilterLoop26(u, offset, stride, 1, 8, thresh, ithresh, hevThresh); + FilterLoop26(v, offset, stride, 1, 8, thresh, ithresh, hevThresh); + } + + public static void HFilter8(Span u, Span v, int offset, int stride, int thresh, int ithresh, int hevThresh) + { + FilterLoop26(u, offset, 1, stride, 8, thresh, ithresh, hevThresh); + FilterLoop26(v, offset, 1, stride, 8, thresh, ithresh, hevThresh); + } + + public static void VFilter8i(Span u, Span v, int offset, int stride, int thresh, int ithresh, int hevThresh) + { + FilterLoop24(u, offset + (4 * stride), stride, 1, 8, thresh, ithresh, hevThresh); + FilterLoop24(v, offset + (4 * stride), stride, 1, 8, thresh, ithresh, hevThresh); + } + + public static void HFilter8i(Span u, Span v, int offset, int stride, int thresh, int ithresh, int hevThresh) + { + FilterLoop24(u, offset + 4, 1, stride, 8, thresh, ithresh, hevThresh); + FilterLoop24(v, offset + 4, 1, stride, 8, thresh, ithresh, hevThresh); + } + + public static uint LoadUv(byte u, byte v) + { + // We process u and v together stashed into 32bit(16bit each). + return (uint)(u | (v << 16)); + } + + public static void YuvToBgr(int y, int u, int v, Span bgr) + { + bgr[0] = (byte)YuvToB(y, u); + bgr[1] = (byte)YuvToG(y, u, v); + bgr[2] = (byte)YuvToR(y, v); + } + + public static int YuvToR(int y, int v) + { + return Clip8(MultHi(y, 19077) + MultHi(v, 26149) - 14234); + } + + public static int YuvToG(int y, int u, int v) + { + return Clip8(MultHi(y, 19077) - MultHi(u, 6419) - MultHi(v, 13320) + 8708); + } + + public static int YuvToB(int y, int u) + { + return Clip8(MultHi(y, 19077) + MultHi(u, 33050) - 17685); + } + + // Complex In-loop filtering (Paragraph 15.3) + private static void FilterLoop24( + Span p, + int offset, + int hStride, + int vStride, + int size, + int thresh, + int ithresh, + int hevThresh) + { + int thresh2 = (2 * thresh) + 1; + while (size-- > 0) + { + if (NeedsFilter2(p, offset, hStride, thresh2, ithresh)) + { + if (Hev(p, offset, hStride, hevThresh)) + { + DoFilter2(p, offset, hStride); + } + else + { + DoFilter4(p, offset, hStride); + } + } + + offset += vStride; + } + } + + private static void FilterLoop26( + Span p, + int offset, + int hStride, + int vStride, + int size, + int thresh, + int ithresh, + int hevThresh) + { + int thresh2 = (2 * thresh) + 1; + while (size-- > 0) + { + if (NeedsFilter2(p, offset, hStride, thresh2, ithresh)) + { + if (Hev(p, offset, hStride, hevThresh)) + { + DoFilter2(p, offset, hStride); + } + else + { + DoFilter6(p, offset, hStride); + } + } + + offset += vStride; + } + } + + private static void DoFilter2(Span p, int offset, int step) + { + // 4 pixels in, 2 pixels out. + int p1 = p[offset - (2 * step)]; + int p0 = p[offset - step]; + int q0 = p[offset]; + int q1 = p[offset + step]; + int a = (3 * (q0 - p0)) + WebPLookupTables.Sclip1[p1 - q1]; + int a1 = WebPLookupTables.Sclip2[(a + 4) >> 3]; + int a2 = WebPLookupTables.Sclip2[(a + 3) >> 3]; + p[offset - step] = WebPLookupTables.Clip1[p0 + a2]; + p[offset] = WebPLookupTables.Clip1[q0 - a1]; + } + + private static void DoFilter4(Span p, int offset, int step) + { + // 4 pixels in, 4 pixels out. + int p1 = p[offset - (2 * step)]; + int p0 = p[offset - step]; + int q0 = p[offset]; + int q1 = p[offset + step]; + int a = 3 * (q0 - p0); + int a1 = WebPLookupTables.Sclip2[(a + 4) >> 3]; + int a2 = WebPLookupTables.Sclip2[(a + 3) >> 3]; + int a3 = (a1 + 1) >> 1; + p[offset - (2 * step)] = WebPLookupTables.Clip1[p1 + a3]; + p[offset - step] = WebPLookupTables.Clip1[p0 + a2]; + p[offset] = WebPLookupTables.Clip1[q0 - a1]; + p[offset + step] = WebPLookupTables.Clip1[q1 - a3]; + } + + private static void DoFilter6(Span p, int offset, int step) + { + // 6 pixels in, 6 pixels out. + int p2 = p[offset - (3 * step)]; + int p1 = p[offset - (2 * step)]; + int p0 = p[offset - step]; + int q0 = p[offset]; + int q1 = p[offset + step]; + int q2 = p[offset + (2 * step)]; + int a = WebPLookupTables.Sclip1[(3 * (q0 - p0)) + WebPLookupTables.Sclip1[p1 - q1]]; + + // a is in [-128,127], a1 in [-27,27], a2 in [-18,18] and a3 in [-9,9] + int a1 = ((27 * a) + 63) >> 7; // eq. to ((3 * a + 7) * 9) >> 7 + int a2 = ((18 * a) + 63) >> 7; // eq. to ((2 * a + 7) * 9) >> 7 + int a3 = ((9 * a) + 63) >> 7; // eq. to ((1 * a + 7) * 9) >> 7 + p[offset - (3 * step)] = WebPLookupTables.Clip1[p2 + a3]; + p[offset - (2 * step)] = WebPLookupTables.Clip1[p1 + a2]; + p[offset - step] = WebPLookupTables.Clip1[p0 + a1]; + p[offset] = WebPLookupTables.Clip1[q0 - a1]; + p[offset + step] = WebPLookupTables.Clip1[q1 - a2]; + p[offset + (2 * step)] = WebPLookupTables.Clip1[q2 - a3]; + } + + private static bool NeedsFilter(Span p, int offset, int step, int t) + { + int p1 = p[offset + (-2 * step)]; + int p0 = p[offset - step]; + int q0 = p[offset]; + int q1 = p[offset + step]; + return ((4 * WebPLookupTables.Abs0[p0 - q0]) + WebPLookupTables.Abs0[p1 - q1]) <= t; + } + + private static bool NeedsFilter2(Span p, int offset, int step, int t, int it) + { + int p3 = p[offset - (4 * step)]; + int p2 = p[offset - (3 * step)]; + int p1 = p[offset - (2 * step)]; + int p0 = p[offset - step]; + int q0 = p[offset]; + int q1 = p[offset + step]; + int q2 = p[offset + (2 * step)]; + int q3 = p[offset + (3 * step)]; + if (((4 * WebPLookupTables.Abs0[p0 - q0]) + WebPLookupTables.Abs0[p1 - q1]) > t) + { + return false; + } + + return WebPLookupTables.Abs0[p3 - p2] <= it && WebPLookupTables.Abs0[p2 - p1] <= it && + WebPLookupTables.Abs0[p1 - p0] <= it && WebPLookupTables.Abs0[q3 - q2] <= it && + WebPLookupTables.Abs0[q2 - q1] <= it && WebPLookupTables.Abs0[q1 - q0] <= it; + } + + private static bool Hev(Span p, int offset, int step, int thresh) + { + int p1 = p[offset - (2 * step)]; + int p0 = p[offset - step]; + int q0 = p[offset]; + int q1 = p[offset + step]; + return (WebPLookupTables.Abs0[p1 - p0] > thresh) || (WebPLookupTables.Abs0[q1 - q0] > thresh); + } + + private static int MultHi(int v, int coeff) + { + return (v * coeff) >> 8; + } + + private static void Store(Span dst, int x, int y, int v) + { + dst[x + (y * WebPConstants.Bps)] = Clip8B(dst[x + (y * WebPConstants.Bps)] + (v >> 3)); + } + + private static void Store2(Span dst, int y, int dc, int d, int c) + { + Store(dst, 0, y, dc + d); + Store(dst, 1, y, dc + c); + Store(dst, 2, y, dc - c); + Store(dst, 3, y, dc - d); + } + + private static int Mul1(int a) + { + return ((a * 20091) >> 16) + a; + } + + private static int Mul2(int a) + { + return (a * 35468) >> 16; + } + + private static byte Clip8B(int v) + { + return (byte)((v & ~0xff) is 0 ? v : (v < 0) ? 0 : 255); + } + + private static byte Clip8(int v) + { + int yuvMask = (256 << 6) - 1; + return (byte)(((v & ~yuvMask) is 0) ? (v >> 6) : (v < 0) ? 0 : 255); + } + + private static void Put8x8uv(byte value, Span dst) + { + for (int j = 0; j < 8; ++j) + { + // memset(dst + j * BPS, value, 8); + Memset(dst, value, j * WebPConstants.Bps, 8); + } + } + + private static void Memset(Span dst, byte value, int startIdx, int count) + { + for (int i = 0; i < count; i++) + { + dst[startIdx + i] = value; + } + } + + private static byte Avg2(byte a, byte b) + { + return (byte)((a + b + 1) >> 1); + } + + private static byte Avg3(byte a, byte b, byte c) + { + return (byte)((a + (2 * b) + c + 2) >> 2); + } + + private static void Dst(Span dst, int x, int y, byte v) + { + dst[x + (y * WebPConstants.Bps)] = v; + } + + private static int Clamp255(int x) + { + return x < 0 ? 0 : (x > 255 ? 255 : x); + } + } +} diff --git a/src/ImageSharp/Formats/WebP/Readme.md b/src/ImageSharp/Formats/WebP/Readme.md new file mode 100644 index 0000000000..c4c8004644 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Readme.md @@ -0,0 +1,9 @@ +# WebP Format + +Reference implementation, specification and stuff like that: + +- [google webp introduction](https://developers.google.com/speed/webp) +- [WebP Spec 1.0.3](https://chromium.googlesource.com/webm/libwebp/+/v1.0.3/doc/webp-container-spec.txt) +- [WebP VP8 chunk Spec](http://tools.ietf.org/html/rfc6386) +- [WebP filefront](https://wiki.fileformat.com/image/webp/) +- [WebP test data](https://github.com/webmproject/libwebp-test-data/) diff --git a/src/ImageSharp/Formats/WebP/ReconstructionFilter.cs b/src/ImageSharp/Formats/WebP/ReconstructionFilter.cs new file mode 100644 index 0000000000..ff59e6b668 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/ReconstructionFilter.cs @@ -0,0 +1,12 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + internal enum ReconstructionFilter + { + None, + Bicubic, + Bilinear + } +} diff --git a/src/ImageSharp/Formats/WebP/VP8BandProbas.cs b/src/ImageSharp/Formats/WebP/VP8BandProbas.cs new file mode 100644 index 0000000000..a9c9420d16 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/VP8BandProbas.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// All the probabilities associated to one band. + /// + internal class Vp8BandProbas + { + public Vp8BandProbas() + { + this.Probabilities = new Vp8ProbaArray[WebPConstants.NumCtx]; + for (int i = 0; i < WebPConstants.NumCtx; i++) + { + this.Probabilities[i] = new Vp8ProbaArray(); + } + } + + public Vp8ProbaArray[] Probabilities { get; } + } +} diff --git a/src/ImageSharp/Formats/WebP/Vp8BitReader.cs b/src/ImageSharp/Formats/WebP/Vp8BitReader.cs new file mode 100644 index 0000000000..365ab3bddd --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Vp8BitReader.cs @@ -0,0 +1,239 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers.Binary; +using System.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// A bit reader for VP8 streams. + /// + internal class Vp8BitReader : BitReaderBase + { + private const int BitsCount = 56; + + /// + /// Current value. + /// + private ulong value; + + /// + /// Current range minus 1. In [127, 254] interval. + /// + private uint range; + + /// + /// Number of valid bits left. + /// + private int bits; + + /// + /// Max packed-read position of the buffer. + /// + private uint bufferMax; + + private uint bufferEnd; + + /// + /// True if input is exhausted. + /// + private bool eof; + + /// + /// Byte position in buffer. + /// + private long pos; + + /// + /// Initializes a new instance of the class. + /// + /// The input stream to read from. + /// The raw image data size in bytes. + /// Used for allocating memory during reading data from the stream. + /// The partition length. + /// Start index in the data array. Defaults to 0. + public Vp8BitReader(Stream inputStream, uint imageDataSize, MemoryAllocator memoryAllocator, uint partitionLength, int startPos = 0) + { + this.ImageDataSize = imageDataSize; + this.PartitionLength = partitionLength; + this.ReadImageDataFromStream(inputStream, (int)imageDataSize, memoryAllocator); + this.InitBitreader(partitionLength, startPos); + } + + /// + /// Initializes a new instance of the class. + /// + /// The raw encoded image data. + /// The partition length. + /// Start index in the data array. Defaults to 0. + public Vp8BitReader(byte[] imageData, uint partitionLength, int startPos = 0) + { + this.Data = imageData; + this.ImageDataSize = (uint)imageData.Length; + this.PartitionLength = partitionLength; + this.InitBitreader(partitionLength, startPos); + } + + public int Pos + { + get { return (int)this.pos; } + } + + public uint ImageDataSize { get; } + + public uint PartitionLength { get; } + + public uint Remaining { get; set; } + + public int GetBit(int prob) + { + uint range = this.range; + if (this.bits < 0) + { + this.LoadNewBytes(); + } + + int pos = this.bits; + uint split = (uint)((range * prob) >> 8); + ulong value = this.value >> pos; + bool bit = value > split; + if (bit) + { + range -= split; + this.value -= (ulong)(split + 1) << pos; + } + else + { + range = split + 1; + } + + int shift = 7 ^ this.BitsLog2Floor(range); + range <<= shift; + this.bits -= shift; + + this.range = range - 1; + + return bit ? 1 : 0; + } + + // Simplified version of VP8GetBit() for prob=0x80 (note shift is always 1 here) + public int GetSigned(int v) + { + if (this.bits < 0) + { + this.LoadNewBytes(); + } + + int pos = this.bits; + uint split = this.range >> 1; + ulong value = this.value >> pos; + ulong mask = (split - value) >> 31; // -1 or 0 + this.bits -= 1; + this.range += (uint)mask; + this.range |= 1; + this.value -= ((split + 1) & mask) << pos; + + return (v ^ (int)mask) - (int)mask; + } + + public bool ReadBool() + { + return this.ReadValue(1) is 1; + } + + public uint ReadValue(int nBits) + { + Guard.MustBeGreaterThan(nBits, 0, nameof(nBits)); + + uint v = 0; + while (nBits-- > 0) + { + v |= (uint)this.GetBit(0x80) << nBits; + } + + return v; + } + + public int ReadSignedValue(int nBits) + { + Guard.MustBeGreaterThan(nBits, 0, nameof(nBits)); + + int value = (int)this.ReadValue(nBits); + return this.ReadValue(1) != 0 ? -value : value; + } + + private void InitBitreader(uint size, int pos = 0) + { + this.range = 255 - 1; + this.value = 0; + this.bits = -8; // to load the very first 8 bits. + this.eof = false; + this.pos = pos; + this.bufferEnd = (uint)(pos + size); + this.bufferMax = (uint)(size > 8 ? pos + size - 8 + 1 : pos); + + this.LoadNewBytes(); + } + + private void LoadNewBytes() + { + if (this.pos < this.bufferMax) + { + ulong inBits = BinaryPrimitives.ReadUInt64LittleEndian(this.Data.AsSpan((int)this.pos, 8)); + this.pos += BitsCount >> 3; + ulong bits = this.ByteSwap64(inBits); + bits >>= 64 - BitsCount; + this.value = bits | (this.value << BitsCount); + this.bits += BitsCount; + } + else + { + this.LoadFinalBytes(); + } + } + + private void LoadFinalBytes() + { + // Only read 8bits at a time. + if (this.pos < this.bufferEnd) + { + this.bits += 8; + this.value = this.Data[this.pos++] | (this.value << 8); + } + else if (!this.eof) + { + this.value <<= 8; + this.bits += 8; + this.eof = true; + } + else + { + this.bits = 0; // This is to avoid undefined behaviour with shifts. + } + } + + private ulong ByteSwap64(ulong x) + { + x = ((x & 0xffffffff00000000ul) >> 32) | ((x & 0x00000000fffffffful) << 32); + x = ((x & 0xffff0000ffff0000ul) >> 16) | ((x & 0x0000ffff0000fffful) << 16); + x = ((x & 0xff00ff00ff00ff00ul) >> 8) | ((x & 0x00ff00ff00ff00fful) << 8); + return x; + } + + // Returns 31 ^ clz(n) = log2(n).Returns 31 ^ clz(n) = log2(n). + private int BitsLog2Floor(uint n) + { + int logValue = 0; + while (n >= 256) + { + logValue += 8; + n >>= 8; + } + + return logValue + WebPLookupTables.LogTable8bit[n]; + } + } +} diff --git a/src/ImageSharp/Formats/WebP/Vp8Decoder.cs b/src/ImageSharp/Formats/WebP/Vp8Decoder.cs new file mode 100644 index 0000000000..f47100ef3a --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Vp8Decoder.cs @@ -0,0 +1,361 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Holds information for decoding a lossy webp image. + /// + internal class Vp8Decoder : IDisposable + { + private Vp8MacroBlock leftMacroBlock; + + /// + /// Initializes a new instance of the class. + /// + /// The frame header. + /// The picture header. + /// The segment header. + /// The probabilities. + /// Used for allocating memory for the pixel data output and the temporary buffers. + public Vp8Decoder(Vp8FrameHeader frameHeader, Vp8PictureHeader pictureHeader, Vp8SegmentHeader segmentHeader, Vp8Proba probabilities, MemoryAllocator memoryAllocator) + { + this.FilterHeader = new Vp8FilterHeader(); + this.FrameHeader = frameHeader; + this.PictureHeader = pictureHeader; + this.SegmentHeader = segmentHeader; + this.Probabilities = probabilities; + this.IntraL = new byte[4]; + this.MbWidth = (int)((this.PictureHeader.Width + 15) >> 4); + this.MbHeight = (int)((this.PictureHeader.Height + 15) >> 4); + this.CacheYStride = 16 * this.MbWidth; + this.CacheUvStride = 8 * this.MbWidth; + this.MacroBlockInfo = new Vp8MacroBlock[this.MbWidth + 1]; + this.MacroBlockData = new Vp8MacroBlockData[this.MbWidth]; + this.YuvTopSamples = new Vp8TopSamples[this.MbWidth]; + this.FilterInfo = new Vp8FilterInfo[this.MbWidth]; + for (int i = 0; i < this.MbWidth; i++) + { + this.MacroBlockInfo[i] = new Vp8MacroBlock(); + this.MacroBlockData[i] = new Vp8MacroBlockData(); + this.YuvTopSamples[i] = new Vp8TopSamples(); + this.FilterInfo[i] = new Vp8FilterInfo(); + } + + this.MacroBlockInfo[this.MbWidth] = new Vp8MacroBlock(); + + this.DeQuantMatrices = new Vp8QuantMatrix[WebPConstants.NumMbSegments]; + this.FilterStrength = new Vp8FilterInfo[WebPConstants.NumMbSegments, 2]; + for (int i = 0; i < WebPConstants.NumMbSegments; i++) + { + this.DeQuantMatrices[i] = new Vp8QuantMatrix(); + for (int j = 0; j < 2; j++) + { + this.FilterStrength[i, j] = new Vp8FilterInfo(); + } + } + + uint width = pictureHeader.Width; + uint height = pictureHeader.Height; + + int extraRows = WebPConstants.FilterExtraRows[(int)LoopFilter.Complex]; // assuming worst case: complex filter + int extraY = extraRows * this.CacheYStride; + int extraUv = (extraRows / 2) * this.CacheUvStride; + this.YuvBuffer = memoryAllocator.Allocate((WebPConstants.Bps * 17) + (WebPConstants.Bps * 9) + extraY); + this.CacheY = memoryAllocator.Allocate((16 * this.CacheYStride) + extraY); + this.CacheU = memoryAllocator.Allocate((16 * this.CacheUvStride) + extraUv); + this.CacheV = memoryAllocator.Allocate((16 * this.CacheUvStride) + extraUv); + this.TmpYBuffer = memoryAllocator.Allocate((int)width); + this.TmpUBuffer = memoryAllocator.Allocate((int)width); + this.TmpVBuffer = memoryAllocator.Allocate((int)width); + this.Pixels = memoryAllocator.Allocate((int)(width * height * 4)); + + this.YuvBuffer.Memory.Span.Fill(205); + this.CacheY.Memory.Span.Fill(205); + this.CacheU.Memory.Span.Fill(205); + this.CacheV.Memory.Span.Fill(205); + + this.Vp8BitReaders = new Vp8BitReader[WebPConstants.MaxNumPartitions]; + } + + /// + /// Gets the frame header. + /// + public Vp8FrameHeader FrameHeader { get; } + + /// + /// Gets the picture header. + /// + public Vp8PictureHeader PictureHeader { get; } + + /// + /// Gets the filter header. + /// + public Vp8FilterHeader FilterHeader { get; } + + /// + /// Gets the segment header. + /// + public Vp8SegmentHeader SegmentHeader { get; } + + /// + /// Gets or sets the number of partitions minus one. + /// + public int NumPartsMinusOne { get; set; } + + /// + /// Gets the per-partition boolean decoders. + /// + public Vp8BitReader[] Vp8BitReaders { get; } + + /// + /// Gets the dequantization matrices (one set of DC/AC dequant factor per segment). + /// + public Vp8QuantMatrix[] DeQuantMatrices { get; } + + /// + /// Gets or sets a value indicating whether to use the skip probabilities. + /// + public bool UseSkipProbability { get; set; } + + /// + /// Gets or sets the skip probability. + /// + public byte SkipProbability { get; set; } + + /// + /// Gets or sets the Probabilities. + /// + public Vp8Proba Probabilities { get; set; } + + /// + /// Gets or sets the top intra modes values: 4 * MbWidth. + /// + public byte[] IntraT { get; set; } + + /// + /// Gets the left intra modes values. + /// + public byte[] IntraL { get; } + + /// + /// Gets the width in macroblock units. + /// + public int MbWidth { get; } + + /// + /// Gets the height in macroblock units. + /// + public int MbHeight { get; } + + /// + /// Gets or sets the top-left x index of the macroblock that must be in-loop filtered. + /// + public int TopLeftMbX { get; set; } + + /// + /// Gets or sets the top-left y index of the macroblock that must be in-loop filtered. + /// + public int TopLeftMbY { get; set; } + + /// + /// Gets or sets the last bottom-right x index of the macroblock that must be decoded. + /// + public int BottomRightMbX { get; set; } + + /// + /// Gets or sets the last bottom-right y index of the macroblock that must be decoded. + /// + public int BottomRightMbY { get; set; } + + /// + /// Gets or sets the current x position in macroblock units. + /// + public int MbX { get; set; } + + /// + /// Gets or sets the current y position in macroblock units. + /// + public int MbY { get; set; } + + /// + /// Gets the parsed reconstruction data. + /// + public Vp8MacroBlockData[] MacroBlockData { get; } + + /// + /// Gets the contextual macroblock info. + /// + public Vp8MacroBlock[] MacroBlockInfo { get; } + + /// + /// Gets or sets the loop filter used. The purpose of the loop filter is to eliminate (or at least reduce) + /// visually objectionable artifacts. + /// + public LoopFilter Filter { get; set; } + + /// + /// Gets the filter strengths. + /// + public Vp8FilterInfo[,] FilterStrength { get; } + + public IMemoryOwner YuvBuffer { get; } + + public Vp8TopSamples[] YuvTopSamples { get; } + + public IMemoryOwner CacheY { get; } + + public IMemoryOwner CacheU { get; } + + public IMemoryOwner CacheV { get; } + + public int CacheYOffset { get; set; } + + public int CacheUvOffset { get; set; } + + public int CacheYStride { get; } + + public int CacheUvStride { get; } + + public IMemoryOwner TmpYBuffer { get; } + + public IMemoryOwner TmpUBuffer { get; } + + public IMemoryOwner TmpVBuffer { get; } + + /// + /// Gets the pixel buffer where the decoded pixel data will be stored. + /// + public IMemoryOwner Pixels { get; } + + /// + /// Gets or sets filter strength info. + /// + public Vp8FilterInfo[] FilterInfo { get; set; } + + public Vp8MacroBlock CurrentMacroBlock + { + get + { + return this.MacroBlockInfo[this.MbX]; + } + } + + public Vp8MacroBlock LeftMacroBlock + { + get + { + if (this.leftMacroBlock is null) + { + this.leftMacroBlock = new Vp8MacroBlock(); + } + + return this.leftMacroBlock; + } + } + + public Vp8MacroBlockData CurrentBlockData + { + get + { + return this.MacroBlockData[this.MbX]; + } + } + + public void PrecomputeFilterStrengths() + { + if (this.Filter is LoopFilter.None) + { + return; + } + + Vp8FilterHeader hdr = this.FilterHeader; + for (int s = 0; s < WebPConstants.NumMbSegments; ++s) + { + int baseLevel; + + // First, compute the initial level. + if (this.SegmentHeader.UseSegment) + { + baseLevel = this.SegmentHeader.FilterStrength[s]; + if (!this.SegmentHeader.Delta) + { + baseLevel += hdr.Level; + } + } + else + { + baseLevel = hdr.Level; + } + + for (int i4x4 = 0; i4x4 <= 1; ++i4x4) + { + Vp8FilterInfo info = this.FilterStrength[s, i4x4]; + int level = baseLevel; + if (hdr.UseLfDelta) + { + level += hdr.RefLfDelta[0]; + if (i4x4 > 0) + { + level += hdr.ModeLfDelta[0]; + } + } + + level = (level < 0) ? 0 : (level > 63) ? 63 : level; + if (level > 0) + { + int iLevel = level; + if (hdr.Sharpness > 0) + { + if (hdr.Sharpness > 4) + { + iLevel >>= 2; + } + else + { + iLevel >>= 1; + } + + if (iLevel > 9 - hdr.Sharpness) + { + iLevel = 9 - hdr.Sharpness; + } + } + + if (iLevel < 1) + { + iLevel = 1; + } + + info.InnerLevel = (byte)iLevel; + info.Limit = (byte)((2 * level) + iLevel); + info.HighEdgeVarianceThreshold = (byte)((level >= 40) ? 2 : (level >= 15) ? 1 : 0); + } + else + { + info.Limit = 0; // no filtering. + } + + info.UseInnerFiltering = (byte)i4x4; + } + } + } + + /// + public void Dispose() + { + this.YuvBuffer.Dispose(); + this.CacheY.Dispose(); + this.CacheU.Dispose(); + this.CacheV.Dispose(); + this.TmpYBuffer.Dispose(); + this.TmpUBuffer.Dispose(); + this.TmpVBuffer.Dispose(); + this.Pixels.Dispose(); + } + } +} diff --git a/src/ImageSharp/Formats/WebP/Vp8FilterHeader.cs b/src/ImageSharp/Formats/WebP/Vp8FilterHeader.cs new file mode 100644 index 0000000000..7093b1bf0a --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Vp8FilterHeader.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + internal class Vp8FilterHeader + { + private const int NumRefLfDeltas = 4; + + private const int NumModeLfDeltas = 4; + + public Vp8FilterHeader() + { + this.RefLfDelta = new int[NumRefLfDeltas]; + this.ModeLfDelta = new int[NumModeLfDeltas]; + } + + /// + /// Gets or sets the loop filter. + /// + public LoopFilter LoopFilter { get; set; } + + // [0..63] + public int Level { get; set; } + + // [0..7] + public int Sharpness { get; set; } + + public bool UseLfDelta { get; set; } + + public int[] RefLfDelta { get; private set; } + + public int[] ModeLfDelta { get; private set; } + } +} diff --git a/src/ImageSharp/Formats/WebP/Vp8FilterInfo.cs b/src/ImageSharp/Formats/WebP/Vp8FilterInfo.cs new file mode 100644 index 0000000000..760e1b5c55 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Vp8FilterInfo.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Filter information. + /// + internal class Vp8FilterInfo : IDeepCloneable + { + /// + /// Initializes a new instance of the class. + /// + public Vp8FilterInfo() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The filter info to create an instance from. + public Vp8FilterInfo(Vp8FilterInfo other) + { + this.Limit = other.Limit; + this.HighEdgeVarianceThreshold = other.HighEdgeVarianceThreshold; + this.InnerLevel = other.InnerLevel; + this.UseInnerFiltering = other.UseInnerFiltering; + } + + /// + /// Gets or sets the filter limit in [3..189], or 0 if no filtering. + /// + public byte Limit { get; set; } + + /// + /// Gets or sets the inner limit in [1..63]. + /// + public byte InnerLevel { get; set; } + + /// + /// Gets or sets a value indicating whether to do inner filtering. + /// TODO: can this be a bool? + /// + public byte UseInnerFiltering { get; set; } + + /// + /// Gets or sets the high edge variance threshold in [0..2]. + /// + public byte HighEdgeVarianceThreshold { get; set; } + + /// + public IDeepCloneable DeepClone() => new Vp8FilterInfo(this); + } +} diff --git a/src/ImageSharp/Formats/WebP/Vp8FrameHeader.cs b/src/ImageSharp/Formats/WebP/Vp8FrameHeader.cs new file mode 100644 index 0000000000..c7bfa67f65 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Vp8FrameHeader.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Vp8 frame header information. + /// + internal class Vp8FrameHeader + { + /// + /// Gets or sets a value indicating whether this is a key frame. + /// + public bool KeyFrame { get; set; } + + /// + /// Gets or sets Vp8 profile [0..3]. + /// + public sbyte Profile { get; set; } + + /// + /// Gets or sets the partition length. + /// + public uint PartitionLength { get; set; } + } +} diff --git a/src/ImageSharp/Formats/WebP/Vp8HeaderType.cs b/src/ImageSharp/Formats/WebP/Vp8HeaderType.cs new file mode 100644 index 0000000000..18e311da3b --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Vp8HeaderType.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Enum for the different VP8 chunk header types. + /// + public enum Vp8HeaderType + { + /// + /// Invalid VP8 header. + /// + Invalid = 0, + + /// + /// A VP8 header. + /// + Vp8 = 1, + + /// + /// VP8 header, signaling the use of VP8L lossless format. + /// + Vp8L = 2, + + /// + /// Header for a extended-VP8 chunk. + /// + Vp8X = 3, + } +} diff --git a/src/ImageSharp/Formats/WebP/Vp8Io.cs b/src/ImageSharp/Formats/WebP/Vp8Io.cs new file mode 100644 index 0000000000..feb763129f --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Vp8Io.cs @@ -0,0 +1,80 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Formats.WebP +{ + internal ref struct Vp8Io + { + /// + /// Gets or sets the picture width in pixels (invariable). + /// Original, uncropped dimensions. + /// The actual area passed to put() is stored in /> field. + /// + public int Width { get; set; } + + /// + /// Gets or sets the picture height in pixels (invariable). + /// Original, uncropped dimensions. + /// The actual area passed to put() is stored in /> field. + /// + public int Height { get; set; } + + /// + /// Gets or sets the y-position of the current macroblock. + /// + public int MbY { get; set; } + + /// + /// Gets or sets number of columns in the sample. + /// + public int MbW { get; set; } + + /// + /// Gets or sets number of rows in the sample. + /// + public int MbH { get; set; } + + /// + /// Gets or sets the luma component. + /// + public Span Y { get; set; } + + /// + /// Gets or sets the U chroma component. + /// + public Span U { get; set; } + + /// + /// Gets or sets the V chroma component. + /// + public Span V { get; set; } + + /// + /// Gets or sets the row stride for luma. + /// + public int YStride { get; set; } + + /// + /// Gets or sets the row stride for chroma. + /// + public int UvStride { get; set; } + + public bool UseCropping { get; set; } + + public int CropLeft { get; set; } + + public int CropRight { get; set; } + + public int CropTop { get; set; } + + public int CropBottom { get; set; } + + public bool UseScaling { get; set; } + + public int ScaledWidth { get; set; } + + public int ScaledHeight { get; set; } + } +} diff --git a/src/ImageSharp/Formats/WebP/Vp8LBitReader.cs b/src/ImageSharp/Formats/WebP/Vp8LBitReader.cs new file mode 100644 index 0000000000..6f3e4610af --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Vp8LBitReader.cs @@ -0,0 +1,219 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// A bit reader for reading lossless webp streams. + /// + internal class Vp8LBitReader : BitReaderBase + { + /// + /// Maximum number of bits (inclusive) the bit-reader can handle. + /// + private const int Vp8LMaxNumBitRead = 24; + + /// + /// Number of bits prefetched. + /// + private const int Lbits = 64; + + /// + /// Minimum number of bytes ready after VP8LFillBitWindow. + /// + private const int Wbits = 32; + + private readonly uint[] bitMask = + { + 0, + 0x000001, 0x000003, 0x000007, 0x00000f, + 0x00001f, 0x00003f, 0x00007f, 0x0000ff, + 0x0001ff, 0x0003ff, 0x0007ff, 0x000fff, + 0x001fff, 0x003fff, 0x007fff, 0x00ffff, + 0x01ffff, 0x03ffff, 0x07ffff, 0x0fffff, + 0x1fffff, 0x3fffff, 0x7fffff, 0xffffff + }; + + /// + /// Pre-fetched bits. + /// + private ulong value; + + /// + /// Buffer length. + /// + private readonly long len; + + /// + /// Byte position in buffer. + /// + private long pos; + + /// + /// Current bit-reading position in value. + /// + private int bitPos; + + /// + /// Initializes a new instance of the class. + /// + /// Lossless compressed image data. + public Vp8LBitReader(byte[] data) + { + this.Data = data; + this.len = data.Length; + this.value = 0; + this.bitPos = 0; + this.Eos = false; + + ulong currentValue = 0; + for (int i = 0; i < 8; ++i) + { + currentValue |= (ulong)this.Data[i] << (8 * i); + } + + this.value = currentValue; + this.pos = 8; + } + + /// + /// Initializes a new instance of the class. + /// + /// The input stream to read from. + /// The raw image data size in bytes. + /// Used for allocating memory during reading data from the stream. + public Vp8LBitReader(Stream inputStream, uint imageDataSize, MemoryAllocator memoryAllocator) + { + long length = imageDataSize; + + this.ReadImageDataFromStream(inputStream, (int)imageDataSize, memoryAllocator); + + this.len = length; + this.value = 0; + this.bitPos = 0; + this.Eos = false; + + if (length > sizeof(long)) + { + length = sizeof(long); + } + + ulong currentValue = 0; + for (int i = 0; i < length; ++i) + { + currentValue |= (ulong)this.Data[i] << (8 * i); + } + + this.value = currentValue; + this.pos = length; + } + + /// + /// Gets or sets a value indicating whether a bit was read past the end of buffer. + /// + public bool Eos { get; set; } + + /// + /// Reads a unsigned short value from the buffer. The bits of each byte are read in least-significant-bit-first order. + /// + /// The number of bits to read (should not exceed 16). + /// A ushort value. + public uint ReadValue(int nBits) + { + Guard.MustBeGreaterThan(nBits, 0, nameof(nBits)); + + if (!this.Eos && nBits <= Vp8LMaxNumBitRead) + { + ulong val = this.PrefetchBits() & this.bitMask[nBits]; + int newBits = this.bitPos + nBits; + this.bitPos = newBits; + this.ShiftBytes(); + return (uint)val; + } + + this.SetEndOfStream(); + return 0; + } + + /// + /// Reads a single bit from the stream. + /// + /// True if the bit read was 1, false otherwise. + public bool ReadBit() + { + uint bit = this.ReadValue(1); + return bit != 0; + } + + /// + /// For jumping over a number of bits in the bit stream when accessed with PrefetchBits and FillBitWindow. + /// + /// The number of bits to advance the position. + public void AdvanceBitPosition(int numberOfBits) + { + this.bitPos += numberOfBits; + } + + /// + /// Return the pre-fetched bits, so they can be looked up. + /// + /// The pre-fetched bits. + public ulong PrefetchBits() + { + return this.value >> (this.bitPos & (Lbits - 1)); + } + + /// + /// Advances the read buffer by 4 bytes to make room for reading next 32 bits. + /// + public void FillBitWindow() + { + if (this.bitPos >= Wbits) + { + this.DoFillBitWindow(); + } + } + + /// + /// Returns true if there was an attempt at reading bit past the end of the buffer. + /// + /// True, if end of buffer was reached. + public bool IsEndOfStream() + { + return this.Eos || ((this.pos == this.len) && (this.bitPos > Lbits)); + } + + private void DoFillBitWindow() + { + this.ShiftBytes(); + } + + /// + /// If not at EOS, reload up to Vp8LLbits byte-by-byte. + /// + private void ShiftBytes() + { + while (this.bitPos >= 8 && this.pos < this.len) + { + this.value >>= 8; + this.value |= (ulong)this.Data[this.pos] << (Lbits - 8); + ++this.pos; + this.bitPos -= 8; + } + + if (this.IsEndOfStream()) + { + this.SetEndOfStream(); + } + } + + private void SetEndOfStream() + { + this.Eos = true; + this.bitPos = 0; // To avoid undefined behaviour with shifts. + } + } +} diff --git a/src/ImageSharp/Formats/WebP/Vp8LDecoder.cs b/src/ImageSharp/Formats/WebP/Vp8LDecoder.cs new file mode 100644 index 0000000000..76010fdb22 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Vp8LDecoder.cs @@ -0,0 +1,70 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Collections.Generic; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Holds information for decoding a lossless webp image. + /// + internal class Vp8LDecoder : IDisposable + { + /// + /// Initializes a new instance of the class. + /// + /// The width of the image. + /// The height of the image. + /// Used for allocating memory for the pixel data output. + public Vp8LDecoder(int width, int height, MemoryAllocator memoryAllocator) + { + this.Width = width; + this.Height = height; + this.Metadata = new Vp8LMetadata(); + this.Pixels = memoryAllocator.Allocate(width * height, AllocationOptions.Clean); + } + + /// + /// Gets or sets the width of the image to decode. + /// + public int Width { get; set; } + + /// + /// Gets or sets the height of the image to decode. + /// + public int Height { get; set; } + + /// + /// Gets or sets the necessary VP8L metadata (like huffman tables) to decode the image. + /// + public Vp8LMetadata Metadata { get; set; } + + /// + /// Gets or sets the transformations which needs to be reversed. + /// + public List Transforms { get; set; } + + /// + /// Gets the pixel data. + /// + public IMemoryOwner Pixels { get; } + + /// + public void Dispose() + { + this.Pixels.Dispose(); + this.Metadata?.HuffmanImage?.Dispose(); + + if (this.Transforms != null) + { + foreach (Vp8LTransform transform in this.Transforms) + { + transform.Data?.Dispose(); + } + } + } + } +} diff --git a/src/ImageSharp/Formats/WebP/Vp8LMetadata.cs b/src/ImageSharp/Formats/WebP/Vp8LMetadata.cs new file mode 100644 index 0000000000..737def7f41 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Vp8LMetadata.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Buffers; + +namespace SixLabors.ImageSharp.Formats.WebP +{ + internal class Vp8LMetadata + { + public int ColorCacheSize { get; set; } + + public ColorCache ColorCache { get; set; } + + public int HuffmanMask { get; set; } + + public int HuffmanSubSampleBits { get; set; } + + public int HuffmanXSize { get; set; } + + public IMemoryOwner HuffmanImage { get; set; } + + public int NumHTreeGroups { get; set; } + + public HTreeGroup[] HTreeGroups { get; set; } + + public HuffmanCode[] HuffmanTables { get; set; } + } +} diff --git a/src/ImageSharp/Formats/WebP/Vp8LTransform.cs b/src/ImageSharp/Formats/WebP/Vp8LTransform.cs new file mode 100644 index 0000000000..ae62123d31 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Vp8LTransform.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Buffers; +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Data associated with a VP8L transformation to reduce the entropy. + /// + [DebuggerDisplay("Transformtype: {TransformType}")] + internal class Vp8LTransform + { + public Vp8LTransform(Vp8LTransformType transformType, int xSize, int ySize) + { + this.TransformType = transformType; + this.XSize = xSize; + this.YSize = ySize; + } + + /// + /// Gets the transform type. + /// + public Vp8LTransformType TransformType { get; } + + /// + /// Gets or sets the subsampling bits defining the transform window. + /// + public int Bits { get; set; } + + /// + /// Gets or sets the transform window X index. + /// + public int XSize { get; set; } + + /// + /// Gets the transform window Y index. + /// + public int YSize { get; } + + /// + /// Gets or sets the transform data. + /// + public IMemoryOwner Data { get; set; } + } +} diff --git a/src/ImageSharp/Formats/WebP/Vp8LTransformType.cs b/src/ImageSharp/Formats/WebP/Vp8LTransformType.cs new file mode 100644 index 0000000000..7e1be4deb2 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Vp8LTransformType.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Enum for the different transform types. Transformations are reversible manipulations of the image data + /// that can reduce the remaining symbolic entropy by modeling spatial and color correlations. + /// Transformations can make the final compression more dense. + /// + public enum Vp8LTransformType : uint + { + /// + /// The predictor transform can be used to reduce entropy by exploiting the fact that neighboring pixels are often correlated. + /// + PredictorTransform = 0, + + /// + /// The goal of the color transform is to decorrelate the R, G and B values of each pixel. + /// Color transform keeps the green (G) value as it is, transforms red (R) based on green and transforms blue (B) based on green and then based on red. + /// + CrossColorTransform = 1, + + /// + /// The subtract green transform subtracts green values from red and blue values of each pixel. + /// When this transform is present, the decoder needs to add the green value to both red and blue. + /// There is no data associated with this transform. + /// + SubtractGreen = 2, + + /// + /// If there are not many unique pixel values, it may be more efficient to create a color index array and replace the pixel values by the array's indices. + /// The color indexing transform achieves this. + /// + ColorIndexingTransform = 3, + } +} diff --git a/src/ImageSharp/Formats/WebP/Vp8MacroBlock.cs b/src/ImageSharp/Formats/WebP/Vp8MacroBlock.cs new file mode 100644 index 0000000000..8ecaa2c83c --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Vp8MacroBlock.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Contextual macroblock information. + /// + internal class Vp8MacroBlock + { + /// + /// Gets or sets non-zero AC/DC coeffs (4bit for luma + 4bit for chroma). + /// + public uint NoneZeroAcDcCoeffs { get; set; } + + /// + /// Gets or sets non-zero DC coeff (1bit). + /// + public uint NoneZeroDcCoeffs { get; set; } + } +} diff --git a/src/ImageSharp/Formats/WebP/Vp8MacroBlockData.cs b/src/ImageSharp/Formats/WebP/Vp8MacroBlockData.cs new file mode 100644 index 0000000000..cfac56d3c7 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Vp8MacroBlockData.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Data needed to reconstruct a macroblock. + /// + internal class Vp8MacroBlockData + { + public Vp8MacroBlockData() + { + this.Modes = new byte[16]; + this.Coeffs = new short[384]; + } + + /// + /// Gets or sets the coefficient. 384 coeffs = (16+4+4) * 4*4. + /// + public short[] Coeffs { get; set; } + + /// + /// Gets or sets a value indicating whether its intra4x4. + /// + public bool IsI4x4 { get; set; } + + /// + /// Gets the modes. One 16x16 mode (#0) or sixteen 4x4 modes. + /// + public byte[] Modes { get; } + + /// + /// Gets or sets the chroma prediction mode. + /// + public byte UvMode { get; set; } + + public uint NonZeroY { get; set; } + + public uint NonZeroUv { get; set; } + + public byte Skip { get; set; } + + public byte Segment { get; set; } + } +} diff --git a/src/ImageSharp/Formats/WebP/Vp8PictureHeader.cs b/src/ImageSharp/Formats/WebP/Vp8PictureHeader.cs new file mode 100644 index 0000000000..3f80b13b29 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Vp8PictureHeader.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + internal class Vp8PictureHeader + { + /// + /// Gets or sets the width of the image. + /// + public uint Width { get; set; } + + /// + /// Gets or sets the Height of the image. + /// + public uint Height { get; set; } + + /// + /// Gets or sets the horizontal scale. + /// + public sbyte XScale { get; set; } + + /// + /// Gets or sets the vertical scale. + /// + public sbyte YScale { get; set; } + + /// + /// Gets or sets the colorspace. + /// 0 - YUV color space similar to the YCrCb color space defined in. + /// 1 - Reserved for future use. + /// + public sbyte ColorSpace { get; set; } + + /// + /// Gets or sets the clamp type. + /// 0 - Decoders are required to clamp the reconstructed pixel values to between 0 and 255 (inclusive). + /// 1 - Reconstructed pixel values are guaranteed to be between 0 and 255; no clamping is necessary. + /// + public sbyte ClampType { get; set; } + } +} diff --git a/src/ImageSharp/Formats/WebP/Vp8Proba.cs b/src/ImageSharp/Formats/WebP/Vp8Proba.cs new file mode 100644 index 0000000000..ae34f936d3 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Vp8Proba.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Data for all frame-persistent probabilities. + /// + internal class Vp8Proba + { + private const int MbFeatureTreeProbs = 3; + + public Vp8Proba() + { + this.Segments = new uint[MbFeatureTreeProbs]; + this.Bands = new Vp8BandProbas[WebPConstants.NumTypes, WebPConstants.NumBands]; + this.BandsPtr = new Vp8BandProbas[WebPConstants.NumTypes, 16 + 1]; + + for (int i = 0; i < WebPConstants.NumTypes; i++) + { + for (int j = 0; j < WebPConstants.NumBands; j++) + { + this.Bands[i, j] = new Vp8BandProbas(); + } + } + + for (int i = 0; i < WebPConstants.NumTypes; i++) + { + for (int j = 0; j < 17; j++) + { + this.BandsPtr[i, j] = new Vp8BandProbas(); + } + } + } + + public uint[] Segments { get; } + + public Vp8BandProbas[,] Bands { get; } + + public Vp8BandProbas[,] BandsPtr { get; } + } +} diff --git a/src/ImageSharp/Formats/WebP/Vp8ProbaArray.cs b/src/ImageSharp/Formats/WebP/Vp8ProbaArray.cs new file mode 100644 index 0000000000..37ccc358b2 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Vp8ProbaArray.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Probabilities associated to one of the contexts. + /// + internal class Vp8ProbaArray + { + public Vp8ProbaArray() + { + this.Probabilities = new byte[WebPConstants.NumProbas]; + } + + public byte[] Probabilities { get; } + } +} diff --git a/src/ImageSharp/Formats/WebP/Vp8Profile.cs b/src/ImageSharp/Formats/WebP/Vp8Profile.cs new file mode 100644 index 0000000000..b1d757cb53 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Vp8Profile.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// The version number setting enables or disables certain features in the bitstream. + /// + internal class Vp8Profile + { + /// + /// Gets or sets the reconstruction filter. + /// + public ReconstructionFilter ReconstructionFilter { get; set; } + + /// + /// Gets or sets the loop filter. + /// + public LoopFilter LoopFilter { get; set; } + } +} diff --git a/src/ImageSharp/Formats/WebP/Vp8QuantMatrix.cs b/src/ImageSharp/Formats/WebP/Vp8QuantMatrix.cs new file mode 100644 index 0000000000..ca9f4b69e9 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Vp8QuantMatrix.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + internal class Vp8QuantMatrix + { + public int[] Y1Mat { get; } = new int[2]; + + public int[] Y2Mat { get; } = new int[2]; + + public int[] UvMat { get; } = new int[2]; + + /// + /// Gets or sets the U/V quantizer value. + /// + public int UvQuant { get; set; } + + /// + /// Gets or sets the dithering amplitude (0 = off, max=255). + /// + public int Dither { get; set; } + } +} diff --git a/src/ImageSharp/Formats/WebP/Vp8SegmentHeader.cs b/src/ImageSharp/Formats/WebP/Vp8SegmentHeader.cs new file mode 100644 index 0000000000..27bce1f043 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Vp8SegmentHeader.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Segment features. + /// + internal class Vp8SegmentHeader + { + private const int NumMbSegments = 4; + + public Vp8SegmentHeader() + { + this.Quantizer = new byte[NumMbSegments]; + this.FilterStrength = new byte[NumMbSegments]; + } + + public bool UseSegment { get; set; } + + /// + /// Gets or sets a value indicating whether to update the segment map or not. + /// + public bool UpdateMap { get; set; } + + /// + /// Gets or sets a value indicating whether to use delta values for quantizer and filter. + /// If this value is false, absolute values are used. + /// + public bool Delta { get; set; } + + /// + /// Gets quantization changes. + /// + public byte[] Quantizer { get; private set; } + + /// + /// Gets the filter strength for segments. + /// + public byte[] FilterStrength { get; private set; } + } +} diff --git a/src/ImageSharp/Formats/WebP/Vp8TopSamples.cs b/src/ImageSharp/Formats/WebP/Vp8TopSamples.cs new file mode 100644 index 0000000000..c6382b5c62 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Vp8TopSamples.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + internal class Vp8TopSamples + { + public byte[] Y { get; } = new byte[16]; + + public byte[] U { get; } = new byte[8]; + + public byte[] V { get; } = new byte[8]; + } +} diff --git a/src/ImageSharp/Formats/WebP/WebPAlphaFilterType.cs b/src/ImageSharp/Formats/WebP/WebPAlphaFilterType.cs new file mode 100644 index 0000000000..dfdc8281cf --- /dev/null +++ b/src/ImageSharp/Formats/WebP/WebPAlphaFilterType.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Enum for the different alpha filter types. + /// + internal enum WebPAlphaFilterType + { + /// + /// No filtering. + /// + None = 0, + + /// + /// Horizontal filter. + /// + Horizontal = 1, + + /// + /// Vertical filter. + /// + Vertical = 2, + + /// + /// Gradient filter. + /// + Gradient = 3, + } +} diff --git a/src/ImageSharp/Formats/WebP/WebPBitsPerPixel.cs b/src/ImageSharp/Formats/WebP/WebPBitsPerPixel.cs new file mode 100644 index 0000000000..57bc16a66c --- /dev/null +++ b/src/ImageSharp/Formats/WebP/WebPBitsPerPixel.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Enumerates the available bits per pixel the webp image uses. + /// + public enum WebPBitsPerPixel : short + { + /// + /// 24 bits per pixel. Each pixel consists of 3 bytes. + /// + Pixel24 = 24, + + /// + /// 32 bits per pixel. Each pixel consists of 4 bytes (an alpha channel is present). + /// + Pixel32 = 32 + } +} diff --git a/src/ImageSharp/Formats/WebP/WebPChunkType.cs b/src/ImageSharp/Formats/WebP/WebPChunkType.cs new file mode 100644 index 0000000000..a03f6bfb17 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/WebPChunkType.cs @@ -0,0 +1,61 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Contains a list of different webp chunk types. + /// + public enum WebPChunkType : uint + { + /// + /// Header signaling the use of VP8 format. + /// + Vp8 = 0x56503820U, + + /// + /// Header for a extended-VP8 chunk. + /// + Vp8L = 0x5650384CU, + + /// + /// Header for a extended-VP8 chunk. + /// + Vp8X = 0x56503858U, + + /// + /// Chunk contains information about the alpha channel. + /// + Alpha = 0x414C5048U, + + /// + /// Chunk which contains a color profile. + /// + Iccp = 0x49434350U, + + /// + /// Chunk which contains EXIF metadata about the image. + /// + Exif = 0x45584946U, + + /// + /// Chunk contains XMP metadata about the image. + /// + Xmp = 0x584D5020U, + + /// + /// For an animated image, this chunk contains the global parameters of the animation. + /// + 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. + /// + Animation = 0x414E4D46, + + /// + /// TODO: not sure what this is for yet. + /// + FRGM = 0x4652474D, + } +} diff --git a/src/ImageSharp/Formats/WebP/WebPConstants.cs b/src/ImageSharp/Formats/WebP/WebPConstants.cs new file mode 100644 index 0000000000..4908c133b0 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/WebPConstants.cs @@ -0,0 +1,174 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Constants used for decoding VP8 and VP8L bitstreams. + /// + internal static class WebPConstants + { + /// + /// The list of file extensions that equate to WebP. + /// + public static readonly IEnumerable FileExtensions = new[] { "webp" }; + + /// + /// The list of mimetypes that equate to a jpeg. + /// + public static readonly IEnumerable MimeTypes = new[] { "image/webp", }; + + /// + /// Signature which identifies a VP8 header. + /// + public static readonly byte[] Vp8MagicBytes = + { + 0x9D, + 0x01, + 0x2A + }; + + /// + /// The header bytes identifying RIFF file. + /// + public static readonly byte[] RiffFourCc = + { + 0x52, // R + 0x49, // I + 0x46, // F + 0x46 // F + }; + + /// + /// The header bytes identifying a WebP. + /// + public static readonly byte[] WebPHeader = + { + 0x57, // W + 0x45, // E + 0x42, // B + 0x50 // P + }; + + /// + /// Signature byte which identifies a VP8L header. + /// + public const byte Vp8LMagicByte = 0x2F; + + /// + /// 3 bits reserved for version. + /// + public const int Vp8LVersionBits = 3; + + /// + /// Bits for width and height infos of a VPL8 image. + /// + public const int Vp8LImageSizeBits = 14; + + /// + /// Maximum number of color cache bits. + /// + public const int MaxColorCacheBits = 11; + + /// + /// The maximum number of allowed transforms in a VP8L bitstream. + /// + public const int MaxNumberOfTransforms = 4; + + public const int MaxAllowedCodeLength = 15; + + public const int DefaultCodeLength = 8; + + public const int HuffmanCodesPerMetaCode = 5; + + public const uint ArgbBlack = 0xff000000; + + public const int NumLiteralCodes = 256; + + public const int NumLengthCodes = 24; + + public const int NumDistanceCodes = 40; + + public const int LengthTableBits = 7; + + public const uint CodeLengthLiterals = 16; + + public const int CodeLengthRepeatCode = 16; + + public static readonly int[] CodeLengthExtraBits = { 2, 3, 7 }; + + public static readonly int[] CodeLengthRepeatOffsets = { 3, 3, 11 }; + + public static readonly int[] AlphabetSize = + { + NumLiteralCodes + NumLengthCodes, + NumLiteralCodes, NumLiteralCodes, NumLiteralCodes, + NumDistanceCodes + }; + + // VP8 constants from here on: + public const int NumMbSegments = 4; + + public const int MaxNumPartitions = 8; + + public const int NumTypes = 4; + + public const int NumBands = 8; + + public const int NumProbas = 11; + + public const int NumCtx = 3; + + // this is the common stride for enc/dec + public const int Bps = 32; + + // intra prediction modes (TODO: maybe use an enum for this) + public const int DcPred = 0; // predict DC using row above and column to the left + public const int TmPred = 1; // propagate second differences a la "True Motion" + public const int VPred = 2; // predict rows using row above + public const int HPred = 3; // predict columns using column to the left + + /// + /// How many extra lines are needed on the MB boundary for caching, given a filtering level. + /// Simple filter(1): up to 2 luma samples are read and 1 is written. + /// Complex filter(2): up to 4 luma samples are read and 3 are written. Same for U/V, so it's 8 samples total (because of the 2x upsampling). + /// + public static readonly byte[] FilterExtraRows = { 0, 2, 8 }; + + // Paragraph 9.9 + public static readonly int[] Bands = + { + 0, 1, 2, 3, 6, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6, 7, 0 + }; + + public static readonly short[] Scan = + { + 0 + (0 * Bps), 4 + (0 * Bps), 8 + (0 * Bps), 12 + (0 * Bps), + 0 + (4 * Bps), 4 + (4 * Bps), 8 + (4 * Bps), 12 + (4 * Bps), + 0 + (8 * Bps), 4 + (8 * Bps), 8 + (8 * Bps), 12 + (8 * Bps), + 0 + (12 * Bps), 4 + (12 * Bps), 8 + (12 * Bps), 12 + (12 * Bps) + }; + + // Residual decoding (Paragraph 13.2 / 13.3) + public static readonly byte[] Cat3 = { 173, 148, 140 }; + public static readonly byte[] Cat4 = { 176, 155, 140, 135 }; + public static readonly byte[] Cat5 = { 180, 157, 141, 134, 130 }; + public static readonly byte[] Cat6 = { 254, 254, 243, 230, 196, 177, 153, 140, 133, 130, 129 }; + public static readonly byte[] Zigzag = { 0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, 10, 7, 11, 14, 15 }; + + public static readonly sbyte[] YModesIntra4 = + { + -0, 1, + -1, 2, + -2, 3, + 4, 6, + -3, 5, + -4, -5, + -6, 7, + -7, 8, + -8, -9 + }; + } +} diff --git a/src/ImageSharp/Formats/WebP/WebPDecoder.cs b/src/ImageSharp/Formats/WebP/WebPDecoder.cs new file mode 100644 index 0000000000..3273f300d4 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/WebPDecoder.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Image decoder for generating an image out of a webp stream. + /// + public sealed class WebPDecoder : IImageDecoder, IWebPDecoderOptions, IImageInfoDetector + { + /// + /// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded. + /// + public bool IgnoreMetadata { get; set; } + + /// + public Image Decode(Configuration configuration, Stream stream) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(stream, nameof(stream)); + + return new WebPDecoderCore(configuration, this).Decode(stream); + } + + /// + public IImageInfo Identify(Configuration configuration, Stream stream) + { + Guard.NotNull(stream, nameof(stream)); + + return new WebPDecoderCore(configuration, this).Identify(stream); + } + + /// + public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); + } +} diff --git a/src/ImageSharp/Formats/WebP/WebPDecoderCore.cs b/src/ImageSharp/Formats/WebP/WebPDecoderCore.cs new file mode 100644 index 0000000000..2971b26389 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/WebPDecoderCore.cs @@ -0,0 +1,516 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers.Binary; +using System.IO; + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Performs the bitmap decoding operation. + /// + internal sealed class WebPDecoderCore + { + /// + /// Reusable buffer. + /// + private readonly byte[] buffer = new byte[4]; + + /// + /// The global configuration. + /// + private readonly Configuration configuration; + + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The stream to decode from. + /// + private Stream currentStream; + + /// + /// The webp specific metadata. + /// + private WebPMetadata webpMetadata; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// The options. + public WebPDecoderCore(Configuration configuration, IWebPDecoderOptions options) + { + this.configuration = configuration; + this.memoryAllocator = configuration.MemoryAllocator; + this.IgnoreMetadata = options.IgnoreMetadata; + } + + /// + /// Gets a value indicating whether the metadata should be ignored when the image is being decoded. + /// + public bool IgnoreMetadata { get; } + + /// + /// Gets the decoded by this decoder instance. + /// + public ImageMetadata Metadata { get; private set; } + + /// + /// Decodes the image from the specified and sets the data to the image. + /// + /// The pixel format. + /// The stream, where the image should be. + /// The decoded image. + public Image Decode(Stream stream) + where TPixel : unmanaged, IPixel + { + this.Metadata = new ImageMetadata(); + this.currentStream = stream; + + uint fileSize = this.ReadImageHeader(); + WebPImageInfo imageInfo = this.ReadVp8Info(); + if (imageInfo.Features != null && imageInfo.Features.Animation) + { + WebPThrowHelper.ThrowNotSupportedException("Animations are not supported"); + } + + var image = new Image(this.configuration, (int)imageInfo.Width, (int)imageInfo.Height, this.Metadata); + Buffer2D pixels = image.GetRootFramePixelBuffer(); + if (imageInfo.IsLossLess) + { + var losslessDecoder = new WebPLosslessDecoder(imageInfo.Vp8LBitReader, this.memoryAllocator, this.configuration); + losslessDecoder.Decode(pixels, image.Width, image.Height); + } + else + { + var lossyDecoder = new WebPLossyDecoder(imageInfo.Vp8BitReader, this.memoryAllocator, this.configuration); + lossyDecoder.Decode(pixels, image.Width, image.Height, imageInfo); + } + + // There can be optional chunks after the image data, like EXIF and XMP. + if (imageInfo.Features != null) + { + this.ParseOptionalChunks(imageInfo.Features); + } + + return image; + } + + /// + /// Reads the raw image information from the specified stream. + /// + /// The containing image data. + public IImageInfo Identify(Stream stream) + { + this.currentStream = stream; + + this.ReadImageHeader(); + WebPImageInfo imageInfo = this.ReadVp8Info(); + + return new ImageInfo(new PixelTypeInfo((int)imageInfo.BitsPerPixel), (int)imageInfo.Width, (int)imageInfo.Height, this.Metadata); + } + + /// + /// Reads and skips over the image header. + /// + /// The chunk size in bytes. + private uint ReadImageHeader() + { + // Skip FourCC header, we already know its a RIFF file at this point. + this.currentStream.Skip(4); + + // Read file size. + // The size of the file in bytes starting at offset 8. + // The file size in the header is the total size of the chunks that follow plus 4 bytes for the ‘WEBP’ FourCC. + uint chunkSize = this.ReadChunkSize(); + + // Skip 'WEBP' from the header. + this.currentStream.Skip(4); + + return chunkSize; + } + + private WebPImageInfo ReadVp8Info() + { + this.Metadata = new ImageMetadata(); + this.webpMetadata = this.Metadata.GetFormatMetadata(WebPFormat.Instance); + + WebPChunkType chunkType = this.ReadChunkType(); + + switch (chunkType) + { + case WebPChunkType.Vp8: + return this.ReadVp8Header(); + case WebPChunkType.Vp8L: + return this.ReadVp8LHeader(); + case WebPChunkType.Vp8X: + return this.ReadVp8XHeader(); + } + + WebPThrowHelper.ThrowImageFormatException("Unrecognized VP8 header"); + + return new WebPImageInfo(); + } + + /// + /// Reads an the extended webp file header. An extended file header consists of: + /// - A 'VP8X' chunk with information about features used in the file. + /// - An optional 'ICCP' chunk with color profile. + /// - An optional 'ANIM' chunk with animation control data. + /// - An optional 'ALPH' chunk with alpha channel data. + /// After the image header, image data will follow. After that optional image metadata chunks (EXIF and XMP) can follow. + /// + /// Information about this webp image. + private WebPImageInfo ReadVp8XHeader() + { + uint chunkSize = this.ReadChunkSize(); + + // The first byte contains information about the image features used. + // The first two bit of it are reserved and should be 0. TODO: should an exception be thrown if its not the case, or just ignore it? + byte imageFeatures = (byte)this.currentStream.ReadByte(); + + // If bit 3 is set, a ICC Profile Chunk should be present. + bool isIccPresent = (imageFeatures & (1 << 5)) != 0; + + // If bit 4 is set, any of the frames of the image contain transparency information ("alpha" chunk). + bool isAlphaPresent = (imageFeatures & (1 << 4)) != 0; + + // If bit 5 is set, a EXIF metadata should be present. + bool isExifPresent = (imageFeatures & (1 << 3)) != 0; + + // If bit 6 is set, XMP metadata should be present. + bool isXmpPresent = (imageFeatures & (1 << 2)) != 0; + + // If bit 7 is set, animation should be present. + bool isAnimationPresent = (imageFeatures & (1 << 1)) != 0; + + // 3 reserved bytes should follow which are supposed to be zero. + this.currentStream.Read(this.buffer, 0, 3); + + // 3 bytes for the width. + this.currentStream.Read(this.buffer, 0, 3); + this.buffer[3] = 0; + uint width = (uint)BinaryPrimitives.ReadInt32LittleEndian(this.buffer) + 1; + + // 3 bytes for the height. + this.currentStream.Read(this.buffer, 0, 3); + this.buffer[3] = 0; + uint height = (uint)BinaryPrimitives.ReadInt32LittleEndian(this.buffer) + 1; + + // Optional chunks ICCP, ALPH and ANIM can follow here. + WebPChunkType chunkType; + if (isIccPresent) + { + chunkType = this.ReadChunkType(); + if (chunkType is WebPChunkType.Iccp) + { + uint iccpChunkSize = this.ReadChunkSize(); + if (!this.IgnoreMetadata) + { + var iccpData = new byte[iccpChunkSize]; + this.currentStream.Read(iccpData, 0, (int)iccpChunkSize); + var profile = new IccProfile(iccpData); + if (profile.CheckIsValid()) + { + this.Metadata.IccProfile = profile; + } + } + else + { + this.currentStream.Skip((int)iccpChunkSize); + } + } + } + + if (isAnimationPresent) + { + this.webpMetadata.Animated = true; + + return new WebPImageInfo() + { + Width = width, + Height = height, + Features = new WebPFeatures() + { + Animation = true + } + }; + } + + byte[] alphaData = null; + byte alphaChunkHeader = 0; + if (isAlphaPresent) + { + chunkType = this.ReadChunkType(); + if (chunkType != WebPChunkType.Alpha) + { + WebPThrowHelper.ThrowImageFormatException($"unexpected chunk type {chunkType}, expected ALPH chunk is missing"); + } + + uint alphaChunkSize = this.ReadChunkSize(); + alphaChunkHeader = (byte)this.currentStream.ReadByte(); + alphaData = new byte[alphaChunkSize - 1]; + this.currentStream.Read(alphaData, 0, alphaData.Length); + } + + var features = new WebPFeatures() + { + Animation = isAnimationPresent, + Alpha = isAlphaPresent, + AlphaData = alphaData, + AlphaChunkHeader = alphaChunkHeader, + ExifProfile = isExifPresent, + IccProfile = isIccPresent, + XmpMetaData = isXmpPresent + }; + + // A VP8 or VP8L chunk should follow here. + chunkType = this.ReadChunkType(); + + // TOOD: check if VP8 or VP8L info about the dimensions match VP8X info + switch (chunkType) + { + case WebPChunkType.Vp8: + return this.ReadVp8Header(features); + case WebPChunkType.Vp8L: + return this.ReadVp8LHeader(features); + } + + WebPThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header"); + + return new WebPImageInfo(); + } + + /// + /// Reads the header of a lossy webp image. + /// + /// Webp features. + /// Information about this webp image. + private WebPImageInfo ReadVp8Header(WebPFeatures features = null) + { + this.webpMetadata.Format = WebPFormatType.Lossy; + + // VP8 data size (not including this 4 bytes). + this.currentStream.Read(this.buffer, 0, 4); + uint dataSize = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + + // remaining counts the available image data payload. + uint remaining = dataSize; + + // Paragraph 9.1 https://tools.ietf.org/html/rfc6386#page-30 + // Frame tag that contains four fields: + // - A 1-bit frame type (0 for key frames, 1 for interframes). + // - A 3-bit version number. + // - A 1-bit show_frame flag. + // - A 19-bit field containing the size of the first data partition in bytes. + this.currentStream.Read(this.buffer, 0, 3); + uint frameTag = (uint)(this.buffer[0] | (this.buffer[1] << 8) | (this.buffer[2] << 16)); + remaining -= 3; + bool isKeyFrame = (frameTag & 0x1) is 0; + if (!isKeyFrame) + { + WebPThrowHelper.ThrowImageFormatException("VP8 header indicates the image is not a key frame"); + } + + uint version = (frameTag >> 1) & 0x7; + if (version > 3) + { + WebPThrowHelper.ThrowImageFormatException($"VP8 header indicates unknown profile {version}"); + } + + bool showFrame = ((frameTag >> 4) & 0x1) is 1; + if (!showFrame) + { + WebPThrowHelper.ThrowImageFormatException("VP8 header indicates that the first frame is invisible"); + } + + uint partitionLength = frameTag >> 5; + if (partitionLength > dataSize) + { + WebPThrowHelper.ThrowImageFormatException("VP8 header contains inconsistent size information"); + } + + // Check for VP8 magic bytes. + this.currentStream.Read(this.buffer, 0, 3); + if (!this.buffer.AsSpan().Slice(0, 3).SequenceEqual(WebPConstants.Vp8MagicBytes)) + { + WebPThrowHelper.ThrowImageFormatException("VP8 magic bytes not found"); + } + + this.currentStream.Read(this.buffer, 0, 4); + uint tmp = (uint)BinaryPrimitives.ReadInt16LittleEndian(this.buffer); + uint width = tmp & 0x3fff; + sbyte xScale = (sbyte)(tmp >> 6); + tmp = (uint)BinaryPrimitives.ReadInt16LittleEndian(this.buffer.AsSpan(2)); + uint height = tmp & 0x3fff; + sbyte yScale = (sbyte)(tmp >> 6); + remaining -= 7; + if (width is 0 || height is 0) + { + WebPThrowHelper.ThrowImageFormatException("width or height can not be zero"); + } + + if (partitionLength > remaining) + { + WebPThrowHelper.ThrowImageFormatException("bad partition length"); + } + + var vp8FrameHeader = new Vp8FrameHeader() + { + KeyFrame = true, + Profile = (sbyte)version, + PartitionLength = partitionLength + }; + + var bitReader = new Vp8BitReader( + this.currentStream, + remaining, + this.memoryAllocator, + partitionLength); + bitReader.Remaining = remaining; + + return new WebPImageInfo() + { + Width = width, + Height = height, + XScale = xScale, + YScale = yScale, + BitsPerPixel = features?.Alpha is true ? WebPBitsPerPixel.Pixel32 : WebPBitsPerPixel.Pixel24, + IsLossLess = false, + Features = features, + Vp8Profile = (sbyte)version, + Vp8FrameHeader = vp8FrameHeader, + Vp8BitReader = bitReader + }; + } + + /// + /// Reads the header of a lossless webp image. + /// + /// Webp image features. + /// Information about this image. + private WebPImageInfo ReadVp8LHeader(WebPFeatures features = null) + { + this.webpMetadata.Format = WebPFormatType.Lossless; + + // VP8 data size. + uint imageDataSize = this.ReadChunkSize(); + + var bitReader = new Vp8LBitReader(this.currentStream, imageDataSize, this.memoryAllocator); + + // One byte signature, should be 0x2f. + uint signature = bitReader.ReadValue(8); + if (signature != WebPConstants.Vp8LMagicByte) + { + WebPThrowHelper.ThrowImageFormatException("Invalid VP8L signature"); + } + + // The first 28 bits of the bitstream specify the width and height of the image. + uint width = bitReader.ReadValue(WebPConstants.Vp8LImageSizeBits) + 1; + uint height = bitReader.ReadValue(WebPConstants.Vp8LImageSizeBits) + 1; + if (width is 0 || height is 0) + { + WebPThrowHelper.ThrowImageFormatException("width or height can not be zero"); + } + + // The alphaIsUsed flag should be set to 0 when all alpha values are 255 in the picture, and 1 otherwise. + // TODO: this flag value is not used yet + bool alphaIsUsed = bitReader.ReadBit(); + + // The next 3 bits are the version. The version number is a 3 bit code that must be set to 0. + // Any other value should be treated as an error. + uint version = bitReader.ReadValue(WebPConstants.Vp8LVersionBits); + if (version != 0) + { + WebPThrowHelper.ThrowNotSupportedException($"Unexpected version number {version} found in VP8L header"); + } + + return new WebPImageInfo() + { + Width = width, + Height = height, + BitsPerPixel = WebPBitsPerPixel.Pixel32, + IsLossLess = true, + Features = features, + Vp8LBitReader = bitReader + }; + } + + /// + /// Parses optional metadata chunks. There SHOULD be at most one chunk of each type ('EXIF' and 'XMP '). + /// If there are more such chunks, readers MAY ignore all except the first one. + /// Also, a file may possibly contain both 'EXIF' and 'XMP ' chunks. + /// + /// The webp features. + private void ParseOptionalChunks(WebPFeatures features) + { + if (this.IgnoreMetadata || (features.ExifProfile is false && features.XmpMetaData is false)) + { + return; + } + + while (this.currentStream.Position < this.currentStream.Length) + { + // Read chunk header. + WebPChunkType chunkType = this.ReadChunkType(); + uint chunkLength = this.ReadChunkSize(); + + if (chunkType is WebPChunkType.Exif) + { + var exifData = new byte[chunkLength]; + this.currentStream.Read(exifData, 0, (int)chunkLength); + this.Metadata.ExifProfile = new ExifProfile(exifData); + } + else + { + // Skip XMP chunk data for now. + this.currentStream.Skip((int)chunkLength); + } + } + } + + /// + /// Identifies the chunk type from the chunk. + /// + /// + /// Thrown if the input stream is not valid. + /// + private WebPChunkType ReadChunkType() + { + if (this.currentStream.Read(this.buffer, 0, 4) is 4) + { + var chunkType = (WebPChunkType)BinaryPrimitives.ReadUInt32BigEndian(this.buffer); + this.webpMetadata.ChunkTypes.Enqueue(chunkType); + return chunkType; + } + + throw new ImageFormatException("Invalid WebP data."); + } + + /// + /// Reads the chunk size. If Chunk Size is odd, a single padding byte will be added to the payload, + /// so the chunk size will be increased by 1 in those cases. + /// + /// The chunk size in bytes. + private uint ReadChunkSize() + { + if (this.currentStream.Read(this.buffer, 0, 4) is 4) + { + uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + return (chunkSize % 2 is 0) ? chunkSize : chunkSize + 1; + } + + throw new ImageFormatException("Invalid WebP data."); + } + } +} diff --git a/src/ImageSharp/Formats/WebP/WebPFeatures.cs b/src/ImageSharp/Formats/WebP/WebPFeatures.cs new file mode 100644 index 0000000000..3fd0350766 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/WebPFeatures.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Image features of a VP8X image. + /// + internal class WebPFeatures + { + /// + /// Gets or sets a value indicating whether this image has a ICC Profile. + /// + public bool IccProfile { get; set; } + + /// + /// Gets or sets a value indicating whether this image has a alpha channel. + /// + public bool Alpha { get; set; } + + /// + /// Gets or sets the alpha data, if an ALPH chunk is present. + /// + public byte[] AlphaData { get; set; } + + /// + /// Gets or sets the alpha chunk header. + /// + public byte AlphaChunkHeader { get; set; } + + /// + /// Gets or sets a value indicating whether this image has a EXIF Profile. + /// + public bool ExifProfile { get; set; } + + /// + /// Gets or sets a value indicating whether this image has XMP Metadata. + /// + public bool XmpMetaData { get; set; } + + /// + /// Gets or sets a value indicating whether this image is a animation. + /// + public bool Animation { get; set; } + } +} diff --git a/src/ImageSharp/Formats/WebP/WebPFormat.cs b/src/ImageSharp/Formats/WebP/WebPFormat.cs new file mode 100644 index 0000000000..48a5952587 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/WebPFormat.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Registers the image encoders, decoders and mime type detectors for the WebP format + /// + public sealed class WebPFormat : IImageFormat + { + /// + /// Gets the current instance. + /// + public static WebPFormat Instance { get; } = new WebPFormat(); + + /// + public string Name => "WebP"; + + /// + public string DefaultMimeType => "image/webp"; + + /// + public IEnumerable MimeTypes => WebPConstants.MimeTypes; + + /// + public IEnumerable FileExtensions => WebPConstants.FileExtensions; + + /// + public WebPMetadata CreateDefaultFormatMetadata() => new WebPMetadata(); + } +} diff --git a/src/ImageSharp/Formats/WebP/WebPFormatType.cs b/src/ImageSharp/Formats/WebP/WebPFormatType.cs new file mode 100644 index 0000000000..291281d006 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/WebPFormatType.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Info about the webp format used. + /// + public enum WebPFormatType + { + /// + /// Unknown webp format. + /// + Unknown, + + /// + /// The lossless webp format. + /// + Lossless, + + /// + /// The lossy webp format. + /// + Lossy, + } +} diff --git a/src/ImageSharp/Formats/WebP/WebPImageFormatDetector.cs b/src/ImageSharp/Formats/WebP/WebPImageFormatDetector.cs new file mode 100644 index 0000000000..dc0ecadc8a --- /dev/null +++ b/src/ImageSharp/Formats/WebP/WebPImageFormatDetector.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Detects WebP file headers. + /// + public sealed class WebPImageFormatDetector : IImageFormatDetector + { + /// + public int HeaderSize => 12; + + /// + public IImageFormat DetectFormat(ReadOnlySpan header) + { + return this.IsSupportedFileFormat(header) ? WebPFormat.Instance : null; + } + + private bool IsSupportedFileFormat(ReadOnlySpan header) + { + return header.Length >= this.HeaderSize && + this.IsRiffContainer(header) && + this.IsWebPFile(header); + } + + /// + /// Checks, if the header starts with a valid RIFF FourCC. + /// + /// The header bytes. + /// True, if its a valid RIFF FourCC. + private bool IsRiffContainer(ReadOnlySpan header) + { + return header.Slice(0, 4).SequenceEqual(WebPConstants.RiffFourCc); + } + + /// + /// Checks if 'WEBP' is present in the header. + /// + /// The header bytes. + /// True, if its a webp file. + private bool IsWebPFile(ReadOnlySpan header) + { + return header.Slice(8, 4).SequenceEqual(WebPConstants.WebPHeader); + } + } +} diff --git a/src/ImageSharp/Formats/WebP/WebPImageInfo.cs b/src/ImageSharp/Formats/WebP/WebPImageInfo.cs new file mode 100644 index 0000000000..19a72c0c28 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/WebPImageInfo.cs @@ -0,0 +1,57 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + internal class WebPImageInfo + { + /// + /// Gets or sets the bitmap width in pixels (signed integer). + /// + public uint Width { get; set; } + + /// + /// Gets or sets the bitmap height in pixels (signed integer). + /// + public uint Height { get; set; } + + public sbyte XScale { get; set; } + + public sbyte YScale { get; set; } + + /// + /// Gets or sets the bits per pixel. + /// + public WebPBitsPerPixel BitsPerPixel { get; set; } + + /// + /// Gets or sets a value indicating whether this image uses lossless compression. + /// + public bool IsLossLess { get; set; } + + /// + /// Gets or sets additional features present in a VP8X image. + /// + public WebPFeatures Features { get; set; } + + /// + /// Gets or sets the VP8 profile / version. Valid values are between 0 and 3. Default value will be the invalid value -1. + /// + public int Vp8Profile { get; set; } = -1; + + /// + /// Gets or sets the VP8 frame header. + /// + public Vp8FrameHeader Vp8FrameHeader { get; set; } + + /// + /// Gets or sets the VP8L bitreader. Will be null, if its not lossless image. + /// + public Vp8LBitReader Vp8LBitReader { get; set; } = null; + + /// + /// Gets or sets the VP8 bitreader. Will be null, if its not a lossy image. + /// + public Vp8BitReader Vp8BitReader { get; set; } = null; + } +} diff --git a/src/ImageSharp/Formats/WebP/WebPLookupTables.cs b/src/ImageSharp/Formats/WebP/WebPLookupTables.cs new file mode 100644 index 0000000000..26c4be7736 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/WebPLookupTables.cs @@ -0,0 +1,580 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.WebP +{ + internal static class WebPLookupTables + { + public static readonly Dictionary Abs0; + + public static readonly Dictionary Clip1; + + public static readonly Dictionary Sclip1; + + public static readonly Dictionary Sclip2; + + public static readonly byte[,][] ModesProba = new byte[10, 10][]; + + public static readonly int[] CodeToPlane = + { + 0x18, 0x07, 0x17, 0x19, 0x28, 0x06, 0x27, 0x29, 0x16, 0x1a, + 0x26, 0x2a, 0x38, 0x05, 0x37, 0x39, 0x15, 0x1b, 0x36, 0x3a, + 0x25, 0x2b, 0x48, 0x04, 0x47, 0x49, 0x14, 0x1c, 0x35, 0x3b, + 0x46, 0x4a, 0x24, 0x2c, 0x58, 0x45, 0x4b, 0x34, 0x3c, 0x03, + 0x57, 0x59, 0x13, 0x1d, 0x56, 0x5a, 0x23, 0x2d, 0x44, 0x4c, + 0x55, 0x5b, 0x33, 0x3d, 0x68, 0x02, 0x67, 0x69, 0x12, 0x1e, + 0x66, 0x6a, 0x22, 0x2e, 0x54, 0x5c, 0x43, 0x4d, 0x65, 0x6b, + 0x32, 0x3e, 0x78, 0x01, 0x77, 0x79, 0x53, 0x5d, 0x11, 0x1f, + 0x64, 0x6c, 0x42, 0x4e, 0x76, 0x7a, 0x21, 0x2f, 0x75, 0x7b, + 0x31, 0x3f, 0x63, 0x6d, 0x52, 0x5e, 0x00, 0x74, 0x7c, 0x41, + 0x4f, 0x10, 0x20, 0x62, 0x6e, 0x30, 0x73, 0x7d, 0x51, 0x5f, + 0x40, 0x72, 0x7e, 0x61, 0x6f, 0x50, 0x71, 0x7f, 0x60, 0x70 + }; + + // 31 ^ clz(i) + public static readonly byte[] LogTable8bit = + { + 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7 + }; + + // Paragraph 14.1 + public static readonly int[] DcTable = + { + 4, 5, 6, 7, 8, 9, 10, 10, + 11, 12, 13, 14, 15, 16, 17, 17, + 18, 19, 20, 20, 21, 21, 22, 22, + 23, 23, 24, 25, 25, 26, 27, 28, + 29, 30, 31, 32, 33, 34, 35, 36, + 37, 37, 38, 39, 40, 41, 42, 43, + 44, 45, 46, 46, 47, 48, 49, 50, + 51, 52, 53, 54, 55, 56, 57, 58, + 59, 60, 61, 62, 63, 64, 65, 66, + 67, 68, 69, 70, 71, 72, 73, 74, + 75, 76, 76, 77, 78, 79, 80, 81, + 82, 83, 84, 85, 86, 87, 88, 89, + 91, 93, 95, 96, 98, 100, 101, 102, + 104, 106, 108, 110, 112, 114, 116, 118, + 122, 124, 126, 128, 130, 132, 134, 136, + 138, 140, 143, 145, 148, 151, 154, 157 + }; + + // Paragraph 14.1 + public static readonly int[] AcTable = + { + 4, 5, 6, 7, 8, 9, 10, 11, + 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, + 28, 29, 30, 31, 32, 33, 34, 35, + 36, 37, 38, 39, 40, 41, 42, 43, + 44, 45, 46, 47, 48, 49, 50, 51, + 52, 53, 54, 55, 56, 57, 58, 60, + 62, 64, 66, 68, 70, 72, 74, 76, + 78, 80, 82, 84, 86, 88, 90, 92, + 94, 96, 98, 100, 102, 104, 106, 108, + 110, 112, 114, 116, 119, 122, 125, 128, + 131, 134, 137, 140, 143, 146, 149, 152, + 155, 158, 161, 164, 167, 170, 173, 177, + 181, 185, 189, 193, 197, 201, 205, 209, + 213, 217, 221, 225, 229, 234, 239, 245, + 249, 254, 259, 264, 269, 274, 279, 284 + }; + + // Paragraph 13 + public static readonly byte[,,,] CoeffsUpdateProba = + { + { + { + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 176, 246, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 223, 241, 252, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 249, 253, 253, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 244, 252, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 234, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 253, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 246, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 239, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 254, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 248, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 251, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 251, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 254, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 254, 253, 255, 254, 255, 255, 255, 255, 255, 255 }, + { 250, 255, 254, 255, 254, 255, 255, 255, 255, 255, 255 }, + { 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + } + }, + { + { + { 217, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 225, 252, 241, 253, 255, 255, 254, 255, 255, 255, 255 }, + { 234, 250, 241, 250, 253, 255, 253, 254, 255, 255, 255 } + }, + { + { 255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 223, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 238, 253, 254, 254, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 248, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 249, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 253, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 247, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 252, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 253, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 254, 253, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 250, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + } + }, + { + { + { 186, 251, 250, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 234, 251, 244, 254, 255, 255, 255, 255, 255, 255, 255 }, + { 251, 251, 243, 253, 254, 255, 254, 255, 255, 255, 255 } + }, + { + { 255, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 236, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 251, 253, 253, 254, 254, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 254, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 254, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + } + }, + { + { + { 248, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 250, 254, 252, 254, 255, 255, 255, 255, 255, 255, 255 }, + { 248, 254, 249, 253, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 253, 253, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 246, 253, 253, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 252, 254, 251, 254, 254, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 254, 252, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 248, 254, 253, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 253, 255, 254, 254, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 251, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 245, 251, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 253, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 251, 253, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 252, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 252, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 249, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 255, 253, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 250, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + }, + { + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, + { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 } + } + } + }; + + // Paragraph 13.5: Default Token Probability Table. + public static readonly byte[,,,] DefaultCoeffsProba = + { + { + { + { 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128 }, + { 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128 }, + { 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128 } + }, + { + { 253, 136, 254, 255, 228, 219, 128, 128, 128, 128, 128 }, + { 189, 129, 242, 255, 227, 213, 255, 219, 128, 128, 128 }, + { 106, 126, 227, 252, 214, 209, 255, 255, 128, 128, 128 } + }, + { + { 1, 98, 248, 255, 236, 226, 255, 255, 128, 128, 128 }, + { 181, 133, 238, 254, 221, 234, 255, 154, 128, 128, 128 }, + { 78, 134, 202, 247, 198, 180, 255, 219, 128, 128, 128 }, + }, + { + { 1, 185, 249, 255, 243, 255, 128, 128, 128, 128, 128 }, + { 184, 150, 247, 255, 236, 224, 128, 128, 128, 128, 128 }, + { 77, 110, 216, 255, 236, 230, 128, 128, 128, 128, 128 }, + }, + { + { 1, 101, 251, 255, 241, 255, 128, 128, 128, 128, 128 }, + { 170, 139, 241, 252, 236, 209, 255, 255, 128, 128, 128 }, + { 37, 116, 196, 243, 228, 255, 255, 255, 128, 128, 128 } + }, + { + { 1, 204, 254, 255, 245, 255, 128, 128, 128, 128, 128 }, + { 207, 160, 250, 255, 238, 128, 128, 128, 128, 128, 128 }, + { 102, 103, 231, 255, 211, 171, 128, 128, 128, 128, 128 } + }, + { + { 1, 152, 252, 255, 240, 255, 128, 128, 128, 128, 128 }, + { 177, 135, 243, 255, 234, 225, 128, 128, 128, 128, 128 }, + { 80, 129, 211, 255, 194, 224, 128, 128, 128, 128, 128 } + }, + { + { 1, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128 }, + { 246, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128 }, + { 255, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128 } + } + }, + { + { + { 198, 35, 237, 223, 193, 187, 162, 160, 145, 155, 62 }, + { 131, 45, 198, 221, 172, 176, 220, 157, 252, 221, 1 }, + { 68, 47, 146, 208, 149, 167, 221, 162, 255, 223, 128 } + }, + { + { 1, 149, 241, 255, 221, 224, 255, 255, 128, 128, 128 }, + { 184, 141, 234, 253, 222, 220, 255, 199, 128, 128, 128 }, + { 81, 99, 181, 242, 176, 190, 249, 202, 255, 255, 128 } + }, + { + { 1, 129, 232, 253, 214, 197, 242, 196, 255, 255, 128 }, + { 99, 121, 210, 250, 201, 198, 255, 202, 128, 128, 128 }, + { 23, 91, 163, 242, 170, 187, 247, 210, 255, 255, 128 } + }, + { + { 1, 200, 246, 255, 234, 255, 128, 128, 128, 128, 128 }, + { 109, 178, 241, 255, 231, 245, 255, 255, 128, 128, 128 }, + { 44, 130, 201, 253, 205, 192, 255, 255, 128, 128, 128 } + }, + { + { 1, 132, 239, 251, 219, 209, 255, 165, 128, 128, 128 }, + { 94, 136, 225, 251, 218, 190, 255, 255, 128, 128, 128 }, + { 22, 100, 174, 245, 186, 161, 255, 199, 128, 128, 128 } + }, + { + { 1, 182, 249, 255, 232, 235, 128, 128, 128, 128, 128 }, + { 124, 143, 241, 255, 227, 234, 128, 128, 128, 128, 128 }, + { 35, 77, 181, 251, 193, 211, 255, 205, 128, 128, 128 } + }, + { + { 1, 157, 247, 255, 236, 231, 255, 255, 128, 128, 128 }, + { 121, 141, 235, 255, 225, 227, 255, 255, 128, 128, 128 }, + { 45, 99, 188, 251, 195, 217, 255, 224, 128, 128, 128 } + }, + { + { 1, 1, 251, 255, 213, 255, 128, 128, 128, 128, 128 }, + { 203, 1, 248, 255, 255, 128, 128, 128, 128, 128, 128 }, + { 137, 1, 177, 255, 224, 255, 128, 128, 128, 128, 128 } + } + }, + { + { + { 253, 9, 248, 251, 207, 208, 255, 192, 128, 128, 128 }, + { 175, 13, 224, 243, 193, 185, 249, 198, 255, 255, 128 }, + { 73, 17, 171, 221, 161, 179, 236, 167, 255, 234, 128 } + }, + { + { 1, 95, 247, 253, 212, 183, 255, 255, 128, 128, 128 }, + { 239, 90, 244, 250, 211, 209, 255, 255, 128, 128, 128 }, + { 155, 77, 195, 248, 188, 195, 255, 255, 128, 128, 128 } + }, + { + { 1, 24, 239, 251, 218, 219, 255, 205, 128, 128, 128 }, + { 201, 51, 219, 255, 196, 186, 128, 128, 128, 128, 128 }, + { 69, 46, 190, 239, 201, 218, 255, 228, 128, 128, 128 } + }, + { + { 1, 191, 251, 255, 255, 128, 128, 128, 128, 128, 128 }, + { 223, 165, 249, 255, 213, 255, 128, 128, 128, 128, 128 }, + { 141, 124, 248, 255, 255, 128, 128, 128, 128, 128, 128 } + }, + { + { 1, 16, 248, 255, 255, 128, 128, 128, 128, 128, 128 }, + { 190, 36, 230, 255, 236, 255, 128, 128, 128, 128, 128 }, + { 149, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128 } + }, + { + { 1, 226, 255, 128, 128, 128, 128, 128, 128, 128, 128 }, + { 247, 192, 255, 128, 128, 128, 128, 128, 128, 128, 128 }, + { 240, 128, 255, 128, 128, 128, 128, 128, 128, 128, 128 } + }, + { + { 1, 134, 252, 255, 255, 128, 128, 128, 128, 128, 128 }, + { 213, 62, 250, 255, 255, 128, 128, 128, 128, 128, 128 }, + { 55, 93, 255, 128, 128, 128, 128, 128, 128, 128, 128 } + }, + { + { 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128 }, + { 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128 }, + { 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128 } + } + }, + { + { + { 202, 24, 213, 235, 186, 191, 220, 160, 240, 175, 255 }, + { 126, 38, 182, 232, 169, 184, 228, 174, 255, 187, 128 }, + { 61, 46, 138, 219, 151, 178, 240, 170, 255, 216, 128 } + }, + { + { 1, 112, 230, 250, 199, 191, 247, 159, 255, 255, 128 }, + { 166, 109, 228, 252, 211, 215, 255, 174, 128, 128, 128 }, + { 39, 77, 162, 232, 172, 180, 245, 178, 255, 255, 128 } + }, + { + { 1, 52, 220, 246, 198, 199, 249, 220, 255, 255, 128 }, + { 124, 74, 191, 243, 183, 193, 250, 221, 255, 255, 128 }, + { 24, 71, 130, 219, 154, 170, 243, 182, 255, 255, 128 } + }, + { + { 1, 182, 225, 249, 219, 240, 255, 224, 128, 128, 128 }, + { 149, 150, 226, 252, 216, 205, 255, 171, 128, 128, 128 }, + { 28, 108, 170, 242, 183, 194, 254, 223, 255, 255, 128 } + }, + { + { 1, 81, 230, 252, 204, 203, 255, 192, 128, 128, 128 }, + { 123, 102, 209, 247, 188, 196, 255, 233, 128, 128, 128 }, + { 20, 95, 153, 243, 164, 173, 255, 203, 128, 128, 128 } + }, + { + { 1, 222, 248, 255, 216, 213, 128, 128, 128, 128, 128 }, + { 168, 175, 246, 252, 235, 205, 255, 255, 128, 128, 128 }, + { 47, 116, 215, 255, 211, 212, 255, 255, 128, 128, 128 } + }, + { + { 1, 121, 236, 253, 212, 214, 255, 255, 128, 128, 128 }, + { 141, 84, 213, 252, 201, 202, 255, 219, 128, 128, 128 }, + { 42, 80, 160, 240, 162, 185, 255, 205, 128, 128, 128 } + }, + { + { 1, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128 }, + { 244, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128 }, + { 238, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128 } + } + } + }; + + static WebPLookupTables() + { + // TODO: maybe use hashset here + Abs0 = new Dictionary(); + for (int i = -255; i <= 255; ++i) + { + Abs0[i] = (byte)((i < 0) ? -i : i); + } + + Clip1 = new Dictionary(); + for (int i = -255; i <= 255 + 255; ++i) + { + Clip1[i] = (byte)((i < 0) ? 0 : (i > 255) ? 255 : i); + } + + Sclip1 = new Dictionary(); + for (int i = -1020; i <= 1020; ++i) + { + Sclip1[i] = (sbyte)((i < -128) ? -128 : (i > 127) ? 127 : i); + } + + Sclip2 = new Dictionary(); + for (int i = -112; i <= 112; ++i) + { + Sclip2[i] = (sbyte)((i < -16) ? -16 : (i > 15) ? 15 : i); + } + + InitializeModesProbabilities(); + } + + private static void InitializeModesProbabilities() + { + // Paragraph 11.5 + ModesProba[0, 0] = new byte[] { 231, 120, 48, 89, 115, 113, 120, 152, 112 }; + ModesProba[0, 1] = new byte[] { 152, 179, 64, 126, 170, 118, 46, 70, 95 }; + ModesProba[0, 2] = new byte[] { 175, 69, 143, 80, 85, 82, 72, 155, 103 }; + ModesProba[0, 3] = new byte[] { 56, 58, 10, 171, 218, 189, 17, 13, 152 }; + ModesProba[0, 4] = new byte[] { 114, 26, 17, 163, 44, 195, 21, 10, 173 }; + ModesProba[0, 5] = new byte[] { 121, 24, 80, 195, 26, 62, 44, 64, 85 }; + ModesProba[0, 6] = new byte[] { 144, 71, 10, 38, 171, 213, 144, 34, 26 }; + ModesProba[0, 7] = new byte[] { 170, 46, 55, 19, 136, 160, 33, 206, 71 }; + ModesProba[0, 8] = new byte[] { 63, 20, 8, 114, 114, 208, 12, 9, 226 }; + ModesProba[0, 9] = new byte[] { 81, 40, 11, 96, 182, 84, 29, 16, 36 }; + ModesProba[1, 0] = new byte[] { 134, 183, 89, 137, 98, 101, 106, 165, 148 }; + ModesProba[1, 1] = new byte[] { 72, 187, 100, 130, 157, 111, 32, 75, 80 }; + ModesProba[1, 2] = new byte[] { 66, 102, 167, 99, 74, 62, 40, 234, 128 }; + ModesProba[1, 3] = new byte[] { 41, 53, 9, 178, 241, 141, 26, 8, 107 }; + ModesProba[1, 4] = new byte[] { 74, 43, 26, 146, 73, 166, 49, 23, 157 }; + ModesProba[1, 5] = new byte[] { 65, 38, 105, 160, 51, 52, 31, 115, 128 }; + ModesProba[1, 6] = new byte[] { 104, 79, 12, 27, 217, 255, 87, 17, 7 }; + ModesProba[1, 7] = new byte[] { 87, 68, 71, 44, 114, 51, 15, 186, 23 }; + ModesProba[1, 8] = new byte[] { 47, 41, 14, 110, 182, 183, 21, 17, 194 }; + ModesProba[1, 9] = new byte[] { 66, 45, 25, 102, 197, 189, 23, 18, 22 }; + ModesProba[2, 0] = new byte[] { 88, 88, 147, 150, 42, 46, 45, 196, 205 }; + ModesProba[2, 1] = new byte[] { 43, 97, 183, 117, 85, 38, 35, 179, 61 }; + ModesProba[2, 2] = new byte[] { 39, 53, 200, 87, 26, 21, 43, 232, 171 }; + ModesProba[2, 3] = new byte[] { 56, 34, 51, 104, 114, 102, 29, 93, 77 }; + ModesProba[2, 4] = new byte[] { 39, 28, 85, 171, 58, 165, 90, 98, 64 }; + ModesProba[2, 5] = new byte[] { 34, 22, 116, 206, 23, 34, 43, 166, 73 }; + ModesProba[2, 6] = new byte[] { 107, 54, 32, 26, 51, 1, 81, 43, 31 }; + ModesProba[2, 7] = new byte[] { 68, 25, 106, 22, 64, 171, 36, 225, 114 }; + ModesProba[2, 8] = new byte[] { 34, 19, 21, 102, 132, 188, 16, 76, 124 }; + ModesProba[2, 9] = new byte[] { 62, 18, 78, 95, 85, 57, 50, 48, 51 }; + ModesProba[3, 0] = new byte[] { 193, 101, 35, 159, 215, 111, 89, 46, 111 }; + ModesProba[3, 1] = new byte[] { 60, 148, 31, 172, 219, 228, 21, 18, 111 }; + ModesProba[3, 2] = new byte[] { 112, 113, 77, 85, 179, 255, 38, 120, 114 }; + ModesProba[3, 3] = new byte[] { 40, 42, 1, 196, 245, 209, 10, 25, 109 }; + ModesProba[3, 4] = new byte[] { 88, 43, 29, 140, 166, 213, 37, 43, 154 }; + ModesProba[3, 5] = new byte[] { 61, 63, 30, 155, 67, 45, 68, 1, 209 }; + ModesProba[3, 6] = new byte[] { 100, 80, 8, 43, 154, 1, 51, 26, 71 }; + ModesProba[3, 7] = new byte[] { 142, 78, 78, 16, 255, 128, 34, 197, 171 }; + ModesProba[3, 8] = new byte[] { 41, 40, 5, 102, 211, 183, 4, 1, 221 }; + ModesProba[3, 9] = new byte[] { 51, 50, 17, 168, 209, 192, 23, 25, 82 }; + ModesProba[4, 0] = new byte[] { 138, 31, 36, 171, 27, 166, 38, 44, 229 }; + ModesProba[4, 1] = new byte[] { 67, 87, 58, 169, 82, 115, 26, 59, 179 }; + ModesProba[4, 2] = new byte[] { 63, 59, 90, 180, 59, 166, 93, 73, 154 }; + ModesProba[4, 3] = new byte[] { 40, 40, 21, 116, 143, 209, 34, 39, 175 }; + ModesProba[4, 4] = new byte[] { 47, 15, 16, 183, 34, 223, 49, 45, 183 }; + ModesProba[4, 5] = new byte[] { 46, 17, 33, 183, 6, 98, 15, 32, 183 }; + ModesProba[4, 6] = new byte[] { 57, 46, 22, 24, 128, 1, 54, 17, 37 }; + ModesProba[4, 7] = new byte[] { 65, 32, 73, 115, 28, 128, 23, 128, 205 }; + ModesProba[4, 8] = new byte[] { 40, 3, 9, 115, 51, 192, 18, 6, 223 }; + ModesProba[4, 9] = new byte[] { 87, 37, 9, 115, 59, 77, 64, 21, 47 }; + ModesProba[5, 0] = new byte[] { 104, 55, 44, 218, 9, 54, 53, 130, 226 }; + ModesProba[5, 1] = new byte[] { 64, 90, 70, 205, 40, 41, 23, 26, 57 }; + ModesProba[5, 2] = new byte[] { 54, 57, 112, 184, 5, 41, 38, 166, 213 }; + ModesProba[5, 3] = new byte[] { 30, 34, 26, 133, 152, 116, 10, 32, 134 }; + ModesProba[5, 4] = new byte[] { 39, 19, 53, 221, 26, 114, 32, 73, 255 }; + ModesProba[5, 5] = new byte[] { 31, 9, 65, 234, 2, 15, 1, 118, 73 }; + ModesProba[5, 6] = new byte[] { 75, 32, 12, 51, 192, 255, 160, 43, 51 }; + ModesProba[5, 7] = new byte[] { 88, 31, 35, 67, 102, 85, 55, 186, 85 }; + ModesProba[5, 8] = new byte[] { 56, 21, 23, 111, 59, 205, 45, 37, 192 }; + ModesProba[5, 9] = new byte[] { 55, 38, 70, 124, 73, 102, 1, 34, 98 }; + ModesProba[6, 0] = new byte[] { 125, 98, 42, 88, 104, 85, 117, 175, 82 }; + ModesProba[6, 1] = new byte[] { 95, 84, 53, 89, 128, 100, 113, 101, 45 }; + ModesProba[6, 2] = new byte[] { 75, 79, 123, 47, 51, 128, 81, 171, 1 }; + ModesProba[6, 3] = new byte[] { 57, 17, 5, 71, 102, 57, 53, 41, 49 }; + ModesProba[6, 4] = new byte[] { 38, 33, 13, 121, 57, 73, 26, 1, 85 }; + ModesProba[6, 5] = new byte[] { 41, 10, 67, 138, 77, 110, 90, 47, 114 }; + ModesProba[6, 6] = new byte[] { 115, 21, 2, 10, 102, 255, 166, 23, 6 }; + ModesProba[6, 7] = new byte[] { 101, 29, 16, 10, 85, 128, 101, 196, 26 }; + ModesProba[6, 8] = new byte[] { 57, 18, 10, 102, 102, 213, 34, 20, 43 }; + ModesProba[6, 9] = new byte[] { 117, 20, 15, 36, 163, 128, 68, 1, 26 }; + ModesProba[7, 0] = new byte[] { 102, 61, 71, 37, 34, 53, 31, 243, 192 }; + ModesProba[7, 1] = new byte[] { 69, 60, 71, 38, 73, 119, 28, 222, 37 }; + ModesProba[7, 2] = new byte[] { 68, 45, 128, 34, 1, 47, 11, 245, 171 }; + ModesProba[7, 3] = new byte[] { 62, 17, 19, 70, 146, 85, 55, 62, 70 }; + ModesProba[7, 4] = new byte[] { 37, 43, 37, 154, 100, 163, 85, 160, 1 }; + ModesProba[7, 5] = new byte[] { 63, 9, 92, 136, 28, 64, 32, 201, 85 }; + ModesProba[7, 6] = new byte[] { 75, 15, 9, 9, 64, 255, 184, 119, 16 }; + ModesProba[7, 7] = new byte[] { 86, 6, 28, 5, 64, 255, 25, 248, 1 }; + ModesProba[7, 8] = new byte[] { 56, 8, 17, 132, 137, 255, 55, 116, 128 }; + ModesProba[7, 9] = new byte[] { 58, 15, 20, 82, 135, 57, 26, 121, 40 }; + ModesProba[8, 0] = new byte[] { 164, 50, 31, 137, 154, 133, 25, 35, 218 }; + ModesProba[8, 1] = new byte[] { 51, 103, 44, 131, 131, 123, 31, 6, 158 }; + ModesProba[8, 2] = new byte[] { 86, 40, 64, 135, 148, 224, 45, 183, 128 }; + ModesProba[8, 3] = new byte[] { 22, 26, 17, 131, 240, 154, 14, 1, 209 }; + ModesProba[8, 4] = new byte[] { 45, 16, 21, 91, 64, 222, 7, 1, 197 }; + ModesProba[8, 5] = new byte[] { 56, 21, 39, 155, 60, 138, 23, 102, 213 }; + ModesProba[8, 6] = new byte[] { 83, 12, 13, 54, 192, 255, 68, 47, 28 }; + ModesProba[8, 7] = new byte[] { 85, 26, 85, 85, 128, 128, 32, 146, 171 }; + ModesProba[8, 8] = new byte[] { 18, 11, 7, 63, 144, 171, 4, 4, 246 }; + ModesProba[8, 9] = new byte[] { 35, 27, 10, 146, 174, 171, 12, 26, 128 }; + ModesProba[9, 0] = new byte[] { 190, 80, 35, 99, 180, 80, 126, 54, 45 }; + ModesProba[9, 1] = new byte[] { 85, 126, 47, 87, 176, 51, 41, 20, 32 }; + ModesProba[9, 2] = new byte[] { 101, 75, 128, 139, 118, 146, 116, 128, 85 }; + ModesProba[9, 3] = new byte[] { 56, 41, 15, 176, 236, 85, 37, 9, 62 }; + ModesProba[9, 4] = new byte[] { 71, 30, 17, 119, 118, 255, 17, 18, 138 }; + ModesProba[9, 5] = new byte[] { 101, 38, 60, 138, 55, 70, 43, 26, 142 }; + ModesProba[9, 6] = new byte[] { 146, 36, 19, 30, 171, 255, 97, 27, 20 }; + ModesProba[9, 7] = new byte[] { 138, 45, 61, 62, 219, 1, 81, 188, 64 }; + ModesProba[9, 8] = new byte[] { 32, 41, 20, 117, 151, 142, 20, 21, 163 }; + ModesProba[9, 9] = new byte[] { 112, 19, 12, 61, 195, 128, 48, 4, 24 }; + } + } +} diff --git a/src/ImageSharp/Formats/WebP/WebPLosslessDecoder.cs b/src/ImageSharp/Formats/WebP/WebPLosslessDecoder.cs new file mode 100644 index 0000000000..1f0d363295 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/WebPLosslessDecoder.cs @@ -0,0 +1,1027 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Decoder for lossless webp images. This code is a port of libwebp, which can be found here: https://chromium.googlesource.com/webm/libwebp + /// + /// + /// The lossless specification can be found here: + /// https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification + /// + internal sealed class WebPLosslessDecoder + { + /// + /// A bit reader for reading lossless webp streams. + /// + private readonly Vp8LBitReader bitReader; + + /// + /// The global configuration. + /// + private readonly Configuration configuration; + + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + private static readonly int BitsSpecialMarker = 0x100; + + private static readonly int NumArgbCacheRows = 16; + + private static readonly uint PackedNonLiteralCode = 0; + + private static readonly int CodeToPlaneCodes = WebPLookupTables.CodeToPlane.Length; + + private static readonly int FixedTableSize = (630 * 3) + 410; + + private static readonly int[] TableSize = + { + FixedTableSize + 654, + FixedTableSize + 656, + FixedTableSize + 658, + FixedTableSize + 662, + FixedTableSize + 670, + FixedTableSize + 686, + FixedTableSize + 718, + FixedTableSize + 782, + FixedTableSize + 912, + FixedTableSize + 1168, + FixedTableSize + 1680, + FixedTableSize + 2704 + }; + + private static readonly byte[] CodeLengthCodeOrder = { 17, 18, 0, 1, 2, 3, 4, 5, 16, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; + + private static readonly int NumCodeLengthCodes = CodeLengthCodeOrder.Length; + + private static readonly byte[] LiteralMap = + { + 0, 1, 1, 1, 0 + }; + + /// + /// Initializes a new instance of the class. + /// + /// Bitreader to read from the stream. + /// Used for allocating memory during processing operations. + /// The configuration. + public WebPLosslessDecoder(Vp8LBitReader bitReader, MemoryAllocator memoryAllocator, Configuration configuration) + { + this.bitReader = bitReader; + this.memoryAllocator = memoryAllocator; + this.configuration = configuration; + } + + /// + /// Decodes the image from the stream using the bitreader. + /// + /// The pixel format. + /// The pixel buffer to store the decoded data. + /// The width of the image. + /// The height of the image. + public void Decode(Buffer2D pixels, int width, int height) + where TPixel : unmanaged, IPixel + { + using (var decoder = new Vp8LDecoder(width, height, this.memoryAllocator)) + { + this.DecodeImageStream(decoder, width, height, true); + this.DecodeImageData(decoder, decoder.Pixels.Memory.Span); + this.DecodePixelValues(decoder, pixels); + } + } + + public IMemoryOwner DecodeImageStream(Vp8LDecoder decoder, int xSize, int ySize, bool isLevel0) + { + int transformXSize = xSize; + int transformYSize = ySize; + int numberOfTransformsPresent = 0; + if (isLevel0) + { + decoder.Transforms = new List(WebPConstants.MaxNumberOfTransforms); + + // Next bit indicates, if a transformation is present. + while (this.bitReader.ReadBit()) + { + if (numberOfTransformsPresent > WebPConstants.MaxNumberOfTransforms) + { + WebPThrowHelper.ThrowImageFormatException($"The maximum number of transforms of {WebPConstants.MaxNumberOfTransforms} was exceeded"); + } + + this.ReadTransformation(transformXSize, transformYSize, decoder); + if (decoder.Transforms[numberOfTransformsPresent].TransformType == Vp8LTransformType.ColorIndexingTransform) + { + transformXSize = LosslessUtils.SubSampleSize(transformXSize, decoder.Transforms[numberOfTransformsPresent].Bits); + } + + numberOfTransformsPresent++; + } + } + else + { + decoder.Metadata = new Vp8LMetadata(); + } + + // Color cache. + bool colorCachePresent = this.bitReader.ReadBit(); + int colorCacheBits = 0; + int colorCacheSize = 0; + if (colorCachePresent) + { + colorCacheBits = (int)this.bitReader.ReadValue(4); + bool coloCacheBitsIsValid = colorCacheBits >= 1 && colorCacheBits <= WebPConstants.MaxColorCacheBits; + if (!coloCacheBitsIsValid) + { + WebPThrowHelper.ThrowImageFormatException("Invalid color cache bits found"); + } + } + + // Read the Huffman codes (may recurse). + this.ReadHuffmanCodes(decoder, transformXSize, transformYSize, colorCacheBits, isLevel0); + decoder.Metadata.ColorCacheSize = colorCacheSize; + + // Finish setting up the color-cache. + if (colorCachePresent) + { + decoder.Metadata.ColorCache = new ColorCache(); + colorCacheSize = 1 << colorCacheBits; + decoder.Metadata.ColorCacheSize = colorCacheSize; + if (!(colorCacheBits >= 1 && colorCacheBits <= WebPConstants.MaxColorCacheBits)) + { + WebPThrowHelper.ThrowImageFormatException("Invalid color cache bits found"); + } + + decoder.Metadata.ColorCache.Init(colorCacheBits); + } + else + { + decoder.Metadata.ColorCacheSize = 0; + } + + this.UpdateDecoder(decoder, transformXSize, transformYSize); + if (isLevel0) + { + // level 0 complete. + return null; + } + + // Use the Huffman trees to decode the LZ77 encoded data. + IMemoryOwner pixelData = this.memoryAllocator.Allocate(decoder.Width * decoder.Height, AllocationOptions.Clean); + this.DecodeImageData(decoder, pixelData.GetSpan()); + + return pixelData; + } + + private void DecodePixelValues(Vp8LDecoder decoder, Buffer2D pixels) + where TPixel : unmanaged, IPixel + { + Span pixelData = decoder.Pixels.GetSpan(); + int width = decoder.Width; + + // Apply reverse transformations, if any are present. + this.ApplyInverseTransforms(decoder, pixelData); + + Span pixelDataAsBytes = MemoryMarshal.Cast(pixelData); + for (int y = 0; y < decoder.Height; y++) + { + Span row = pixelDataAsBytes.Slice(y * width * 4, width * 4); + Span pixelSpan = pixels.GetRowSpan(y); + PixelOperations.Instance.FromBgra32Bytes( + this.configuration, + row, + pixelSpan, + width); + } + } + + private void DecodeImageData(Vp8LDecoder decoder, Span pixelData) + { + int lastPixel = 0; + int width = decoder.Width; + int height = decoder.Height; + int row = lastPixel / width; + int col = lastPixel % width; + int lenCodeLimit = WebPConstants.NumLiteralCodes + WebPConstants.NumLengthCodes; + int colorCacheSize = decoder.Metadata.ColorCacheSize; + ColorCache colorCache = decoder.Metadata.ColorCache; + int colorCacheLimit = lenCodeLimit + colorCacheSize; + int mask = decoder.Metadata.HuffmanMask; + HTreeGroup[] hTreeGroup = this.GetHTreeGroupForPos(decoder.Metadata, col, row); + + int totalPixels = width * height; + int decodedPixels = 0; + int lastCached = decodedPixels; + while (decodedPixels < totalPixels) + { + int code; + if ((col & mask) is 0) + { + hTreeGroup = this.GetHTreeGroupForPos(decoder.Metadata, col, row); + } + + if (hTreeGroup[0].IsTrivialCode) + { + pixelData[decodedPixels] = hTreeGroup[0].LiteralArb; + this.AdvanceByOne(ref col, ref row, width, colorCache, ref decodedPixels, pixelData, ref lastCached); + continue; + } + + this.bitReader.FillBitWindow(); + if (hTreeGroup[0].UsePackedTable) + { + code = (int)this.ReadPackedSymbols(hTreeGroup, pixelData, decodedPixels); + if (this.bitReader.IsEndOfStream()) + { + break; + } + + if (code == PackedNonLiteralCode) + { + this.AdvanceByOne(ref col, ref row, width, colorCache, ref decodedPixels, pixelData, ref lastCached); + continue; + } + } + else + { + code = (int)this.ReadSymbol(hTreeGroup[0].HTrees[HuffIndex.Green]); + } + + if (this.bitReader.IsEndOfStream()) + { + break; + } + + // Literal + if (code < WebPConstants.NumLiteralCodes) + { + if (hTreeGroup[0].IsTrivialLiteral) + { + pixelData[decodedPixels] = hTreeGroup[0].LiteralArb | ((uint)code << 8); + } + else + { + uint red = this.ReadSymbol(hTreeGroup[0].HTrees[HuffIndex.Red]); + this.bitReader.FillBitWindow(); + uint blue = this.ReadSymbol(hTreeGroup[0].HTrees[HuffIndex.Blue]); + uint alpha = this.ReadSymbol(hTreeGroup[0].HTrees[HuffIndex.Alpha]); + if (this.bitReader.IsEndOfStream()) + { + break; + } + + int pixelIdx = decodedPixels; + pixelData[pixelIdx] = (uint)(((byte)alpha << 24) | ((byte)red << 16) | ((byte)code << 8) | (byte)blue); + } + + this.AdvanceByOne(ref col, ref row, width, colorCache, ref decodedPixels, pixelData, ref lastCached); + } + else if (code < lenCodeLimit) + { + // Backward reference is used. + int lengthSym = code - WebPConstants.NumLiteralCodes; + int length = this.GetCopyLength(lengthSym); + uint distSymbol = this.ReadSymbol(hTreeGroup[0].HTrees[HuffIndex.Dist]); + this.bitReader.FillBitWindow(); + int distCode = this.GetCopyDistance((int)distSymbol); + int dist = this.PlaneCodeToDistance(width, distCode); + if (this.bitReader.IsEndOfStream()) + { + break; + } + + this.CopyBlock(pixelData, decodedPixels, dist, length); + decodedPixels += length; + col += length; + while (col >= width) + { + col -= width; + ++row; + } + + if ((col & mask) != 0) + { + hTreeGroup = this.GetHTreeGroupForPos(decoder.Metadata, col, row); + } + + if (colorCache != null) + { + while (lastCached < decodedPixels) + { + colorCache.Insert(pixelData[lastCached]); + lastCached++; + } + } + } + else if (code < colorCacheLimit) + { + // Color cache should be used. + int key = code - lenCodeLimit; + while (lastCached < decodedPixels) + { + colorCache.Insert(pixelData[lastCached]); + lastCached++; + } + + pixelData[decodedPixels] = colorCache.Lookup(key); + this.AdvanceByOne(ref col, ref row, width, colorCache, ref decodedPixels, pixelData, ref lastCached); + } + else + { + WebPThrowHelper.ThrowImageFormatException("Webp parsing error"); + } + } + } + + private void AdvanceByOne(ref int col, ref int row, int width, ColorCache colorCache, ref int decodedPixels, Span pixelData, ref int lastCached) + { + ++col; + decodedPixels++; + if (col >= width) + { + col = 0; + ++row; + + if (colorCache != null) + { + while (lastCached < decodedPixels) + { + colorCache.Insert(pixelData[lastCached]); + lastCached++; + } + } + } + } + + private void ReadHuffmanCodes(Vp8LDecoder decoder, int xSize, int ySize, int colorCacheBits, bool allowRecursion) + { + int maxAlphabetSize = 0; + int numHTreeGroups = 1; + int numHTreeGroupsMax = 1; + + // If the next bit is zero, there is only one meta Huffman code used everywhere in the image. No more data is stored. + // If this bit is one, the image uses multiple meta Huffman codes. These meta Huffman codes are stored as an entropy image. + if (allowRecursion && this.bitReader.ReadBit()) + { + // Use meta Huffman codes. + uint huffmanPrecision = this.bitReader.ReadValue(3) + 2; + int huffmanXSize = LosslessUtils.SubSampleSize(xSize, (int)huffmanPrecision); + int huffmanYSize = LosslessUtils.SubSampleSize(ySize, (int)huffmanPrecision); + int huffmanPixels = huffmanXSize * huffmanYSize; + IMemoryOwner huffmanImage = this.DecodeImageStream(decoder, huffmanXSize, huffmanYSize, false); + Span huffmanImageSpan = huffmanImage.GetSpan(); + decoder.Metadata.HuffmanSubSampleBits = (int)huffmanPrecision; + for (int i = 0; i < huffmanPixels; ++i) + { + // The huffman data is stored in red and green bytes. + uint group = (huffmanImageSpan[i] >> 8) & 0xffff; + huffmanImageSpan[i] = group; + if (group >= numHTreeGroupsMax) + { + numHTreeGroupsMax = (int)group + 1; + } + } + + numHTreeGroups = numHTreeGroupsMax; + decoder.Metadata.HuffmanImage = huffmanImage; + } + + // Find maximum alphabet size for the hTree group. + for (int j = 0; j < WebPConstants.HuffmanCodesPerMetaCode; j++) + { + int alphabetSize = WebPConstants.AlphabetSize[j]; + if (j == 0 && colorCacheBits > 0) + { + alphabetSize += 1 << colorCacheBits; + } + + if (maxAlphabetSize < alphabetSize) + { + maxAlphabetSize = alphabetSize; + } + } + + int tableSize = TableSize[colorCacheBits]; + var huffmanTables = new HuffmanCode[numHTreeGroups * tableSize]; + var hTreeGroups = new HTreeGroup[numHTreeGroups]; + Span huffmanTable = huffmanTables.AsSpan(); + for (int i = 0; i < numHTreeGroupsMax; i++) + { + hTreeGroups[i] = new HTreeGroup(HuffmanUtils.HuffmanPackedTableSize); + HTreeGroup hTreeGroup = hTreeGroups[i]; + int totalSize = 0; + bool isTrivialLiteral = true; + int maxBits = 0; + var codeLengths = new int[maxAlphabetSize]; + for (int j = 0; j < WebPConstants.HuffmanCodesPerMetaCode; j++) + { + int alphabetSize = WebPConstants.AlphabetSize[j]; + if (j == 0 && colorCacheBits > 0) + { + alphabetSize += 1 << colorCacheBits; + } + + int size = this.ReadHuffmanCode(alphabetSize, codeLengths, huffmanTable); + if (size is 0) + { + WebPThrowHelper.ThrowImageFormatException("Huffman table size is zero"); + } + + hTreeGroup.HTrees.Add(huffmanTable.ToArray()); + + if (isTrivialLiteral && LiteralMap[j] == 1) + { + isTrivialLiteral = huffmanTable[0].BitsUsed == 0; + } + + totalSize += huffmanTable[0].BitsUsed; + huffmanTable = huffmanTable.Slice(size); + + if (j <= HuffIndex.Alpha) + { + int localMaxBits = codeLengths[0]; + int k; + for (k = 1; k < alphabetSize; ++k) + { + if (codeLengths[k] > localMaxBits) + { + localMaxBits = codeLengths[k]; + } + } + + maxBits += localMaxBits; + } + } + + hTreeGroup.IsTrivialLiteral = isTrivialLiteral; + hTreeGroup.IsTrivialCode = false; + if (isTrivialLiteral) + { + uint red = hTreeGroup.HTrees[HuffIndex.Red].First().Value; + uint blue = hTreeGroup.HTrees[HuffIndex.Blue].First().Value; + uint green = hTreeGroup.HTrees[HuffIndex.Green].First().Value; + uint alpha = hTreeGroup.HTrees[HuffIndex.Alpha].First().Value; + hTreeGroup.LiteralArb = (alpha << 24) | (red << 16) | blue; + if (totalSize == 0 && green < WebPConstants.NumLiteralCodes) + { + hTreeGroup.IsTrivialCode = true; + hTreeGroup.LiteralArb |= green << 8; + } + } + + hTreeGroup.UsePackedTable = !hTreeGroup.IsTrivialCode && maxBits < HuffmanUtils.HuffmanPackedBits; + if (hTreeGroup.UsePackedTable) + { + this.BuildPackedTable(hTreeGroup); + } + } + + decoder.Metadata.NumHTreeGroups = numHTreeGroups; + decoder.Metadata.HTreeGroups = hTreeGroups; + decoder.Metadata.HuffmanTables = huffmanTables; + } + + private int ReadHuffmanCode(int alphabetSize, int[] codeLengths, Span table) + { + bool simpleCode = this.bitReader.ReadBit(); + for (int i = 0; i < alphabetSize; i++) + { + codeLengths[i] = 0; + } + + if (simpleCode) + { + // (i) Simple Code Length Code. + // This variant is used in the special case when only 1 or 2 Huffman code lengths are non - zero, + // and are in the range of[0, 255].All other Huffman code lengths are implicitly zeros. + + // Read symbols, codes & code lengths directly. + uint numSymbols = this.bitReader.ReadValue(1) + 1; + uint firstSymbolLenCode = this.bitReader.ReadValue(1); + + // The first code is either 1 bit or 8 bit code. + uint symbol = this.bitReader.ReadValue((firstSymbolLenCode is 0) ? 1 : 8); + codeLengths[symbol] = 1; + + // The second code (if present), is always 8 bit long. + if (numSymbols is 2) + { + symbol = this.bitReader.ReadValue(8); + codeLengths[symbol] = 1; + } + } + else + { + // (ii) Normal Code Length Code: + // The code lengths of a Huffman code are read as follows: num_code_lengths specifies the number of code lengths; + // the rest of the code lengths (according to the order in kCodeLengthCodeOrder) are zeros. + var codeLengthCodeLengths = new int[NumCodeLengthCodes]; + uint numCodes = this.bitReader.ReadValue(4) + 4; + if (numCodes > NumCodeLengthCodes) + { + WebPThrowHelper.ThrowImageFormatException("Bitstream error, numCodes has an invalid value"); + } + + for (int i = 0; i < numCodes; i++) + { + codeLengthCodeLengths[CodeLengthCodeOrder[i]] = (int)this.bitReader.ReadValue(3); + } + + this.ReadHuffmanCodeLengths(table.ToArray(), codeLengthCodeLengths, alphabetSize, codeLengths); + } + + int size = HuffmanUtils.BuildHuffmanTable(table, HuffmanUtils.HuffmanTableBits, codeLengths, alphabetSize); + + return size; + } + + private void ReadHuffmanCodeLengths(HuffmanCode[] table, int[] codeLengthCodeLengths, int numSymbols, int[] codeLengths) + { + int maxSymbol; + int symbol = 0; + int prevCodeLen = WebPConstants.DefaultCodeLength; + int size = HuffmanUtils.BuildHuffmanTable(table, WebPConstants.LengthTableBits, codeLengthCodeLengths, NumCodeLengthCodes); + if (size is 0) + { + WebPThrowHelper.ThrowImageFormatException("Error building huffman table"); + } + + if (this.bitReader.ReadBit()) + { + int lengthNBits = 2 + (2 * (int)this.bitReader.ReadValue(3)); + maxSymbol = 2 + (int)this.bitReader.ReadValue(lengthNBits); + } + else + { + maxSymbol = numSymbols; + } + + while (symbol < numSymbols) + { + if (maxSymbol-- is 0) + { + break; + } + + this.bitReader.FillBitWindow(); + ulong prefetchBits = this.bitReader.PrefetchBits(); + ulong idx = prefetchBits & 127; + HuffmanCode huffmanCode = table[idx]; + this.bitReader.AdvanceBitPosition(huffmanCode.BitsUsed); + uint codeLen = huffmanCode.Value; + if (codeLen < WebPConstants.CodeLengthLiterals) + { + codeLengths[symbol++] = (int)codeLen; + if (codeLen != 0) + { + prevCodeLen = (int)codeLen; + } + } + else + { + bool usePrev = codeLen == WebPConstants.CodeLengthRepeatCode; + uint slot = codeLen - WebPConstants.CodeLengthLiterals; + int extraBits = WebPConstants.CodeLengthExtraBits[slot]; + int repeatOffset = WebPConstants.CodeLengthRepeatOffsets[slot]; + int repeat = (int)(this.bitReader.ReadValue(extraBits) + repeatOffset); + if (symbol + repeat > numSymbols) + { + // TODO: not sure, if this should be treated as an error here + return; + } + + int length = usePrev ? prevCodeLen : 0; + while (repeat-- > 0) + { + codeLengths[symbol++] = length; + } + } + } + } + + /// + /// Reads the transformations, if any are present. + /// + /// The width of the image. + /// The height of the image. + /// Vp8LDecoder where the transformations will be stored. + private void ReadTransformation(int xSize, int ySize, Vp8LDecoder decoder) + { + var transformType = (Vp8LTransformType)this.bitReader.ReadValue(2); + var transform = new Vp8LTransform(transformType, xSize, ySize); + + // Each transform is allowed to be used only once. + if (decoder.Transforms.Any(t => t.TransformType == transform.TransformType)) + { + WebPThrowHelper.ThrowImageFormatException("Each transform can only be present once"); + } + + switch (transformType) + { + case Vp8LTransformType.SubtractGreen: + // There is no data associated with this transform. + break; + case Vp8LTransformType.ColorIndexingTransform: + // The transform data contains color table size and the entries in the color table. + // 8 bit value for color table size. + uint numColors = this.bitReader.ReadValue(8) + 1; + int bits = (numColors > 16) ? 0 + : (numColors > 4) ? 1 + : (numColors > 2) ? 2 + : 3; + transform.Bits = bits; + using (IMemoryOwner colorMap = this.DecodeImageStream(decoder, (int)numColors, 1, false)) + { + int finalNumColors = 1 << (8 >> transform.Bits); + IMemoryOwner newColorMap = this.memoryAllocator.Allocate(finalNumColors, AllocationOptions.Clean); + LosslessUtils.ExpandColorMap((int)numColors, colorMap.GetSpan(), newColorMap.GetSpan()); + transform.Data = newColorMap; + } + + break; + + case Vp8LTransformType.PredictorTransform: + case Vp8LTransformType.CrossColorTransform: + { + // The first 3 bits of prediction data define the block width and height in number of bits. + transform.Bits = (int)this.bitReader.ReadValue(3) + 2; + int blockWidth = LosslessUtils.SubSampleSize(transform.XSize, transform.Bits); + int blockHeight = LosslessUtils.SubSampleSize(transform.YSize, transform.Bits); + IMemoryOwner transformData = this.DecodeImageStream(decoder, blockWidth, blockHeight, false); + transform.Data = transformData; + break; + } + } + + decoder.Transforms.Add(transform); + } + + /// + /// A WebP lossless image can go through four different types of transformation before being entropy encoded. + /// This will reverses the transformations, if any are present. + /// + /// The decoder holding the transformation infos. + /// The pixel data to apply the transformation. + private void ApplyInverseTransforms(Vp8LDecoder decoder, Span pixelData) + { + List transforms = decoder.Transforms; + for (int i = transforms.Count - 1; i >= 0; i--) + { + Vp8LTransformType transformType = transforms[i].TransformType; + switch (transformType) + { + case Vp8LTransformType.PredictorTransform: + using (IMemoryOwner output = this.memoryAllocator.Allocate(pixelData.Length, AllocationOptions.Clean)) + { + LosslessUtils.PredictorInverseTransform(transforms[i], pixelData, output.GetSpan()); + } + + break; + case Vp8LTransformType.SubtractGreen: + LosslessUtils.AddGreenToBlueAndRed(pixelData); + break; + case Vp8LTransformType.CrossColorTransform: + LosslessUtils.ColorSpaceInverseTransform(transforms[i], pixelData); + break; + case Vp8LTransformType.ColorIndexingTransform: + LosslessUtils.ColorIndexInverseTransform(transforms[i], pixelData); + break; + } + } + } + + public void DecodeAlphaData(AlphaDecoder dec) + { + Span pixelData = dec.Vp8LDec.Pixels.Memory.Span; + Span data = MemoryMarshal.Cast(pixelData); + int row = 0; + int col = 0; + Vp8LDecoder vp8LDec = dec.Vp8LDec; + int width = vp8LDec.Width; + int height = vp8LDec.Height; + Vp8LMetadata hdr = vp8LDec.Metadata; + int pos = 0; // Current position. + int end = width * height; // End of data. + int last = end; // Last pixel to decode. + int lastRow = height; + int lenCodeLimit = WebPConstants.NumLiteralCodes + WebPConstants.NumLengthCodes; + int mask = hdr.HuffmanMask; + HTreeGroup[] htreeGroup = (pos < last) ? this.GetHTreeGroupForPos(hdr, col, row) : null; + while (!this.bitReader.Eos && pos < last) + { + // Only update when changing tile. + if ((col & mask) is 0) + { + htreeGroup = this.GetHTreeGroupForPos(hdr, col, row); + } + + this.bitReader.FillBitWindow(); + int code = (int)this.ReadSymbol(htreeGroup[0].HTrees[HuffIndex.Green]); + if (code < WebPConstants.NumLiteralCodes) + { + // Literal + data[pos] = (byte)code; + ++pos; + ++col; + + if (col >= width) + { + col = 0; + ++row; + if (row <= lastRow && (row % NumArgbCacheRows is 0)) + { + this.ExtractPalettedAlphaRows(dec, 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 = this.PlaneCodeToDistance(width, distCode); + if (pos >= dist && end - pos >= length) + { + data.Slice(pos - dist, length).CopyTo(data.Slice(pos)); + } + else + { + WebPThrowHelper.ThrowImageFormatException("error while decoding alpha data"); + } + + pos += length; + col += length; + while (col >= width) + { + col -= width; + ++row; + if (row <= lastRow && (row % NumArgbCacheRows is 0)) + { + this.ExtractPalettedAlphaRows(dec, row); + } + } + + if (pos < last && (col & mask) > 0) + { + htreeGroup = this.GetHTreeGroupForPos(hdr, col, row); + } + } + else + { + WebPThrowHelper.ThrowImageFormatException("bitstream error while parsing alpha data"); + } + + this.bitReader.Eos = this.bitReader.IsEndOfStream(); + } + + // Process the remaining rows corresponding to last row-block. + this.ExtractPalettedAlphaRows(dec, row > lastRow ? lastRow : row); + } + + private void ExtractPalettedAlphaRows(AlphaDecoder dec, int lastRow) + { + // For vertical and gradient filtering, we need to decode the part above the + // cropTop row, in order to have the correct spatial predictors. + int topRow = (dec.AlphaFilterType is WebPAlphaFilterType.None || dec.AlphaFilterType is WebPAlphaFilterType.Horizontal) + ? dec.CropTop + : dec.LastRow; + int firstRow = (dec.LastRow < topRow) ? topRow : dec.LastRow; + if (lastRow > firstRow) + { + // Special method for paletted alpha data. We only process the cropped area. + Span output = dec.Alpha.Memory.Span; + Span pixelData = dec.Vp8LDec.Pixels.Memory.Span; + Span pixelDataAsBytes = MemoryMarshal.Cast(pixelData); + Span dst = output.Slice(dec.Width * firstRow); + Span input = pixelDataAsBytes.Slice(dec.Vp8LDec.Width * firstRow); + + // TODO: check if any and the correct transform is present + Vp8LTransform transform = dec.Vp8LDec.Transforms[0]; + this.ColorIndexInverseTransformAlpha(transform, firstRow, lastRow, input, dst); + dec.AlphaApplyFilter(firstRow, lastRow, dst, dec.Width); + } + + dec.LastRow = lastRow; + } + + private void ColorIndexInverseTransformAlpha( + Vp8LTransform transform, + int yStart, + int yEnd, + Span src, + Span dst) + { + int bitsPerPixel = 8 >> transform.Bits; + int width = transform.XSize; + Span colorMap = transform.Data.Memory.Span; + int srcOffset = 0; + int dstOffset = 0; + if (bitsPerPixel < 8) + { + int pixelsPerByte = 1 << transform.Bits; + int countMask = pixelsPerByte - 1; + int bitMask = (1 << bitsPerPixel) - 1; + for (int y = yStart; y < yEnd; ++y) + { + int packedPixels = 0; + for (int x = 0; x < width; ++x) + { + if ((x & countMask) is 0) + { + packedPixels = src[srcOffset]; + srcOffset++; + } + + dst[dstOffset] = GetAlphaValue((int)colorMap[packedPixels & bitMask]); + dstOffset++; + packedPixels >>= bitsPerPixel; + } + } + } + else + { + MapAlpha(src, colorMap, dst, yStart, yEnd, width); + } + } + + private static void MapAlpha(Span src, Span colorMap, Span dst, int yStart, int yEnd, int width) + { + int offset = 0; + for (int y = yStart; y < yEnd; ++y) + { + for (int x = 0; x < width; ++x) + { + dst[offset] = GetAlphaValue((int)colorMap[src[offset]]); + offset++; + } + } + } + + private void UpdateDecoder(Vp8LDecoder decoder, int width, int height) + { + int numBits = decoder.Metadata.HuffmanSubSampleBits; + decoder.Width = width; + decoder.Height = height; + decoder.Metadata.HuffmanXSize = LosslessUtils.SubSampleSize(width, numBits); + decoder.Metadata.HuffmanMask = (numBits is 0) ? ~0 : (1 << numBits) - 1; + } + + private uint ReadPackedSymbols(HTreeGroup[] group, Span pixelData, int decodedPixels) + { + uint val = (uint)(this.bitReader.PrefetchBits() & (HuffmanUtils.HuffmanPackedTableSize - 1)); + HuffmanCode code = group[0].PackedTable[val]; + if (code.BitsUsed < BitsSpecialMarker) + { + this.bitReader.AdvanceBitPosition(code.BitsUsed); + pixelData[decodedPixels] = code.Value; + return PackedNonLiteralCode; + } + + this.bitReader.AdvanceBitPosition(code.BitsUsed - BitsSpecialMarker); + + return code.Value; + } + + private void CopyBlock(Span pixelData, int decodedPixels, int dist, int length) + { + if (dist >= length) + { + Span src = pixelData.Slice(decodedPixels - dist, length); + Span dest = pixelData.Slice(decodedPixels); + src.CopyTo(dest); + } + else + { + Span src = pixelData.Slice(decodedPixels - dist); + Span dest = pixelData.Slice(decodedPixels); + for (int i = 0; i < length; ++i) + { + dest[i] = src[i]; + } + } + } + + private void BuildPackedTable(HTreeGroup hTreeGroup) + { + for (uint code = 0; code < HuffmanUtils.HuffmanPackedTableSize; ++code) + { + uint bits = code; + HuffmanCode huff = hTreeGroup.PackedTable[bits]; + HuffmanCode hCode = hTreeGroup.HTrees[HuffIndex.Green][bits]; + if (hCode.Value >= WebPConstants.NumLiteralCodes) + { + huff.BitsUsed = hCode.BitsUsed + BitsSpecialMarker; + huff.Value = hCode.Value; + } + else + { + huff.BitsUsed = 0; + huff.Value = 0; + bits >>= this.AccumulateHCode(hCode, 8, huff); + bits >>= this.AccumulateHCode(hTreeGroup.HTrees[HuffIndex.Red][bits], 16, huff); + bits >>= this.AccumulateHCode(hTreeGroup.HTrees[HuffIndex.Blue][bits], 0, huff); + bits >>= this.AccumulateHCode(hTreeGroup.HTrees[HuffIndex.Alpha][bits], 24, huff); + } + } + } + + /// + /// Decodes the next Huffman code from the bit-stream. + /// FillBitWindow(br) needs to be called at minimum every second call to ReadSymbol, in order to pre-fetch enough bits. + /// + private uint ReadSymbol(Span table) + { + uint val = (uint)this.bitReader.PrefetchBits(); + Span tableSpan = table.Slice((int)(val & HuffmanUtils.HuffmanTableMask)); + int nBits = tableSpan[0].BitsUsed - HuffmanUtils.HuffmanTableBits; + if (nBits > 0) + { + this.bitReader.AdvanceBitPosition(HuffmanUtils.HuffmanTableBits); + val = (uint)this.bitReader.PrefetchBits(); + tableSpan = tableSpan.Slice((int)tableSpan[0].Value); + tableSpan = tableSpan.Slice((int)val & ((1 << nBits) - 1)); + } + + this.bitReader.AdvanceBitPosition(tableSpan[0].BitsUsed); + + return tableSpan[0].Value; + } + + private HTreeGroup[] GetHTreeGroupForPos(Vp8LMetadata metadata, int x, int y) + { + uint metaIndex = this.GetMetaIndex(metadata.HuffmanImage, metadata.HuffmanXSize, metadata.HuffmanSubSampleBits, x, y); + return metadata.HTreeGroups.AsSpan((int)metaIndex).ToArray(); + } + + private uint GetMetaIndex(IMemoryOwner huffmanImage, int xSize, int bits, int x, int y) + { + if (bits is 0) + { + return 0; + } + + Span huffmanImageSpan = huffmanImage.GetSpan(); + return huffmanImageSpan[(xSize * (y >> bits)) + (x >> bits)]; + } + + private int PlaneCodeToDistance(int xSize, int planeCode) + { + if (planeCode > CodeToPlaneCodes) + { + return planeCode - CodeToPlaneCodes; + } + + int distCode = WebPLookupTables.CodeToPlane[planeCode - 1]; + int yOffset = distCode >> 4; + int xOffset = 8 - (distCode & 0xf); + int dist = (yOffset * xSize) + xOffset; + + // dist < 1 can happen if xSize is very small. + return (dist >= 1) ? dist : 1; + } + + private int GetCopyDistance(int distanceSymbol) + { + if (distanceSymbol < 4) + { + return distanceSymbol + 1; + } + + int extraBits = (distanceSymbol - 2) >> 1; + int offset = (2 + (distanceSymbol & 1)) << extraBits; + + return (int)(offset + this.bitReader.ReadValue(extraBits) + 1); + } + + private int GetCopyLength(int lengthSymbol) + { + // Length and distance prefixes are encoded the same way. + return this.GetCopyDistance(lengthSymbol); + } + + private int AccumulateHCode(HuffmanCode hCode, int shift, HuffmanCode huff) + { + huff.BitsUsed += hCode.BitsUsed; + huff.Value |= hCode.Value << shift; + return hCode.BitsUsed; + } + + private static byte GetAlphaValue(int val) + { + return (byte)((val >> 8) & 0xff); + } + } +} diff --git a/src/ImageSharp/Formats/WebP/WebPLossyDecoder.cs b/src/ImageSharp/Formats/WebP/WebPLossyDecoder.cs new file mode 100644 index 0000000000..c24e60812b --- /dev/null +++ b/src/ImageSharp/Formats/WebP/WebPLossyDecoder.cs @@ -0,0 +1,1413 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Linq; +using System.Runtime.InteropServices; + +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.WebP +{ + internal sealed class WebPLossyDecoder + { + /// + /// A bit reader for reading lossy webp streams. + /// + private readonly Vp8BitReader bitReader; + + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The global configuration. + /// + private readonly Configuration configuration; + + /// + /// Initializes a new instance of the class. + /// + /// Bitreader to read from the stream. + /// Used for allocating memory during processing operations. + /// The configuration. + public WebPLossyDecoder(Vp8BitReader bitReader, MemoryAllocator memoryAllocator, Configuration configuration) + { + this.bitReader = bitReader; + this.memoryAllocator = memoryAllocator; + this.configuration = configuration; + } + + public void Decode(Buffer2D pixels, int width, int height, WebPImageInfo info) + where TPixel : unmanaged, IPixel + { + // Paragraph 9.2: color space and clamp type follow. + sbyte colorSpace = (sbyte)this.bitReader.ReadValue(1); + sbyte clampType = (sbyte)this.bitReader.ReadValue(1); + var pictureHeader = new Vp8PictureHeader() + { + Width = (uint)width, + Height = (uint)height, + XScale = info.XScale, + YScale = info.YScale, + ColorSpace = colorSpace, + ClampType = clampType + }; + + // Paragraph 9.3: Parse the segment header. + var proba = new Vp8Proba(); + Vp8SegmentHeader vp8SegmentHeader = this.ParseSegmentHeader(proba); + + using (var decoder = new Vp8Decoder(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.5: Parse partitions. + this.ParsePartitions(decoder); + + // Paragraph 9.6: Dequantization Indices. + this.ParseDequantizationIndices(decoder); + + // Ignore the value of update probabilities. + this.bitReader.ReadBool(); + + // Paragraph 13.4: Parse probabilities. + this.ParseProbabilities(decoder); + + // Decode image data. + this.ParseFrame(decoder, io); + + if (info.Features?.Alpha is true) + { + using (var alphaDecoder = new AlphaDecoder( + width, + height, + info.Features.AlphaData, + info.Features.AlphaChunkHeader, + this.memoryAllocator, + this.configuration)) + { + alphaDecoder.Decode(); + this.DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels, alphaDecoder.Alpha); + } + } + else + { + this.DecodePixelValues(width, height, decoder.Pixels.Memory.Span, pixels); + } + } + } + + private void DecodePixelValues(int width, int height, Span pixelData, Buffer2D pixels, IMemoryOwner alpha = null) + where TPixel : unmanaged, IPixel + { + if (alpha != null) + { + TPixel color = default; + Span alphaSpan = alpha.Memory.Span; + for (int y = 0; y < height; y++) + { + Span pixelRow = pixels.GetRowSpan(y); + for (int x = 0; x < width; x++) + { + int offset = (y * width) + x; + int idxBgr = offset * 3; + byte b = pixelData[idxBgr]; + byte g = pixelData[idxBgr + 1]; + byte r = pixelData[idxBgr + 2]; + byte a = alphaSpan[offset]; + color.FromBgra32(new Bgra32(r, g, b, a)); + pixelRow[x] = color; + } + } + + return; + } + + for (int y = 0; y < height; y++) + { + Span row = pixelData.Slice(y * width * 3, width * 3); + Span pixelSpan = pixels.GetRowSpan(y); + PixelOperations.Instance.FromBgr24Bytes( + this.configuration, + row, + pixelSpan, + width); + } + } + + private void ParseFrame(Vp8Decoder dec, Vp8Io io) + { + for (dec.MbY = 0; dec.MbY < dec.BottomRightMbY; ++dec.MbY) + { + // Parse bitstream for this row. + long bitreaderIdx = dec.MbY & dec.NumPartsMinusOne; + Vp8BitReader bitreader = dec.Vp8BitReaders[bitreaderIdx]; + + // Parse intra mode mode row. + for (int mbX = 0; mbX < dec.MbWidth; ++mbX) + { + this.ParseIntraMode(dec, mbX); + } + + for (; dec.MbX < dec.MbWidth; ++dec.MbX) + { + this.DecodeMacroBlock(dec, bitreader); + } + + // Prepare for next scanline. + this.InitScanline(dec); + + // Reconstruct, filter and emit the row. + this.ProcessRow(dec, io); + } + } + + private void ParseIntraMode(Vp8Decoder dec, int mbX) + { + Vp8MacroBlockData block = dec.MacroBlockData[mbX]; + Span top = dec.IntraT.AsSpan(4 * mbX, 4); + byte[] left = dec.IntraL; + + if (dec.SegmentHeader.UpdateMap) + { + // Hardcoded tree parsing. + block.Segment = this.bitReader.GetBit((int)dec.Probabilities.Segments[0]) is 0 + ? (byte)this.bitReader.GetBit((int)dec.Probabilities.Segments[1]) + : (byte)(this.bitReader.GetBit((int)dec.Probabilities.Segments[2]) + 2); + } + else + { + // default for intra + block.Segment = 0; + } + + if (dec.UseSkipProbability) + { + block.Skip = (byte)this.bitReader.GetBit(dec.SkipProbability); + } + + block.IsI4x4 = this.bitReader.GetBit(145) is 0; + if (!block.IsI4x4) + { + // Hardcoded 16x16 intra-mode decision tree. + int yMode = this.bitReader.GetBit(156) != 0 ? + this.bitReader.GetBit(128) != 0 ? WebPConstants.TmPred : WebPConstants.HPred : + this.bitReader.GetBit(163) != 0 ? WebPConstants.VPred : WebPConstants.DcPred; + block.Modes[0] = (byte)yMode; + for (int i = 0; i < left.Length; i++) + { + left[i] = (byte)yMode; + top[i] = (byte)yMode; + } + } + else + { + Span modes = block.Modes.AsSpan(); + for (int y = 0; y < 4; ++y) + { + int yMode = left[y]; + for (int x = 0; x < 4; ++x) + { + byte[] prob = WebPLookupTables.ModesProba[top[x], yMode]; + int i = WebPConstants.YModesIntra4[this.bitReader.GetBit(prob[0])]; + while (i > 0) + { + i = WebPConstants.YModesIntra4[(2 * i) + this.bitReader.GetBit(prob[i])]; + } + + yMode = -i; + top[x] = (byte)yMode; + } + + top.CopyTo(modes); + modes = modes.Slice(4); + left[y] = (byte)yMode; + } + } + + // Hardcoded UVMode decision tree. + block.UvMode = (byte)(this.bitReader.GetBit(142) is 0 ? 0 : + this.bitReader.GetBit(114) is 0 ? 2 : + this.bitReader.GetBit(183) != 0 ? 1 : 3); + } + + private void InitScanline(Vp8Decoder dec) + { + Vp8MacroBlock left = dec.LeftMacroBlock; + left.NoneZeroAcDcCoeffs = 0; + left.NoneZeroDcCoeffs = 0; + for (int i = 0; i < dec.IntraL.Length; i++) + { + dec.IntraL[i] = 0; + } + + dec.MbX = 0; + } + + private void ProcessRow(Vp8Decoder dec, Vp8Io io) + { + this.ReconstructRow(dec); + this.FinishRow(dec, io); + } + + private void ReconstructRow(Vp8Decoder dec) + { + int mby = dec.MbY; + int yOff = (WebPConstants.Bps * 1) + 8; + int uOff = yOff + (WebPConstants.Bps * 16) + WebPConstants.Bps; + int vOff = uOff + 16; + + Span yuv = dec.YuvBuffer.Memory.Span; + Span yDst = yuv.Slice(yOff); + Span uDst = yuv.Slice(uOff); + Span vDst = yuv.Slice(vOff); + + // Initialize left-most block. + for (int i = 0; i < 16; ++i) + { + yuv[(i * WebPConstants.Bps) - 1 + yOff] = 129; + } + + for (int i = 0; i < 8; ++i) + { + yuv[(i * WebPConstants.Bps) - 1 + uOff] = 129; + yuv[(i * WebPConstants.Bps) - 1 + vOff] = 129; + } + + // Init top-left sample on left column too. + if (mby > 0) + { + yuv[yOff - 1 - WebPConstants.Bps] = yuv[uOff - 1 - WebPConstants.Bps] = yuv[vOff - 1 - WebPConstants.Bps] = 129; + } + else + { + // We only need to do this init once at block (0,0). + // Afterward, it remains valid for the whole topmost row. + Span tmp = yuv.Slice(yOff - WebPConstants.Bps - 1, 16 + 4 + 1); + for (int i = 0; i < tmp.Length; ++i) + { + tmp[i] = 127; + } + + tmp = yuv.Slice(uOff - WebPConstants.Bps - 1, 8 + 1); + for (int i = 0; i < tmp.Length; ++i) + { + tmp[i] = 127; + } + + tmp = yuv.Slice(vOff - WebPConstants.Bps - 1, 8 + 1); + for (int i = 0; i < tmp.Length; ++i) + { + tmp[i] = 127; + } + } + + // Reconstruct one row. + for (int mbx = 0; mbx < dec.MbWidth; ++mbx) + { + Vp8MacroBlockData block = dec.MacroBlockData[mbx]; + + // Rotate in the left samples from previously decoded block. We move four + // pixels at a time for alignment reason, and because of in-loop filter. + if (mbx > 0) + { + for (int i = -1; i < 16; ++i) + { + int srcIdx = (i * WebPConstants.Bps) + 12 + yOff; + int dstIdx = (i * WebPConstants.Bps) - 4 + yOff; + yuv.Slice(srcIdx, 4).CopyTo(yuv.Slice(dstIdx)); + } + + for (int i = -1; i < 8; ++i) + { + int srcIdx = (i * WebPConstants.Bps) + 4 + uOff; + int dstIdx = (i * WebPConstants.Bps) - 4 + uOff; + yuv.Slice(srcIdx, 4).CopyTo(yuv.Slice(dstIdx)); + srcIdx = (i * WebPConstants.Bps) + 4 + vOff; + dstIdx = (i * WebPConstants.Bps) - 4 + vOff; + yuv.Slice(srcIdx, 4).CopyTo(yuv.Slice(dstIdx)); + } + } + + // Bring top samples into the cache. + Vp8TopSamples topYuv = dec.YuvTopSamples[mbx]; + short[] coeffs = block.Coeffs; + uint bits = block.NonZeroY; + if (mby > 0) + { + topYuv.Y.CopyTo(yuv.Slice(yOff - WebPConstants.Bps)); + topYuv.U.CopyTo(yuv.Slice(uOff - WebPConstants.Bps)); + topYuv.V.CopyTo(yuv.Slice(vOff - WebPConstants.Bps)); + } + + // Predict and add residuals. + if (block.IsI4x4) + { + Span topRight = yuv.Slice(yOff - WebPConstants.Bps + 16); + if (mby > 0) + { + if (mbx >= dec.MbWidth - 1) + { + // On rightmost border. + topRight[0] = topYuv.Y[15]; + topRight[1] = topYuv.Y[15]; + topRight[2] = topYuv.Y[15]; + topRight[3] = topYuv.Y[15]; + } + else + { + dec.YuvTopSamples[mbx + 1].Y.AsSpan(0, 4).CopyTo(topRight); + } + } + + // Replicate the top-right pixels below. + Span topRightUint = MemoryMarshal.Cast(yuv.Slice(yOff - WebPConstants.Bps + 16)); + topRightUint[WebPConstants.Bps] = topRightUint[2 * WebPConstants.Bps] = topRightUint[3 * WebPConstants.Bps] = topRightUint[0]; + + // Predict and add residuals for all 4x4 blocks in turn. + for (int n = 0; n < 16; ++n, bits <<= 2) + { + int offset = yOff + WebPConstants.Scan[n]; + Span dst = yuv.Slice(offset); + byte lumaMode = block.Modes[n]; + switch (lumaMode) + { + case 0: + LossyUtils.DC4(dst, yuv, offset); + break; + case 1: + LossyUtils.TM4(dst, yuv, offset); + break; + case 2: + LossyUtils.VE4(dst, yuv, offset); + break; + case 3: + LossyUtils.HE4(dst, yuv, offset); + break; + case 4: + LossyUtils.RD4(dst, yuv, offset); + break; + case 5: + LossyUtils.VR4(dst, yuv, offset); + break; + case 6: + LossyUtils.LD4(dst, yuv, offset); + break; + case 7: + LossyUtils.VL4(dst, yuv, offset); + break; + case 8: + LossyUtils.HD4(dst, yuv, offset); + break; + case 9: + LossyUtils.HU4(dst, yuv, offset); + break; + } + + this.DoTransform(bits, coeffs.AsSpan(n * 16), dst); + } + } + else + { + // 16x16 + int mode = CheckMode(mbx, mby, block.Modes[0]); + switch (mode) + { + case 0: + LossyUtils.DC16(yDst, yuv, yOff); + break; + case 1: + LossyUtils.TM16(yDst, yuv, yOff); + break; + case 2: + LossyUtils.VE16(yDst, yuv, yOff); + break; + case 3: + LossyUtils.HE16(yDst, yuv, yOff); + break; + case 4: + LossyUtils.DC16NoTop(yDst, yuv, yOff); + break; + case 5: + LossyUtils.DC16NoLeft(yDst, yuv, yOff); + break; + case 6: + LossyUtils.DC16NoTopLeft(yDst); + break; + } + + if (bits != 0) + { + for (int n = 0; n < 16; ++n, bits <<= 2) + { + this.DoTransform(bits, coeffs.AsSpan(n * 16), yDst.Slice(WebPConstants.Scan[n])); + } + } + } + + // Chroma + uint bitsUv = block.NonZeroUv; + int chromaMode = CheckMode(mbx, mby, block.UvMode); + switch (chromaMode) + { + case 0: + LossyUtils.DC8uv(uDst, yuv, uOff); + LossyUtils.DC8uv(vDst, yuv, vOff); + break; + case 1: + LossyUtils.TM8uv(uDst, yuv, uOff); + LossyUtils.TM8uv(vDst, yuv, vOff); + break; + case 2: + LossyUtils.VE8uv(uDst, yuv, uOff); + LossyUtils.VE8uv(vDst, yuv, vOff); + break; + case 3: + LossyUtils.HE8uv(uDst, yuv, uOff); + LossyUtils.HE8uv(vDst, yuv, vOff); + break; + case 4: + LossyUtils.DC8uvNoTop(uDst, yuv, uOff); + LossyUtils.DC8uvNoTop(vDst, yuv, vOff); + break; + case 5: + LossyUtils.DC8uvNoLeft(uDst, yuv, uOff); + LossyUtils.DC8uvNoLeft(vDst, yuv, vOff); + break; + case 6: + LossyUtils.DC8uvNoTopLeft(uDst); + LossyUtils.DC8uvNoTopLeft(vDst); + break; + } + + this.DoUVTransform(bitsUv >> 0, coeffs.AsSpan(16 * 16), uDst); + this.DoUVTransform(bitsUv >> 8, coeffs.AsSpan(20 * 16), vDst); + + // Stash away top samples for next block. + if (mby < dec.MbHeight - 1) + { + yDst.Slice(15 * WebPConstants.Bps, 16).CopyTo(topYuv.Y); + uDst.Slice(7 * WebPConstants.Bps, 8).CopyTo(topYuv.U); + vDst.Slice(7 * WebPConstants.Bps, 8).CopyTo(topYuv.V); + } + + // Transfer reconstructed samples from yuv_buffer cache to final destination. + Span yOut = dec.CacheY.Memory.Span.Slice(dec.CacheYOffset + (mbx * 16)); + Span uOut = dec.CacheU.Memory.Span.Slice(dec.CacheUvOffset + (mbx * 8)); + Span vOut = dec.CacheV.Memory.Span.Slice(dec.CacheUvOffset + (mbx * 8)); + for (int j = 0; j < 16; ++j) + { + yDst.Slice(j * WebPConstants.Bps, Math.Min(16, yOut.Length)).CopyTo(yOut.Slice(j * dec.CacheYStride)); + } + + for (int j = 0; j < 8; ++j) + { + uDst.Slice(j * WebPConstants.Bps, Math.Min(8, uOut.Length)).CopyTo(uOut.Slice(j * dec.CacheUvStride)); + vDst.Slice(j * WebPConstants.Bps, Math.Min(8, vOut.Length)).CopyTo(vOut.Slice(j * dec.CacheUvStride)); + } + } + } + + private void FilterRow(Vp8Decoder dec) + { + int mby = dec.MbY; + for (int mbx = dec.TopLeftMbX; mbx < dec.BottomRightMbX; ++mbx) + { + this.DoFilter(dec, mbx, mby); + } + } + + private void DoFilter(Vp8Decoder dec, int mbx, int mby) + { + int yBps = dec.CacheYStride; + Vp8FilterInfo filterInfo = dec.FilterInfo[mbx]; + int iLevel = filterInfo.InnerLevel; + int limit = filterInfo.Limit; + + if (limit is 0) + { + return; + } + + if (dec.Filter is LoopFilter.Simple) + { + int offset = dec.CacheYOffset + (mbx * 16); + if (mbx > 0) + { + LossyUtils.SimpleHFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4); + } + + if (filterInfo.UseInnerFiltering > 0) + { + LossyUtils.SimpleHFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit); + } + + if (mby > 0) + { + LossyUtils.SimpleVFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4); + } + + if (filterInfo.UseInnerFiltering > 0) + { + LossyUtils.SimpleVFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit); + } + } + else if (dec.Filter is 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 > 0) + { + 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 > 0) + { + 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); + } + } + } + + private void FinishRow(Vp8Decoder dec, Vp8Io io) + { + int extraYRows = WebPConstants.FilterExtraRows[(int)dec.Filter]; + int ySize = extraYRows * dec.CacheYStride; + int uvSize = (extraYRows / 2) * dec.CacheUvStride; + Span yDst = dec.CacheY.Memory.Span; + Span uDst = dec.CacheU.Memory.Span; + Span vDst = dec.CacheV.Memory.Span; + int mby = dec.MbY; + bool isFirstRow = mby is 0; + bool isLastRow = mby >= dec.BottomRightMbY - 1; + bool filterRow = (dec.Filter != LoopFilter.None) && + (dec.MbY >= dec.TopLeftMbY) && (dec.MbY <= dec.BottomRightMbY); + + if (filterRow) + { + this.FilterRow(dec); + } + + int yStart = mby * 16; + int yEnd = (mby + 1) * 16; + if (!isFirstRow) + { + yStart -= extraYRows; + io.Y = yDst; + io.U = uDst; + io.V = vDst; + } + else + { + io.Y = dec.CacheY.Memory.Span.Slice(dec.CacheYOffset); + io.U = dec.CacheU.Memory.Span.Slice(dec.CacheUvOffset); + io.V = dec.CacheV.Memory.Span.Slice(dec.CacheUvOffset); + } + + if (!isLastRow) + { + yEnd -= extraYRows; + } + + if (yEnd > io.CropBottom) + { + yEnd = io.CropBottom; // make sure we don't overflow on last row. + } + + if (yStart < yEnd) + { + io.Y = io.Y.Slice(io.CropLeft); + io.U = io.U.Slice(io.CropLeft); + io.V = io.V.Slice(io.CropLeft); + + io.MbY = yStart - io.CropTop; + io.MbW = io.CropRight - io.CropLeft; + io.MbH = yEnd - yStart; + this.EmitRgb(dec, io); + } + + // Rotate top samples if needed. + if (!isLastRow) + { + yDst.Slice(16 * dec.CacheYStride, ySize).CopyTo(dec.CacheY.Memory.Span); + uDst.Slice(8 * dec.CacheUvStride, uvSize).CopyTo(dec.CacheU.Memory.Span); + vDst.Slice(8 * dec.CacheUvStride, uvSize).CopyTo(dec.CacheV.Memory.Span); + } + } + + private int EmitRgb(Vp8Decoder dec, Vp8Io io) + { + Span buf = dec.Pixels.Memory.Span; + int numLinesOut = io.MbH; // a priori guess. + Span curY = io.Y; + Span curU = io.U; + Span curV = io.V; + Span tmpYBuffer = dec.TmpYBuffer.Memory.Span; + Span tmpUBuffer = dec.TmpUBuffer.Memory.Span; + Span tmpVBuffer = dec.TmpVBuffer.Memory.Span; + Span topU = tmpUBuffer; + Span topV = tmpVBuffer; + int bpp = 3; + int bufferStride = bpp * io.Width; + int dstStartIdx = io.MbY * bufferStride; + Span dst = buf.Slice(dstStartIdx); + int yEnd = io.MbY + io.MbH; + int mbw = io.MbW; + int uvw = (mbw + 1) / 2; + int y = io.MbY; + + if (y is 0) + { + // First line is special cased. We mirror the u/v samples at boundary. + this.UpSample(curY, null, curU, curV, curU, curV, dst, null, mbw); + } + else + { + // We can finish the left-over line from previous call. + this.UpSample(tmpYBuffer, curY, topU, topV, curU, curV, buf.Slice(dstStartIdx - bufferStride), dst, mbw); + numLinesOut++; + } + + // Loop over each output pairs of row. + for (; y + 2 < yEnd; y += 2) + { + topU = curU; + topV = curV; + curU = curU.Slice(io.UvStride); + curV = curV.Slice(io.UvStride); + this.UpSample(curY.Slice(io.YStride), curY.Slice(2 * io.YStride), topU, topV, curU, curV, dst.Slice(bufferStride), dst.Slice(2 * bufferStride), mbw); + curY = curY.Slice(2 * io.YStride); + dst = dst.Slice(2 * bufferStride); + } + + // Move to last row. + curY = curY.Slice(io.YStride); + if (io.CropTop + yEnd < io.CropBottom) + { + // Save the unfinished samples for next call (as we're not done yet). + curY.Slice(0, mbw).CopyTo(tmpYBuffer); + curU.Slice(0, uvw).CopyTo(tmpUBuffer); + curV.Slice(0, uvw).CopyTo(tmpVBuffer); + + // The upsampler leaves a row unfinished behind (except for the very last row). + numLinesOut--; + } + else + { + // Process the very last row of even-sized picture. + if ((yEnd & 1) is 0) + { + this.UpSample(curY, null, curU, curV, curU, curV, dst.Slice(bufferStride), null, mbw); + } + } + + return numLinesOut; + } + + private void UpSample(Span topY, Span bottomY, Span topU, Span topV, Span curU, Span curV, Span topDst, Span bottomDst, int len) + { + int xStep = 3; + int lastPixelPair = (len - 1) >> 1; + uint tluv = LossyUtils.LoadUv(topU[0], topV[0]); // top-left sample + uint luv = LossyUtils.LoadUv(curU[0], curV[0]); // left-sample + uint uv0 = ((3 * tluv) + luv + 0x00020002u) >> 2; + LossyUtils.YuvToBgr(topY[0], (int)(uv0 & 0xff), (int)(uv0 >> 16), topDst); + + if (bottomY != null) + { + uv0 = ((3 * luv) + tluv + 0x00020002u) >> 2; + LossyUtils.YuvToBgr(bottomY[0], (int)uv0 & 0xff, (int)(uv0 >> 16), bottomDst); + } + + for (int x = 1; x <= lastPixelPair; ++x) + { + uint tuv = LossyUtils.LoadUv(topU[x], topV[x]); // top sample + uint uv = LossyUtils.LoadUv(curU[x], curV[x]); // sample + + // Precompute invariant values associated with first and second diagonals. + uint avg = tluv + tuv + luv + uv + 0x00080008u; + uint diag12 = (avg + (2 * (tuv + luv))) >> 3; + uint diag03 = (avg + (2 * (tluv + uv))) >> 3; + uv0 = (diag12 + tluv) >> 1; + uint uv1 = (diag03 + tuv) >> 1; + LossyUtils.YuvToBgr(topY[(2 * x) - 1], (int)(uv0 & 0xff), (int)(uv0 >> 16), topDst.Slice(((2 * x) - 1) * xStep)); + LossyUtils.YuvToBgr(topY[(2 * x) - 0], (int)(uv1 & 0xff), (int)(uv1 >> 16), topDst.Slice(((2 * x) - 0) * xStep)); + + if (bottomY != null) + { + uv0 = (diag03 + luv) >> 1; + uv1 = (diag12 + uv) >> 1; + LossyUtils.YuvToBgr(bottomY[(2 * x) - 1], (int)(uv0 & 0xff), (int)(uv0 >> 16), bottomDst.Slice(((2 * x) - 1) * xStep)); + LossyUtils.YuvToBgr(bottomY[(2 * x) + 0], (int)(uv1 & 0xff), (int)(uv1 >> 16), bottomDst.Slice(((2 * x) + 0) * xStep)); + } + + tluv = tuv; + luv = uv; + } + + if ((len & 1) is 0) + { + uv0 = ((3 * tluv) + luv + 0x00020002u) >> 2; + LossyUtils.YuvToBgr(topY[len - 1], (int)(uv0 & 0xff), (int)(uv0 >> 16), topDst.Slice((len - 1) * xStep)); + if (bottomY != null) + { + uv0 = ((3 * luv) + tluv + 0x00020002u) >> 2; + LossyUtils.YuvToBgr(bottomY[len - 1], (int)(uv0 & 0xff), (int)(uv0 >> 16), bottomDst.Slice((len - 1) * xStep)); + } + } + } + + private void DoTransform(uint bits, Span src, Span dst) + { + switch (bits >> 30) + { + case 3: + LossyUtils.Transform(src, dst, false); + break; + case 2: + LossyUtils.TransformAc3(src, dst); + break; + case 1: + LossyUtils.TransformDc(src, dst); + break; + default: + break; + } + } + + private void DoUVTransform(uint bits, Span src, Span dst) + { + // any non-zero coeff at all? + if ((bits & 0xff) > 0) + { + // any non-zero AC coefficient? + if ((bits & 0xaa) > 0) + { + LossyUtils.TransformUv(src, dst); // note we don't use the AC3 variant for U/V. + } + else + { + LossyUtils.TransformDcuv(src, dst); + } + } + } + + private void DecodeMacroBlock(Vp8Decoder dec, Vp8BitReader bitreader) + { + Vp8MacroBlock left = dec.LeftMacroBlock; + Vp8MacroBlock macroBlock = dec.CurrentMacroBlock; + Vp8MacroBlockData blockData = dec.CurrentBlockData; + int skip = dec.UseSkipProbability ? blockData.Skip : 0; + + if (skip is 0) + { + this.ParseResiduals(dec, bitreader, macroBlock); + } + else + { + left.NoneZeroAcDcCoeffs = macroBlock.NoneZeroAcDcCoeffs = 0; + if (!blockData.IsI4x4) + { + left.NoneZeroDcCoeffs = macroBlock.NoneZeroDcCoeffs = 0; + } + + blockData.NonZeroY = 0; + blockData.NonZeroUv = 0; + } + + // Store filter info. + if (dec.Filter != LoopFilter.None) + { + Vp8FilterInfo precomputedFilterInfo = dec.FilterStrength[blockData.Segment, blockData.IsI4x4 ? 1 : 0]; + dec.FilterInfo[dec.MbX] = (Vp8FilterInfo)precomputedFilterInfo.DeepClone(); + dec.FilterInfo[dec.MbX].UseInnerFiltering |= (byte)(skip is 0 ? 1 : 0); + } + } + + private bool ParseResiduals(Vp8Decoder dec, Vp8BitReader br, Vp8MacroBlock mb) + { + uint nonZeroY = 0; + uint nonZeroUv = 0; + int first; + int dstOffset = 0; + Vp8MacroBlockData block = dec.CurrentBlockData; + Vp8QuantMatrix q = dec.DeQuantMatrices[block.Segment]; + Vp8BandProbas[,] bands = dec.Probabilities.BandsPtr; + Vp8BandProbas[] acProba; + Vp8MacroBlock leftMb = dec.LeftMacroBlock; + short[] dst = block.Coeffs; + for (int i = 0; i < dst.Length; i++) + { + dst[i] = 0; + } + + if (!block.IsI4x4) + { + // Parse DC + var dc = new short[16]; + int ctx = (int)(mb.NoneZeroDcCoeffs + leftMb.NoneZeroDcCoeffs); + int nz = this.GetCoeffs(br, GetBandsRow(bands, 1), ctx, q.Y2Mat, 0, dc); + mb.NoneZeroDcCoeffs = leftMb.NoneZeroDcCoeffs = (uint)(nz > 0 ? 1 : 0); + if (nz > 1) + { + // More than just the DC -> perform the full transform. + this.TransformWht(dc, dst); + } + else + { + // Only DC is non-zero -> inlined simplified transform. + int dc0 = (dc[0] + 3) >> 3; + for (int i = 0; i < 16 * 16; i += 16) + { + dst[i] = (short)dc0; + } + } + + first = 1; + acProba = GetBandsRow(bands, 0); + } + else + { + first = 0; + acProba = GetBandsRow(bands, 3); + } + + byte tnz = (byte)(mb.NoneZeroAcDcCoeffs & 0x0f); + byte lnz = (byte)(leftMb.NoneZeroAcDcCoeffs & 0x0f); + + for (int y = 0; y < 4; ++y) + { + int l = lnz & 1; + uint nzCoeffs = 0; + for (int x = 0; x < 4; ++x) + { + int ctx = l + (tnz & 1); + int nz = this.GetCoeffs(br, acProba, ctx, q.Y1Mat, first, dst.AsSpan(dstOffset)); + l = (nz > first) ? 1 : 0; + tnz = (byte)((tnz >> 1) | (l << 7)); + nzCoeffs = NzCodeBits(nzCoeffs, nz, dst[dstOffset] != 0 ? 1 : 0); + dstOffset += 16; + } + + tnz >>= 4; + lnz = (byte)((lnz >> 1) | (l << 7)); + nonZeroY = (nonZeroY << 8) | nzCoeffs; + } + + uint outTnz = tnz; + uint outLnz = (uint)(lnz >> 4); + + for (int ch = 0; ch < 4; ch += 2) + { + uint nzCoeffs = 0; + tnz = (byte)(mb.NoneZeroAcDcCoeffs >> (4 + ch)); + lnz = (byte)(leftMb.NoneZeroAcDcCoeffs >> (4 + ch)); + for (int y = 0; y < 2; ++y) + { + int l = lnz & 1; + for (int x = 0; x < 2; ++x) + { + int ctx = l + (tnz & 1); + int nz = this.GetCoeffs(br, GetBandsRow(bands, 2), ctx, q.UvMat, 0, dst.AsSpan(dstOffset)); + l = (nz > 0) ? 1 : 0; + tnz = (byte)((tnz >> 1) | (l << 3)); + nzCoeffs = NzCodeBits(nzCoeffs, nz, dst[dstOffset] != 0 ? 1 : 0); + dstOffset += 16; + } + + tnz >>= 2; + lnz = (byte)((lnz >> 1) | (l << 5)); + } + + // Note: we don't really need the per-4x4 details for U/V blocks. + nonZeroUv |= nzCoeffs << (4 * ch); + outTnz |= (uint)((tnz << 4) << ch); + outLnz |= (uint)((lnz & 0xf0) << ch); + } + + mb.NoneZeroAcDcCoeffs = outTnz; + leftMb.NoneZeroAcDcCoeffs = outLnz; + + block.NonZeroY = nonZeroY; + block.NonZeroUv = nonZeroUv; + + return (nonZeroY | nonZeroUv) is 0; + } + + private int GetCoeffs(Vp8BitReader br, Vp8BandProbas[] prob, int ctx, int[] dq, int n, Span coeffs) + { + // Returns the position of the last non - zero coeff plus one. + Vp8ProbaArray p = prob[n].Probabilities[ctx]; + for (; n < 16; ++n) + { + if (br.GetBit(p.Probabilities[0]) is 0) + { + // Previous coeff was last non - zero coeff. + return n; + } + + // Sequence of zero coeffs. + while (br.GetBit(p.Probabilities[1]) is 0) + { + p = prob[++n].Probabilities[0]; + if (n is 16) + { + return 16; + } + } + + // Non zero coeffs. + int v; + if (br.GetBit(p.Probabilities[2]) is 0) + { + v = 1; + p = prob[n + 1].Probabilities[1]; + } + else + { + v = this.GetLargeValue(br, p.Probabilities); + p = prob[n + 1].Probabilities[2]; + } + + int idx = n > 0 ? 1 : 0; + coeffs[WebPConstants.Zigzag[n]] = (short)(br.GetSigned(v) * dq[idx]); + } + + return 16; + } + + private int GetLargeValue(Vp8BitReader br, byte[] p) + { + // See section 13 - 2: http://tools.ietf.org/html/rfc6386#section-13.2 + int v; + if (br.GetBit(p[3]) is 0) + { + if (br.GetBit(p[4]) is 0) + { + v = 2; + } + else + { + v = 3 + br.GetBit(p[5]); + } + } + else + { + if (br.GetBit(p[6]) is 0) + { + if (br.GetBit(p[7]) is 0) + { + v = 5 + br.GetBit(159); + } + else + { + v = 7 + (2 * br.GetBit(165)); + v += br.GetBit(145); + } + } + else + { + int bit1 = br.GetBit(p[8]); + int bit0 = br.GetBit(p[9 + bit1]); + int cat = (2 * bit1) + bit0; + v = 0; + byte[] tab = null; + switch (cat) + { + case 0: + tab = WebPConstants.Cat3; + break; + case 1: + tab = WebPConstants.Cat4; + break; + case 2: + tab = WebPConstants.Cat5; + break; + case 3: + tab = WebPConstants.Cat6; + break; + default: + WebPThrowHelper.ThrowImageFormatException("VP8 parsing error"); + break; + } + + for (int i = 0; i < tab.Length; i++) + { + v += v + br.GetBit(tab[i]); + } + + v += 3 + (8 << cat); + } + } + + return v; + } + + /// + /// Paragraph 14.3: Implementation of the Walsh-Hadamard transform inversion. + /// + private void TransformWht(short[] input, short[] output) + { + var tmp = new int[16]; + for (int i = 0; i < 4; ++i) + { + int a0 = input[0 + i] + input[12 + i]; + int a1 = input[4 + i] + input[8 + i]; + int a2 = input[4 + i] - input[8 + i]; + int a3 = input[0 + i] - input[12 + i]; + tmp[0 + i] = a0 + a1; + tmp[8 + i] = a0 - a1; + tmp[4 + i] = a3 + a2; + tmp[12 + i] = a3 - a2; + } + + int outputOffset = 0; + for (int i = 0; i < 4; ++i) + { + int dc = tmp[0 + (i * 4)] + 3; + int a0 = dc + tmp[3 + (i * 4)]; + int a1 = tmp[1 + (i * 4)] + tmp[2 + (i * 4)]; + int a2 = tmp[1 + (i * 4)] - tmp[2 + (i * 4)]; + int a3 = dc - tmp[3 + (i * 4)]; + output[outputOffset + 0] = (short)((a0 + a1) >> 3); + output[outputOffset + 16] = (short)((a3 + a2) >> 3); + output[outputOffset + 32] = (short)((a0 - a1) >> 3); + output[outputOffset + 48] = (short)((a3 - a2) >> 3); + outputOffset += 64; + } + } + + private Vp8SegmentHeader ParseSegmentHeader(Vp8Proba proba) + { + var vp8SegmentHeader = new Vp8SegmentHeader + { + UseSegment = this.bitReader.ReadBool() + }; + if (vp8SegmentHeader.UseSegment) + { + vp8SegmentHeader.UpdateMap = this.bitReader.ReadBool(); + bool updateData = this.bitReader.ReadBool(); + if (updateData) + { + vp8SegmentHeader.Delta = this.bitReader.ReadBool(); + bool hasValue; + for (int i = 0; i < vp8SegmentHeader.Quantizer.Length; i++) + { + hasValue = this.bitReader.ReadBool(); + int quantizeValue = hasValue ? this.bitReader.ReadSignedValue(7) : 0; + vp8SegmentHeader.Quantizer[i] = (byte)quantizeValue; + } + + for (int i = 0; i < vp8SegmentHeader.FilterStrength.Length; i++) + { + hasValue = this.bitReader.ReadBool(); + int filterStrengthValue = hasValue ? this.bitReader.ReadSignedValue(6) : 0; + vp8SegmentHeader.FilterStrength[i] = (byte)filterStrengthValue; + } + + if (vp8SegmentHeader.UpdateMap) + { + for (int s = 0; s < proba.Segments.Length; ++s) + { + hasValue = this.bitReader.ReadBool(); + proba.Segments[s] = hasValue ? this.bitReader.ReadValue(8) : 255; + } + } + } + } + else + { + vp8SegmentHeader.UpdateMap = false; + } + + return vp8SegmentHeader; + } + + private void ParseFilterHeader(Vp8Decoder dec) + { + Vp8FilterHeader vp8FilterHeader = dec.FilterHeader; + vp8FilterHeader.LoopFilter = this.bitReader.ReadBool() ? LoopFilter.Simple : LoopFilter.Complex; + vp8FilterHeader.Level = (int)this.bitReader.ReadValue(6); + vp8FilterHeader.Sharpness = (int)this.bitReader.ReadValue(3); + vp8FilterHeader.UseLfDelta = this.bitReader.ReadBool(); + + dec.Filter = (vp8FilterHeader.Level is 0) ? LoopFilter.None : vp8FilterHeader.LoopFilter; + if (vp8FilterHeader.UseLfDelta) + { + // Update lf-delta? + if (this.bitReader.ReadBool()) + { + bool hasValue; + for (int i = 0; i < vp8FilterHeader.RefLfDelta.Length; i++) + { + hasValue = this.bitReader.ReadBool(); + if (hasValue) + { + vp8FilterHeader.RefLfDelta[i] = this.bitReader.ReadSignedValue(6); + } + } + + for (int i = 0; i < vp8FilterHeader.ModeLfDelta.Length; i++) + { + hasValue = this.bitReader.ReadBool(); + if (hasValue) + { + vp8FilterHeader.ModeLfDelta[i] = this.bitReader.ReadSignedValue(6); + } + } + } + } + + int extraRows = WebPConstants.FilterExtraRows[(int)dec.Filter]; + int extraY = extraRows * dec.CacheYStride; + int extraUv = (extraRows / 2) * dec.CacheUvStride; + dec.CacheYOffset = extraY; + dec.CacheUvOffset = extraUv; + } + + private void ParsePartitions(Vp8Decoder dec) + { + uint size = this.bitReader.Remaining - this.bitReader.PartitionLength; + int startIdx = (int)this.bitReader.PartitionLength; + Span sz = this.bitReader.Data.AsSpan(startIdx); + int sizeLeft = (int)size; + dec.NumPartsMinusOne = (1 << (int)this.bitReader.ReadValue(2)) - 1; + int lastPart = dec.NumPartsMinusOne; + + int partStart = startIdx + (lastPart * 3); + sizeLeft -= lastPart * 3; + for (int p = 0; p < lastPart; ++p) + { + int pSize = sz[0] | (sz[1] << 8) | (sz[2] << 16); + if (pSize > sizeLeft) + { + pSize = sizeLeft; + } + + dec.Vp8BitReaders[p] = new Vp8BitReader(this.bitReader.Data, (uint)pSize, partStart); + partStart += pSize; + sizeLeft -= pSize; + sz = sz.Slice(3); + } + + dec.Vp8BitReaders[lastPart] = new Vp8BitReader(this.bitReader.Data, (uint)sizeLeft, partStart); + } + + private void ParseDequantizationIndices(Vp8Decoder decoder) + { + Vp8SegmentHeader vp8SegmentHeader = decoder.SegmentHeader; + + int baseQ0 = (int)this.bitReader.ReadValue(7); + bool hasValue = this.bitReader.ReadBool(); + int dqy1Dc = hasValue ? this.bitReader.ReadSignedValue(4) : 0; + hasValue = this.bitReader.ReadBool(); + int dqy2Dc = hasValue ? this.bitReader.ReadSignedValue(4) : 0; + hasValue = this.bitReader.ReadBool(); + int dqy2Ac = hasValue ? this.bitReader.ReadSignedValue(4) : 0; + hasValue = this.bitReader.ReadBool(); + int dquvDc = hasValue ? this.bitReader.ReadSignedValue(4) : 0; + hasValue = this.bitReader.ReadBool(); + int dquvAc = hasValue ? this.bitReader.ReadSignedValue(4) : 0; + for (int i = 0; i < WebPConstants.NumMbSegments; ++i) + { + int q; + if (vp8SegmentHeader.UseSegment) + { + q = vp8SegmentHeader.Quantizer[i]; + if (!vp8SegmentHeader.Delta) + { + q += baseQ0; + } + } + else + { + if (i > 0) + { + decoder.DeQuantMatrices[i] = decoder.DeQuantMatrices[0]; + continue; + } + else + { + q = baseQ0; + } + } + + Vp8QuantMatrix m = decoder.DeQuantMatrices[i]; + m.Y1Mat[0] = WebPLookupTables.DcTable[Clip(q + dqy1Dc, 127)]; + m.Y1Mat[1] = WebPLookupTables.AcTable[Clip(q + 0, 127)]; + m.Y2Mat[0] = WebPLookupTables.DcTable[Clip(q + dqy2Dc, 127)] * 2; + + // For all x in [0..284], x*155/100 is bitwise equal to (x*101581) >> 16. + // The smallest precision for that is '(x*6349) >> 12' but 16 is a good word size. + m.Y2Mat[1] = (WebPLookupTables.AcTable[Clip(q + dqy2Ac, 127)] * 101581) >> 16; + if (m.Y2Mat[1] < 8) + { + m.Y2Mat[1] = 8; + } + + m.UvMat[0] = WebPLookupTables.DcTable[Clip(q + dquvDc, 117)]; + m.UvMat[1] = WebPLookupTables.AcTable[Clip(q + dquvAc, 127)]; + + // For dithering strength evaluation. + m.UvQuant = q + dquvAc; + } + } + + private void ParseProbabilities(Vp8Decoder dec) + { + Vp8Proba proba = dec.Probabilities; + + for (int t = 0; t < WebPConstants.NumTypes; ++t) + { + for (int b = 0; b < WebPConstants.NumBands; ++b) + { + for (int c = 0; c < WebPConstants.NumCtx; ++c) + { + for (int p = 0; p < WebPConstants.NumProbas; ++p) + { + byte prob = WebPLookupTables.CoeffsUpdateProba[t, b, c, p]; + int v = this.bitReader.GetBit(prob) != 0 + ? (int)this.bitReader.ReadValue(8) + : WebPLookupTables.DefaultCoeffsProba[t, b, c, p]; + proba.Bands[t, b].Probabilities[c].Probabilities[p] = (byte)v; + } + } + } + + for (int b = 0; b < 16 + 1; ++b) + { + proba.BandsPtr[t, b] = proba.Bands[t, WebPConstants.Bands[b]]; + } + } + + dec.UseSkipProbability = this.bitReader.ReadBool(); + if (dec.UseSkipProbability) + { + dec.SkipProbability = (byte)this.bitReader.ReadValue(8); + } + } + + private static Vp8Io InitializeVp8Io(Vp8Decoder dec, Vp8PictureHeader pictureHeader) + { + var io = default(Vp8Io); + io.Width = (int)pictureHeader.Width; + io.Height = (int)pictureHeader.Height; + io.UseCropping = false; + io.CropTop = 0; + io.CropLeft = 0; + io.CropRight = io.Width; + io.CropBottom = io.Height; + io.UseScaling = false; + io.ScaledWidth = io.Width; + io.ScaledHeight = io.ScaledHeight; + io.MbW = io.Width; + io.MbH = io.Height; + io.YStride = (int)(16 * ((pictureHeader.Width + 15) >> 4)); + io.UvStride = (int)(8 * ((pictureHeader.Width + 15) >> 4)); + + int intraPredModeSize = 4 * dec.MbWidth; + dec.IntraT = new byte[intraPredModeSize]; + + int extraPixels = WebPConstants.FilterExtraRows[(int)dec.Filter]; + if (dec.Filter is LoopFilter.Complex) + { + // For complex filter, we need to preserve the dependency chain. + dec.TopLeftMbX = 0; + dec.TopLeftMbY = 0; + } + else + { + // For simple filter, we can filter only the cropped region. We include 'extraPixels' on + // the other side of the boundary, since vertical or horizontal filtering of the previous + // macroblock can modify some abutting pixels. + dec.TopLeftMbX = (io.CropLeft - extraPixels) >> 4; + dec.TopLeftMbY = (io.CropTop - extraPixels) >> 4; + if (dec.TopLeftMbX < 0) + { + dec.TopLeftMbX = 0; + } + + if (dec.TopLeftMbY < 0) + { + dec.TopLeftMbY = 0; + } + } + + // We need some 'extra' pixels on the right/bottom. + dec.BottomRightMbY = (io.CropBottom + 15 + extraPixels) >> 4; + dec.BottomRightMbX = (io.CropRight + 15 + extraPixels) >> 4; + if (dec.BottomRightMbX > dec.MbWidth) + { + dec.BottomRightMbX = dec.MbWidth; + } + + if (dec.BottomRightMbY > dec.MbHeight) + { + dec.BottomRightMbY = dec.MbHeight; + } + + return io; + } + + private static uint NzCodeBits(uint nzCoeffs, int nz, int dcNz) + { + nzCoeffs <<= 2; + nzCoeffs |= (uint)((nz > 3) ? 3 : (nz > 1) ? 2 : dcNz); + return nzCoeffs; + } + + private static Vp8BandProbas[] GetBandsRow(Vp8BandProbas[,] bands, int rowIdx) + { + Vp8BandProbas[] bandsRow = Enumerable.Range(0, bands.GetLength(1)).Select(x => bands[rowIdx, x]).ToArray(); + return bandsRow; + } + + private static int CheckMode(int mbx, int mby, int mode) + { + // B_DC_PRED + if (mode is 0) + { + if (mbx is 0) + { + return (mby is 0) + ? 6 // B_DC_PRED_NOTOPLEFT + : 5; // B_DC_PRED_NOLEFT + } + + return (mby is 0) + ? 4 // B_DC_PRED_NOTOP + : 0; // B_DC_PRED + } + + return mode; + } + + private static int Clip(int value, int max) + { + return value < 0 ? 0 : value > max ? max : value; + } + } +} diff --git a/src/ImageSharp/Formats/WebP/WebPMetadata.cs b/src/ImageSharp/Formats/WebP/WebPMetadata.cs new file mode 100644 index 0000000000..4788d2b0bd --- /dev/null +++ b/src/ImageSharp/Formats/WebP/WebPMetadata.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Provides WebP specific metadata information for the image. + /// + public class WebPMetadata : IDeepCloneable + { + /// + /// Initializes a new instance of the class. + /// + public WebPMetadata() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private WebPMetadata(WebPMetadata other) + { + this.Animated = other.Animated; + this.Format = other.Format; + } + + /// + /// Gets or sets the webp format used. Either lossless or lossy. + /// + public WebPFormatType Format { get; set; } + + /// + /// Gets or sets all found chunk types ordered by appearance. + /// + public Queue ChunkTypes { get; set; } = new Queue(); + + /// + /// Gets or sets a value indicating whether the webp file contains a animation. + /// + public bool Animated { get; set; } = false; + + /// + public IDeepCloneable DeepClone() => new WebPMetadata(this); + } +} diff --git a/src/ImageSharp/Formats/WebP/WebPThrowHelper.cs b/src/ImageSharp/Formats/WebP/WebPThrowHelper.cs new file mode 100644 index 0000000000..7cc4df2466 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/WebPThrowHelper.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.WebP +{ + internal static class WebPThrowHelper + { + /// + /// Cold path optimization for throwing -s + /// + /// The error message for the exception. + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowImageFormatException(string errorMessage) + { + throw new ImageFormatException(errorMessage); + } + + /// + /// Cold path optimization for throwing -s + /// + /// The error message for the exception. + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowNotSupportedException(string errorMessage) + { + throw new NotSupportedException(errorMessage); + } + } +} diff --git a/src/ImageSharp/Formats/WebP/WebpConfigurationModule.cs b/src/ImageSharp/Formats/WebP/WebpConfigurationModule.cs new file mode 100644 index 0000000000..051b738538 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/WebpConfigurationModule.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP +{ + /// + /// Registers the image encoders, decoders and mime type detectors for the webp format. + /// + public sealed class WebPConfigurationModule : IConfigurationModule + { + /// + public void Configure(Configuration configuration) + { + configuration.ImageFormatsManager.SetDecoder(WebPFormat.Instance, new WebPDecoder()); + configuration.ImageFormatsManager.AddImageFormatDetector(new WebPImageFormatDetector()); + } + } +} diff --git a/src/ImageSharp/Formats/WebP/rfc6386_lossy_specification.pdf b/src/ImageSharp/Formats/WebP/rfc6386_lossy_specification.pdf new file mode 100644 index 0000000000..d421b34cce Binary files /dev/null and b/src/ImageSharp/Formats/WebP/rfc6386_lossy_specification.pdf differ diff --git a/tests/ImageSharp.Benchmarks/Codecs/DecodeWebp.cs b/tests/ImageSharp.Benchmarks/Codecs/DecodeWebp.cs new file mode 100644 index 0000000000..5ec22cbad7 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/DecodeWebp.cs @@ -0,0 +1,87 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using BenchmarkDotNet.Attributes; + +using ImageMagick; + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs +{ + [Config(typeof(Config.ShortClr))] + public class DecodeWebp : BenchmarkBase + { + private byte[] webpLossyBytes; + private byte[] webpLosslessBytes; + + private string TestImageLossyFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImageLossy); + private string TestImageLosslessFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImageLossless); + + [Params(TestImages.WebP.Lossy.Bike)] + public string TestImageLossy { get; set; } + + [Params(TestImages.WebP.Lossless.BikeThreeTransforms)] + public string TestImageLossless { get; set; } + + [GlobalSetup] + public void ReadImages() + { + if (this.webpLossyBytes is null) + { + this.webpLossyBytes = File.ReadAllBytes(this.TestImageLossyFullPath); + } + + if (this.webpLosslessBytes is null) + { + this.webpLosslessBytes = File.ReadAllBytes(this.TestImageLosslessFullPath); + } + } + + [Benchmark(Description = "Magick Lossy WebP")] + public int WebpLossyMagick() + { + var settings = new MagickReadSettings { Format = MagickFormat.WebP }; + using (var image = new MagickImage(new MemoryStream(this.webpLossyBytes), settings)) + { + return image.Width; + } + } + + [Benchmark(Description = "ImageSharp Lossy Webp")] + public int WebpLossy() + { + using (var memoryStream = new MemoryStream(this.webpLossyBytes)) + { + using (var image = Image.Load(memoryStream)) + { + return image.Height; + } + } + } + + [Benchmark(Description = "Magick Lossless WebP")] + public int WebpLosslessMagick() + { + var settings = new MagickReadSettings { Format = MagickFormat.WebP }; + using (var image = new MagickImage(new MemoryStream(this.webpLosslessBytes), settings)) + { + return image.Width; + } + } + + [Benchmark(Description = "ImageSharp Lossless Webp")] + public int WebpLossless() + { + using (var memoryStream = new MemoryStream(this.webpLosslessBytes)) + { + using (var image = Image.Load(memoryStream)) + { + return image.Height; + } + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebPDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebPDecoderTests.cs new file mode 100644 index 0000000000..f2c2d9c2dc --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/WebP/WebPDecoderTests.cs @@ -0,0 +1,318 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; + +using SixLabors.ImageSharp.Formats.WebP; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; + +using Xunit; + +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Tests.Formats.WebP +{ + using static SixLabors.ImageSharp.Tests.TestImages.WebP; + + public class WebPDecoderTests + { + private static WebPDecoder WebpDecoder => new WebPDecoder(); + + private static MagickReferenceDecoder ReferenceDecoder => new MagickReferenceDecoder(); + + [Theory] + [InlineData(Lossless.GreenTransform1, 1000, 307, 32)] + [InlineData(Lossless.BikeThreeTransforms, 250, 195, 32)] + [InlineData(Lossless.NoTransform2, 128, 128, 32)] + [InlineData(Lossy.Alpha1, 1000, 307, 32)] + [InlineData(Lossy.Alpha2, 1000, 307, 32)] + [InlineData(Lossy.Bike, 250, 195, 24)] + public void Identify_DetectsCorrectDimensionsAndBitDepth( + string imagePath, + int expectedWidth, + int expectedHeight, + int expectedBitsPerPixel) + { + var testFile = TestFile.Create(imagePath); + using (var stream = new MemoryStream(testFile.Bytes, false)) + { + IImageInfo imageInfo = Image.Identify(stream); + Assert.NotNull(imageInfo); + Assert.Equal(expectedWidth, imageInfo.Width); + Assert.Equal(expectedHeight, imageInfo.Height); + Assert.Equal(expectedBitsPerPixel, imageInfo.PixelType.BitsPerPixel); + } + } + + [Theory] + [WithFile(Lossy.Bike, PixelTypes.Rgba32)] + [WithFile(Lossy.NoFilter01, PixelTypes.Rgba32)] + [WithFile(Lossy.NoFilter02, PixelTypes.Rgba32)] + [WithFile(Lossy.NoFilter03, PixelTypes.Rgba32)] + [WithFile(Lossy.NoFilter04, PixelTypes.Rgba32)] + [WithFile(Lossy.NoFilter05, PixelTypes.Rgba32)] + [WithFile(Lossy.SegmentationNoFilter01, PixelTypes.Rgba32)] + [WithFile(Lossy.SegmentationNoFilter02, PixelTypes.Rgba32)] + [WithFile(Lossy.SegmentationNoFilter03, PixelTypes.Rgba32)] + public void WebpDecoder_CanDecode_Lossy_WithoutFilter(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(WebpDecoder)) + { + image.DebugSave(provider); + image.CompareToOriginal(provider, ReferenceDecoder); + } + } + + [Theory] + [WithFile(Lossy.SimpleFilter01, PixelTypes.Rgba32)] + [WithFile(Lossy.SimpleFilter02, PixelTypes.Rgba32)] + [WithFile(Lossy.SimpleFilter03, PixelTypes.Rgba32)] + [WithFile(Lossy.SimpleFilter04, PixelTypes.Rgba32)] + [WithFile(Lossy.SimpleFilter05, PixelTypes.Rgba32)] + public void WebpDecoder_CanDecode_Lossy_WithSimpleFilter(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(WebpDecoder)) + { + image.DebugSave(provider); + image.CompareToOriginal(provider, ReferenceDecoder); + } + } + + [Theory] + [WithFile(Lossy.IccpComplexFilter, PixelTypes.Rgba32)] + [WithFile(Lossy.VeryShort, PixelTypes.Rgba32)] + [WithFile(Lossy.BikeComplexFilter, PixelTypes.Rgba32)] + [WithFile(Lossy.ComplexFilter01, PixelTypes.Rgba32)] + [WithFile(Lossy.ComplexFilter02, PixelTypes.Rgba32)] + [WithFile(Lossy.ComplexFilter03, PixelTypes.Rgba32)] + [WithFile(Lossy.ComplexFilter04, PixelTypes.Rgba32)] + [WithFile(Lossy.ComplexFilter05, PixelTypes.Rgba32)] + [WithFile(Lossy.ComplexFilter06, PixelTypes.Rgba32)] + [WithFile(Lossy.ComplexFilter07, PixelTypes.Rgba32)] + [WithFile(Lossy.ComplexFilter08, PixelTypes.Rgba32)] + [WithFile(Lossy.ComplexFilter09, PixelTypes.Rgba32)] + public void WebpDecoder_CanDecode_Lossy_WithComplexFilter(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(WebpDecoder)) + { + image.DebugSave(provider); + image.CompareToOriginal(provider, ReferenceDecoder); + } + } + + [Theory] + [WithFile(Lossy.Small01, PixelTypes.Rgba32)] + [WithFile(Lossy.Small02, PixelTypes.Rgba32)] + [WithFile(Lossy.Small03, PixelTypes.Rgba32)] + [WithFile(Lossy.Small04, PixelTypes.Rgba32)] + public void WebpDecoder_CanDecode_Lossy_VerySmall(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(WebpDecoder)) + { + image.DebugSave(provider); + image.CompareToOriginal(provider, ReferenceDecoder); + } + } + + [Theory] + [WithFile(Lossy.SegmentationNoFilter04, PixelTypes.Rgba32)] + [WithFile(Lossy.SegmentationNoFilter05, PixelTypes.Rgba32)] + [WithFile(Lossy.SegmentationNoFilter06, PixelTypes.Rgba32)] + [WithFile(Lossy.SegmentationComplexFilter01, PixelTypes.Rgba32)] + [WithFile(Lossy.SegmentationComplexFilter02, PixelTypes.Rgba32)] + [WithFile(Lossy.SegmentationComplexFilter03, PixelTypes.Rgba32)] + [WithFile(Lossy.SegmentationComplexFilter04, PixelTypes.Rgba32)] + [WithFile(Lossy.SegmentationComplexFilter05, PixelTypes.Rgba32)] + public void WebpDecoder_CanDecode_Lossy_WithPartitions(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(WebpDecoder)) + { + image.DebugSave(provider); + image.CompareToOriginal(provider, ReferenceDecoder); + } + } + + [Theory] + [WithFile(Lossy.Partitions01, PixelTypes.Rgba32)] + [WithFile(Lossy.Partitions02, PixelTypes.Rgba32)] + [WithFile(Lossy.Partitions03, PixelTypes.Rgba32)] + public void WebpDecoder_CanDecode_Lossy_WithSegmentation(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(WebpDecoder)) + { + image.DebugSave(provider); + image.CompareToOriginal(provider, ReferenceDecoder); + } + } + + [Theory] + [WithFile(Lossy.Sharpness01, PixelTypes.Rgba32)] + [WithFile(Lossy.Sharpness02, PixelTypes.Rgba32)] + [WithFile(Lossy.Sharpness03, PixelTypes.Rgba32)] + [WithFile(Lossy.Sharpness04, PixelTypes.Rgba32)] + [WithFile(Lossy.Sharpness05, PixelTypes.Rgba32)] + [WithFile(Lossy.Sharpness06, PixelTypes.Rgba32)] + public void WebpDecoder_CanDecode_Lossy_WithSharpnessLevel(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(WebpDecoder)) + { + image.DebugSave(provider); + image.CompareToOriginal(provider, ReferenceDecoder); + } + } + + [Theory] + [WithFile(Lossy.AlphaNoCompression, PixelTypes.Rgba32)] + [WithFile(Lossy.AlphaNoCompressionNoFilter, PixelTypes.Rgba32)] + [WithFile(Lossy.AlphaCompressedNoFilter, PixelTypes.Rgba32)] + [WithFile(Lossy.AlphaNoCompressionHorizontalFilter, PixelTypes.Rgba32)] + [WithFile(Lossy.AlphaNoCompressionVerticalFilter, PixelTypes.Rgba32)] + [WithFile(Lossy.AlphaNoCompressionGradientFilter, PixelTypes.Rgba32)] + [WithFile(Lossy.AlphaCompressedHorizontalFilter, PixelTypes.Rgba32)] + [WithFile(Lossy.AlphaCompressedVerticalFilter, PixelTypes.Rgba32)] + [WithFile(Lossy.AlphaCompressedGradientFilter, PixelTypes.Rgba32)] + public void WebpDecoder_CanDecode_Lossy_WithAlpha(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(WebpDecoder)) + { + image.DebugSave(provider); + image.CompareToOriginal(provider, ReferenceDecoder); + } + } + + [Theory] + [WithFile(Lossless.NoTransform1, PixelTypes.Rgba32)] + [WithFile(Lossless.NoTransform2, PixelTypes.Rgba32)] + public void WebpDecoder_CanDecode_Lossless_WithoutTransforms(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(WebpDecoder)) + { + image.DebugSave(provider); + image.CompareToOriginal(provider, ReferenceDecoder); + } + } + + [Theory] + [WithFile(Lossless.GreenTransform1, PixelTypes.Rgba32)] + [WithFile(Lossless.GreenTransform2, PixelTypes.Rgba32)] + [WithFile(Lossless.GreenTransform3, PixelTypes.Rgba32)] + [WithFile(Lossless.GreenTransform4, PixelTypes.Rgba32)] + // TODO: Reference decoder throws here MagickCorruptImageErrorException, webpinfo also indicates an error here, but decoding the image seems to work. + // [WithFile(Lossless.GreenTransform5, PixelTypes.Rgba32)] + public void WebpDecoder_CanDecode_Lossless_WithSubstractGreenTransform( + TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(WebpDecoder)) + { + image.DebugSave(provider); + image.CompareToOriginal(provider, ReferenceDecoder); + } + } + + [Theory] + [WithFile(Lossless.ColorIndexTransform1, PixelTypes.Rgba32)] + [WithFile(Lossless.ColorIndexTransform2, PixelTypes.Rgba32)] + [WithFile(Lossless.ColorIndexTransform3, PixelTypes.Rgba32)] + [WithFile(Lossless.ColorIndexTransform4, PixelTypes.Rgba32)] + [WithFile(Lossless.ColorIndexTransform5, PixelTypes.Rgba32)] + public void WebpDecoder_CanDecode_Lossless_WithColorIndexTransform(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(WebpDecoder)) + { + image.DebugSave(provider); + image.CompareToOriginal(provider, ReferenceDecoder); + } + } + + [Theory] + [WithFile(Lossless.PredictorTransform1, PixelTypes.Rgba32)] + [WithFile(Lossless.PredictorTransform2, PixelTypes.Rgba32)] + public void WebpDecoder_CanDecode_Lossless_WithPredictorTransform(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(WebpDecoder)) + { + image.DebugSave(provider); + image.CompareToOriginal(provider, ReferenceDecoder); + } + } + + [Theory] + [WithFile(Lossless.CrossColorTransform1, PixelTypes.Rgba32)] + [WithFile(Lossless.CrossColorTransform2, PixelTypes.Rgba32)] + public void WebpDecoder_CanDecode_Lossless_WithCrossColorTransform(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(WebpDecoder)) + { + image.DebugSave(provider); + image.CompareToOriginal(provider, ReferenceDecoder); + } + } + + [Theory] + [WithFile(Lossless.TwoTransforms1, PixelTypes.Rgba32)] + [WithFile(Lossless.TwoTransforms2, PixelTypes.Rgba32)] + [WithFile(Lossless.TwoTransforms3, PixelTypes.Rgba32)] + [WithFile(Lossless.TwoTransforms4, PixelTypes.Rgba32)] + [WithFile(Lossless.TwoTransforms5, PixelTypes.Rgba32)] + [WithFile(Lossless.TwoTransforms6, PixelTypes.Rgba32)] + [WithFile(Lossless.TwoTransforms7, PixelTypes.Rgba32)] + [WithFile(Lossless.TwoTransforms8, PixelTypes.Rgba32)] + [WithFile(Lossless.TwoTransforms9, PixelTypes.Rgba32)] + [WithFile(Lossless.TwoTransforms10, PixelTypes.Rgba32)] + [WithFile(Lossless.TwoTransforms11, PixelTypes.Rgba32)] + [WithFile(Lossless.TwoTransforms12, PixelTypes.Rgba32)] + [WithFile(Lossless.TwoTransforms13, PixelTypes.Rgba32)] + public void WebpDecoder_CanDecode_Lossless_WithTwoTransforms(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(WebpDecoder)) + { + image.DebugSave(provider); + image.CompareToOriginal(provider, ReferenceDecoder); + } + } + + [Theory] + [WithFile(Lossless.ThreeTransforms1, PixelTypes.Rgba32)] + [WithFile(Lossless.ThreeTransforms2, PixelTypes.Rgba32)] + [WithFile(Lossless.ThreeTransforms3, PixelTypes.Rgba32)] + [WithFile(Lossless.ThreeTransforms4, PixelTypes.Rgba32)] + [WithFile(Lossless.ThreeTransforms5, PixelTypes.Rgba32)] + [WithFile(Lossless.ThreeTransforms6, PixelTypes.Rgba32)] + [WithFile(Lossless.ThreeTransforms7, PixelTypes.Rgba32)] + [WithFile(Lossless.BikeThreeTransforms, PixelTypes.Rgba32)] + public void WebpDecoder_CanDecode_Lossless_WithThreeTransforms(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(WebpDecoder)) + { + image.DebugSave(provider); + image.CompareToOriginal(provider, ReferenceDecoder); + } + } + + [Theory] + [WithFile(Lossless.LossLessCorruptImage1, PixelTypes.Rgba32)] + [WithFile(Lossless.LossLessCorruptImage2, PixelTypes.Rgba32)] + [WithFile(Lossless.LossLessCorruptImage3, PixelTypes.Rgba32)] + [WithFile(Lossless.LossLessCorruptImage4, PixelTypes.Rgba32)] + public void WebpDecoder_ThrowImageFormatException_OnInvalidImages(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Assert.Throws(() => { using (provider.GetImage(WebpDecoder)) { } }); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebPMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebPMetaDataTests.cs new file mode 100644 index 0000000000..44f46c05a6 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/WebP/WebPMetaDataTests.cs @@ -0,0 +1,57 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats.WebP; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Tests.Formats.WebP +{ + public class WebPMetadataTests + { + [Theory] + [WithFile(TestImages.WebP.Lossless.WithExif, PixelTypes.Rgba32, false)] + [WithFile(TestImages.WebP.Lossy.WithExif, PixelTypes.Rgba32, true)] + public void IgnoreMetadata_ControlsWhetherExifIsParsed(TestImageProvider provider, bool ignoreMetadata) + where TPixel : unmanaged, IPixel + { + var decoder = new WebPDecoder { IgnoreMetadata = ignoreMetadata }; + + using (Image image = provider.GetImage(decoder)) + { + if (ignoreMetadata) + { + Assert.Null(image.Metadata.ExifProfile); + } + else + { + Assert.NotNull(image.Metadata.ExifProfile); + Assert.NotEmpty(image.Metadata.ExifProfile.Values); + } + } + } + + [Theory] + [WithFile(TestImages.WebP.Lossless.WithIccp, PixelTypes.Rgba32, false)] + [WithFile(TestImages.WebP.Lossy.WithIccp, PixelTypes.Rgba32, true)] + public void IgnoreMetadata_ControlsWhetherIccpIsParsed(TestImageProvider provider, bool ignoreMetadata) + where TPixel : unmanaged, IPixel + { + var decoder = new WebPDecoder { IgnoreMetadata = ignoreMetadata }; + + using (Image image = provider.GetImage(decoder)) + { + if (ignoreMetadata) + { + Assert.Null(image.Metadata.IccProfile); + } + else + { + Assert.NotNull(image.Metadata.IccProfile); + Assert.NotEmpty(image.Metadata.IccProfile.Entries); + } + } + } + } +} diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 2e58ac970c..517fe38f1b 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -391,5 +391,147 @@ public static class Tga public const string Bit16Pal = "Tga/targa_16bit_pal.tga"; public const string Bit24Pal = "Tga/targa_24bit_pal.tga"; } + + public static class WebP + { + public static class Animated + { + public const string Animated1 = "WebP/animated-webp.webp"; + public const string Animated2 = "WebP/animated2.webp"; + public const string Animated3 = "WebP/animated3.webp"; + public const string Animated4 = "WebP/animated_lossy.webp"; + } + + public static class Lossless + { + public const string WithExif = "WebP/exif_lossless.webp"; + public const string WithIccp = "WebP/iccp_lossless.webp"; + public const string NoTransform1 = "WebP/lossless_vec_1_0.webp"; + public const string NoTransform2 = "WebP/lossless_vec_2_0.webp"; + public const string GreenTransform1 = "WebP/lossless1.webp"; + public const string GreenTransform2 = "WebP/lossless2.webp"; + public const string GreenTransform3 = "WebP/lossless3.webp"; + public const string GreenTransform4 = "WebP/lossless_vec_1_4.webp"; + public const string GreenTransform5 = "WebP/lossless_vec_2_4.webp"; + public const string CrossColorTransform1 = "WebP/lossless_vec_1_8.webp"; + public const string CrossColorTransform2 = "WebP/lossless_vec_2_8.webp"; + public const string PredictorTransform1 = "WebP/lossless_vec_1_2.webp"; + public const string PredictorTransform2 = "WebP/lossless_vec_2_2.webp"; + public const string ColorIndexTransform1 = "WebP/lossless4.webp"; + public const string ColorIndexTransform2 = "WebP/lossless_vec_1_1.webp"; + public const string ColorIndexTransform3 = "WebP/lossless_vec_1_5.webp"; + public const string ColorIndexTransform4 = "WebP/lossless_vec_2_1.webp"; + public const string ColorIndexTransform5 = "WebP/lossless_vec_2_5.webp"; + public const string TwoTransforms1 = "Webp/lossless_vec_1_10.webp"; // cross_color, predictor + public const string TwoTransforms2 = "Webp/lossless_vec_1_12.webp"; // cross_color, substract_green + public const string TwoTransforms3 = "Webp/lossless_vec_1_13.webp"; // color_indexing, cross_color + public const string TwoTransforms4 = "Webp/lossless_vec_1_3.webp"; // color_indexing, predictor + public const string TwoTransforms5 = "Webp/lossless_vec_1_6.webp"; // substract_green, predictor + public const string TwoTransforms6 = "Webp/lossless_vec_1_7.webp"; // color_indexing, predictor + public const string TwoTransforms7 = "Webp/lossless_vec_1_9.webp"; // color_indexing, cross_color + public const string TwoTransforms8 = "Webp/lossless_vec_2_10.webp"; // predictor, cross_color + public const string TwoTransforms9 = "Webp/lossless_vec_2_12.webp"; // substract_green, cross_color + public const string TwoTransforms10 = "Webp/lossless_vec_2_13.webp"; // color_indexing, cross_color + public const string TwoTransforms11 = "Webp/lossless_vec_2_3.webp"; // color_indexing, predictor + public const string TwoTransforms12 = "Webp/lossless_vec_2_6.webp"; // substract_green, predictor + public const string TwoTransforms13 = "Webp/lossless_vec_2_9.webp"; // color_indexing, predictor + public const string ThreeTransforms1 = "Webp/color_cache_bits_11.webp"; // substract_green, predictor, cross_color + public const string ThreeTransforms2 = "Webp/lossless_vec_1_11.webp"; // color_indexing, predictor, cross_color + public const string ThreeTransforms3 = "Webp/lossless_vec_1_14.webp"; // substract_green, predictor, cross_color + public const string ThreeTransforms4 = "Webp/lossless_vec_1_15.webp"; // color_indexing, predictor, cross_color + public const string ThreeTransforms5 = "Webp/lossless_vec_2_11.webp"; // color_indexing, predictor, cross_color + public const string ThreeTransforms6 = "Webp/lossless_vec_2_14.webp"; // substract_green, predictor, cross_color + public const string ThreeTransforms7 = "Webp/lossless_vec_2_15.webp"; // color_indexing, predictor, cross_color + public const string BikeThreeTransforms = "Webp/bike_lossless.webp"; // substract_green, predictor, cross_color + + // Invalid / corrupted images + // Below images have errors according to webpinfo. The error message webpinfo gives is "Truncated data detected when parsing RIFF payload." + public const string LossLessCorruptImage1 = "Webp/lossless_big_random_alpha.webp"; // substract_green, predictor, cross_color. + public const string LossLessCorruptImage2 = "Webp/lossless_vec_2_7.webp"; // color_indexing, predictor. + public const string LossLessCorruptImage3 = "Webp/lossless_color_transform.webp"; // cross_color, predictor + public const string LossLessCorruptImage4 = "Webp/near_lossless_75.webp"; // predictor, cross_color. + } + + public static class Lossy + { + public const string WithExif = "WebP/exif_lossy.webp"; + public const string WithIccp = "WebP/iccp_lossy.webp"; + + // Lossy images without macroblock filtering. + public const string Bike = "WebP/bike_lossy.webp"; + public const string NoFilter01 = "WebP/vp80-01-intra-1400.webp"; + public const string NoFilter02 = "WebP/vp80-00-comprehensive-010.webp"; + public const string NoFilter03 = "WebP/vp80-00-comprehensive-005.webp"; + public const string NoFilter04 = "WebP/vp80-01-intra-1417.webp"; + public const string NoFilter05 = "WebP/vp80-02-inter-1402.webp"; + public const string NoFilter06 = "WebP/test.webp"; + + // Lossy images with a simple filter. + public const string SimpleFilter01 = "WebP/segment01.webp"; + public const string SimpleFilter02 = "WebP/segment02.webp"; + public const string SimpleFilter03 = "WebP/vp80-00-comprehensive-003.webp"; + public const string SimpleFilter04 = "WebP/vp80-00-comprehensive-007.webp"; + public const string SimpleFilter05 = "WebP/test-nostrong.webp"; + + // Lossy images with a complex filter. + public const string IccpComplexFilter = "WebP/iccp_lossy.webp"; + public const string VeryShort = "WebP/very_short.webp"; + public const string BikeComplexFilter = "WebP/bike_lossy_complex_filter.webp"; + public const string ComplexFilter01 = "WebP/vp80-02-inter-1418.webp"; + public const string ComplexFilter02 = "WebP/vp80-02-inter-1418.webp"; + public const string ComplexFilter03 = "WebP/vp80-00-comprehensive-002.webp"; + public const string ComplexFilter04 = "WebP/vp80-00-comprehensive-006.webp"; + public const string ComplexFilter05 = "WebP/vp80-00-comprehensive-009.webp"; + public const string ComplexFilter06 = "WebP/vp80-00-comprehensive-012.webp"; + public const string ComplexFilter07 = "WebP/vp80-00-comprehensive-015.webp"; + public const string ComplexFilter08 = "WebP/vp80-00-comprehensive-016.webp"; + public const string ComplexFilter09 = "WebP/vp80-00-comprehensive-017.webp"; + + // Lossy with partitions. + public const string Partitions01 = "WebP/vp80-04-partitions-1404.webp"; + public const string Partitions02 = "WebP/vp80-04-partitions-1405.webp"; + public const string Partitions03 = "WebP/vp80-04-partitions-1406.webp"; + + // Lossy with segmentation. + public const string SegmentationNoFilter01 = "WebP/vp80-03-segmentation-1401.webp"; + public const string SegmentationNoFilter02 = "WebP/vp80-03-segmentation-1403.webp"; + public const string SegmentationNoFilter03 = "WebP/vp80-03-segmentation-1407.webp"; + public const string SegmentationNoFilter04 = "WebP/vp80-03-segmentation-1408.webp"; + public const string SegmentationNoFilter05 = "WebP/vp80-03-segmentation-1409.webp"; + public const string SegmentationNoFilter06 = "WebP/vp80-03-segmentation-1410.webp"; + public const string SegmentationComplexFilter01 = "WebP/vp80-03-segmentation-1413.webp"; + public const string SegmentationComplexFilter02 = "WebP/vp80-03-segmentation-1425.webp"; + public const string SegmentationComplexFilter03 = "WebP/vp80-03-segmentation-1426.webp"; + public const string SegmentationComplexFilter04 = "WebP/vp80-03-segmentation-1427.webp"; + public const string SegmentationComplexFilter05 = "WebP/vp80-03-segmentation-1432.webp"; + + // Lossy with sharpness level. + public const string Sharpness01 = "WebP/vp80-05-sharpness-1428.webp"; + public const string Sharpness02 = "WebP/vp80-05-sharpness-1429.webp"; + public const string Sharpness03 = "WebP/vp80-05-sharpness-1430.webp"; + public const string Sharpness04 = "WebP/vp80-05-sharpness-1431.webp"; + public const string Sharpness05 = "WebP/vp80-05-sharpness-1433.webp"; + public const string Sharpness06 = "WebP/vp80-05-sharpness-1434.webp"; + + // Very small images (all with complex filter). + public const string Small01 = "WebP/small_13x1.webp"; + public const string Small02 = "WebP/small_1x1.webp"; + public const string Small03 = "WebP/small_1x13.webp"; + public const string Small04 = "WebP/small_31x13.webp"; + + // Lossy images with an alpha channel. + public const string Alpha1 = "WebP/lossy_alpha1.webp"; + public const string Alpha2 = "WebP/lossy_alpha2.webp"; + public const string AlphaNoCompression = "WebP/alpha_no_compression.webp"; + public const string AlphaNoCompressionNoFilter = "WebP/alpha_filter_0_method_0.webp"; + public const string AlphaCompressedNoFilter = "WebP/alpha_filter_0_method_1.webp"; + public const string AlphaNoCompressionHorizontalFilter = "WebP/alpha_filter_1_method_0.webp"; + public const string AlphaCompressedHorizontalFilter = "WebP/alpha_filter_1_method_1.webp"; + public const string AlphaNoCompressionVerticalFilter = "WebP/alpha_filter_2_method_0.webp"; + public const string AlphaCompressedVerticalFilter = "WebP/alpha_filter_2_method_1.webp"; + public const string AlphaNoCompressionGradientFilter = "WebP/alpha_filter_3_method_0.webp"; + public const string AlphaCompressedGradientFilter = "WebP/alpha_filter_3_method_1.webp"; + } + } } } diff --git a/tests/Images/External b/tests/Images/External index 1fea1ceab8..99a2bc523c 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit 1fea1ceab89e87cc5f11376fa46164d3d27566c0 +Subproject commit 99a2bc523cd4eb00e37af20d1b2088fa11564c57 diff --git a/tests/Images/Input/WebP/alpha_color_cache.webp b/tests/Images/Input/WebP/alpha_color_cache.webp new file mode 100644 index 0000000000..7d10c87170 Binary files /dev/null and b/tests/Images/Input/WebP/alpha_color_cache.webp differ diff --git a/tests/Images/Input/WebP/alpha_filter_0_method_0.webp b/tests/Images/Input/WebP/alpha_filter_0_method_0.webp new file mode 100644 index 0000000000..b935b5c57f Binary files /dev/null and b/tests/Images/Input/WebP/alpha_filter_0_method_0.webp differ diff --git a/tests/Images/Input/WebP/alpha_filter_0_method_1.webp b/tests/Images/Input/WebP/alpha_filter_0_method_1.webp new file mode 100644 index 0000000000..b333bee81e Binary files /dev/null and b/tests/Images/Input/WebP/alpha_filter_0_method_1.webp differ diff --git a/tests/Images/Input/WebP/alpha_filter_1.webp b/tests/Images/Input/WebP/alpha_filter_1.webp new file mode 100644 index 0000000000..fcda70e86f Binary files /dev/null and b/tests/Images/Input/WebP/alpha_filter_1.webp differ diff --git a/tests/Images/Input/WebP/alpha_filter_1_method_0.webp b/tests/Images/Input/WebP/alpha_filter_1_method_0.webp new file mode 100644 index 0000000000..009131c0e8 Binary files /dev/null and b/tests/Images/Input/WebP/alpha_filter_1_method_0.webp differ diff --git a/tests/Images/Input/WebP/alpha_filter_1_method_1.webp b/tests/Images/Input/WebP/alpha_filter_1_method_1.webp new file mode 100644 index 0000000000..bd9bf2f2e6 Binary files /dev/null and b/tests/Images/Input/WebP/alpha_filter_1_method_1.webp differ diff --git a/tests/Images/Input/WebP/alpha_filter_2.webp b/tests/Images/Input/WebP/alpha_filter_2.webp new file mode 100644 index 0000000000..13f77f51fb Binary files /dev/null and b/tests/Images/Input/WebP/alpha_filter_2.webp differ diff --git a/tests/Images/Input/WebP/alpha_filter_2_method_0.webp b/tests/Images/Input/WebP/alpha_filter_2_method_0.webp new file mode 100644 index 0000000000..0e3ec8dcef Binary files /dev/null and b/tests/Images/Input/WebP/alpha_filter_2_method_0.webp differ diff --git a/tests/Images/Input/WebP/alpha_filter_2_method_1.webp b/tests/Images/Input/WebP/alpha_filter_2_method_1.webp new file mode 100644 index 0000000000..5713523667 Binary files /dev/null and b/tests/Images/Input/WebP/alpha_filter_2_method_1.webp differ diff --git a/tests/Images/Input/WebP/alpha_filter_3.webp b/tests/Images/Input/WebP/alpha_filter_3.webp new file mode 100644 index 0000000000..6624e55359 Binary files /dev/null and b/tests/Images/Input/WebP/alpha_filter_3.webp differ diff --git a/tests/Images/Input/WebP/alpha_filter_3_method_0.webp b/tests/Images/Input/WebP/alpha_filter_3_method_0.webp new file mode 100644 index 0000000000..f49e557994 Binary files /dev/null and b/tests/Images/Input/WebP/alpha_filter_3_method_0.webp differ diff --git a/tests/Images/Input/WebP/alpha_filter_3_method_1.webp b/tests/Images/Input/WebP/alpha_filter_3_method_1.webp new file mode 100644 index 0000000000..271ab892c0 Binary files /dev/null and b/tests/Images/Input/WebP/alpha_filter_3_method_1.webp differ diff --git a/tests/Images/Input/WebP/alpha_no_compression.webp b/tests/Images/Input/WebP/alpha_no_compression.webp new file mode 100644 index 0000000000..021572fb5e Binary files /dev/null and b/tests/Images/Input/WebP/alpha_no_compression.webp differ diff --git a/tests/Images/Input/WebP/animated-webp.webp b/tests/Images/Input/WebP/animated-webp.webp new file mode 100644 index 0000000000..7a859309d8 Binary files /dev/null and b/tests/Images/Input/WebP/animated-webp.webp differ diff --git a/tests/Images/Input/WebP/animated2.webp b/tests/Images/Input/WebP/animated2.webp new file mode 100644 index 0000000000..91c879e7aa Binary files /dev/null and b/tests/Images/Input/WebP/animated2.webp differ diff --git a/tests/Images/Input/WebP/animated3.webp b/tests/Images/Input/WebP/animated3.webp new file mode 100644 index 0000000000..124e064b89 Binary files /dev/null and b/tests/Images/Input/WebP/animated3.webp differ diff --git a/tests/Images/Input/WebP/animated_lossy.webp b/tests/Images/Input/WebP/animated_lossy.webp new file mode 100644 index 0000000000..fe0266fea6 Binary files /dev/null and b/tests/Images/Input/WebP/animated_lossy.webp differ diff --git a/tests/Images/Input/WebP/bad_palette_index.webp b/tests/Images/Input/WebP/bad_palette_index.webp new file mode 100644 index 0000000000..89064d81e4 Binary files /dev/null and b/tests/Images/Input/WebP/bad_palette_index.webp differ diff --git a/tests/Images/Input/WebP/big_endian_bug_393.webp b/tests/Images/Input/WebP/big_endian_bug_393.webp new file mode 100644 index 0000000000..c21a078c12 Binary files /dev/null and b/tests/Images/Input/WebP/big_endian_bug_393.webp differ diff --git a/tests/Images/Input/WebP/bike_lossless.webp b/tests/Images/Input/WebP/bike_lossless.webp new file mode 100644 index 0000000000..058407b999 Binary files /dev/null and b/tests/Images/Input/WebP/bike_lossless.webp differ diff --git a/tests/Images/Input/WebP/bike_lossy.webp b/tests/Images/Input/WebP/bike_lossy.webp new file mode 100644 index 0000000000..2e1b7b33a1 Binary files /dev/null and b/tests/Images/Input/WebP/bike_lossy.webp differ diff --git a/tests/Images/Input/WebP/bike_lossy_complex_filter.webp b/tests/Images/Input/WebP/bike_lossy_complex_filter.webp new file mode 100644 index 0000000000..9b5d622453 Binary files /dev/null and b/tests/Images/Input/WebP/bike_lossy_complex_filter.webp differ diff --git a/tests/Images/Input/WebP/bryce.webp b/tests/Images/Input/WebP/bryce.webp new file mode 100644 index 0000000000..63a11af76b Binary files /dev/null and b/tests/Images/Input/WebP/bryce.webp differ diff --git a/tests/Images/Input/WebP/bug3.webp b/tests/Images/Input/WebP/bug3.webp new file mode 100644 index 0000000000..0b59987e4d Binary files /dev/null and b/tests/Images/Input/WebP/bug3.webp differ diff --git a/tests/Images/Input/WebP/color_cache_bits_11.webp b/tests/Images/Input/WebP/color_cache_bits_11.webp new file mode 100644 index 0000000000..0404b9cac2 Binary files /dev/null and b/tests/Images/Input/WebP/color_cache_bits_11.webp differ diff --git a/tests/Images/Input/WebP/exif_lossless.webp b/tests/Images/Input/WebP/exif_lossless.webp new file mode 100644 index 0000000000..18ef4153fd Binary files /dev/null and b/tests/Images/Input/WebP/exif_lossless.webp differ diff --git a/tests/Images/Input/WebP/exif_lossy.webp b/tests/Images/Input/WebP/exif_lossy.webp new file mode 100644 index 0000000000..440250dd8f Binary files /dev/null and b/tests/Images/Input/WebP/exif_lossy.webp differ diff --git a/tests/Images/Input/WebP/grid.png b/tests/Images/Input/WebP/grid.png new file mode 100644 index 0000000000..041c4f2997 Binary files /dev/null and b/tests/Images/Input/WebP/grid.png differ diff --git a/tests/Images/Input/WebP/iccp_lossless.webp b/tests/Images/Input/WebP/iccp_lossless.webp new file mode 100644 index 0000000000..d125c0484b Binary files /dev/null and b/tests/Images/Input/WebP/iccp_lossless.webp differ diff --git a/tests/Images/Input/WebP/iccp_lossy.webp b/tests/Images/Input/WebP/iccp_lossy.webp new file mode 100644 index 0000000000..a95d83de74 Binary files /dev/null and b/tests/Images/Input/WebP/iccp_lossy.webp differ diff --git a/tests/Images/Input/WebP/lossless1.webp b/tests/Images/Input/WebP/lossless1.webp new file mode 100644 index 0000000000..c0cdebf52c Binary files /dev/null and b/tests/Images/Input/WebP/lossless1.webp differ diff --git a/tests/Images/Input/WebP/lossless2.webp b/tests/Images/Input/WebP/lossless2.webp new file mode 100644 index 0000000000..7b20fe01b4 Binary files /dev/null and b/tests/Images/Input/WebP/lossless2.webp differ diff --git a/tests/Images/Input/WebP/lossless3.webp b/tests/Images/Input/WebP/lossless3.webp new file mode 100644 index 0000000000..56a4f9fbd7 Binary files /dev/null and b/tests/Images/Input/WebP/lossless3.webp differ diff --git a/tests/Images/Input/WebP/lossless4.webp b/tests/Images/Input/WebP/lossless4.webp new file mode 100644 index 0000000000..b7a77b5c4a Binary files /dev/null and b/tests/Images/Input/WebP/lossless4.webp differ diff --git a/tests/Images/Input/WebP/lossless_big_random_alpha.webp b/tests/Images/Input/WebP/lossless_big_random_alpha.webp new file mode 100644 index 0000000000..5df96dad4f Binary files /dev/null and b/tests/Images/Input/WebP/lossless_big_random_alpha.webp differ diff --git a/tests/Images/Input/WebP/lossless_color_transform.webp b/tests/Images/Input/WebP/lossless_color_transform.webp new file mode 100644 index 0000000000..b59ffcfea4 Binary files /dev/null and b/tests/Images/Input/WebP/lossless_color_transform.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_1_0.webp b/tests/Images/Input/WebP/lossless_vec_1_0.webp new file mode 100644 index 0000000000..5da0ae20d1 Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_1_0.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_1_1.webp b/tests/Images/Input/WebP/lossless_vec_1_1.webp new file mode 100644 index 0000000000..426af4a207 Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_1_1.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_1_10.webp b/tests/Images/Input/WebP/lossless_vec_1_10.webp new file mode 100644 index 0000000000..89147a0b9d Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_1_10.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_1_11.webp b/tests/Images/Input/WebP/lossless_vec_1_11.webp new file mode 100644 index 0000000000..b465dd328e Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_1_11.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_1_12.webp b/tests/Images/Input/WebP/lossless_vec_1_12.webp new file mode 100644 index 0000000000..fabe293f85 Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_1_12.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_1_13.webp b/tests/Images/Input/WebP/lossless_vec_1_13.webp new file mode 100644 index 0000000000..684ca66400 Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_1_13.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_1_14.webp b/tests/Images/Input/WebP/lossless_vec_1_14.webp new file mode 100644 index 0000000000..667fce9411 Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_1_14.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_1_15.webp b/tests/Images/Input/WebP/lossless_vec_1_15.webp new file mode 100644 index 0000000000..2dd011fec6 Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_1_15.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_1_2.webp b/tests/Images/Input/WebP/lossless_vec_1_2.webp new file mode 100644 index 0000000000..4f8160b15e Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_1_2.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_1_3.webp b/tests/Images/Input/WebP/lossless_vec_1_3.webp new file mode 100644 index 0000000000..5871866886 Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_1_3.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_1_4.webp b/tests/Images/Input/WebP/lossless_vec_1_4.webp new file mode 100644 index 0000000000..9de86d8f01 Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_1_4.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_1_5.webp b/tests/Images/Input/WebP/lossless_vec_1_5.webp new file mode 100644 index 0000000000..1bd1e8c67a Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_1_5.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_1_6.webp b/tests/Images/Input/WebP/lossless_vec_1_6.webp new file mode 100644 index 0000000000..24fd10ffd5 Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_1_6.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_1_7.webp b/tests/Images/Input/WebP/lossless_vec_1_7.webp new file mode 100644 index 0000000000..f5e313e3b1 Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_1_7.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_1_8.webp b/tests/Images/Input/WebP/lossless_vec_1_8.webp new file mode 100644 index 0000000000..9b718c0d44 Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_1_8.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_1_9.webp b/tests/Images/Input/WebP/lossless_vec_1_9.webp new file mode 100644 index 0000000000..a35d706197 Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_1_9.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_2_0.webp b/tests/Images/Input/WebP/lossless_vec_2_0.webp new file mode 100644 index 0000000000..288a232c6e Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_2_0.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_2_1.webp b/tests/Images/Input/WebP/lossless_vec_2_1.webp new file mode 100644 index 0000000000..658f9da9c9 Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_2_1.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_2_10.webp b/tests/Images/Input/WebP/lossless_vec_2_10.webp new file mode 100644 index 0000000000..f7d5315b1b Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_2_10.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_2_11.webp b/tests/Images/Input/WebP/lossless_vec_2_11.webp new file mode 100644 index 0000000000..94171acb40 Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_2_11.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_2_12.webp b/tests/Images/Input/WebP/lossless_vec_2_12.webp new file mode 100644 index 0000000000..8e935921a8 Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_2_12.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_2_13.webp b/tests/Images/Input/WebP/lossless_vec_2_13.webp new file mode 100644 index 0000000000..9e1b06a3d1 Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_2_13.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_2_14.webp b/tests/Images/Input/WebP/lossless_vec_2_14.webp new file mode 100644 index 0000000000..3c897a6afe Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_2_14.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_2_15.webp b/tests/Images/Input/WebP/lossless_vec_2_15.webp new file mode 100644 index 0000000000..adaec20803 Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_2_15.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_2_2.webp b/tests/Images/Input/WebP/lossless_vec_2_2.webp new file mode 100644 index 0000000000..dd8ca3aa6a Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_2_2.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_2_3.webp b/tests/Images/Input/WebP/lossless_vec_2_3.webp new file mode 100644 index 0000000000..944655c17f Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_2_3.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_2_4.webp b/tests/Images/Input/WebP/lossless_vec_2_4.webp new file mode 100644 index 0000000000..ae3e281dd2 Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_2_4.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_2_5.webp b/tests/Images/Input/WebP/lossless_vec_2_5.webp new file mode 100644 index 0000000000..c5fe061645 Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_2_5.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_2_6.webp b/tests/Images/Input/WebP/lossless_vec_2_6.webp new file mode 100644 index 0000000000..88ba935212 Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_2_6.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_2_7.webp b/tests/Images/Input/WebP/lossless_vec_2_7.webp new file mode 100644 index 0000000000..fd843507a3 Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_2_7.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_2_8.webp b/tests/Images/Input/WebP/lossless_vec_2_8.webp new file mode 100644 index 0000000000..37c0bb6613 Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_2_8.webp differ diff --git a/tests/Images/Input/WebP/lossless_vec_2_9.webp b/tests/Images/Input/WebP/lossless_vec_2_9.webp new file mode 100644 index 0000000000..3f5f43324c Binary files /dev/null and b/tests/Images/Input/WebP/lossless_vec_2_9.webp differ diff --git a/tests/Images/Input/WebP/lossy_alpha1.webp b/tests/Images/Input/WebP/lossy_alpha1.webp new file mode 100644 index 0000000000..14fe79dcff Binary files /dev/null and b/tests/Images/Input/WebP/lossy_alpha1.webp differ diff --git a/tests/Images/Input/WebP/lossy_alpha2.webp b/tests/Images/Input/WebP/lossy_alpha2.webp new file mode 100644 index 0000000000..97e0ff1a36 Binary files /dev/null and b/tests/Images/Input/WebP/lossy_alpha2.webp differ diff --git a/tests/Images/Input/WebP/lossy_alpha3.webp b/tests/Images/Input/WebP/lossy_alpha3.webp new file mode 100644 index 0000000000..f2899df5e4 Binary files /dev/null and b/tests/Images/Input/WebP/lossy_alpha3.webp differ diff --git a/tests/Images/Input/WebP/lossy_alpha4.webp b/tests/Images/Input/WebP/lossy_alpha4.webp new file mode 100644 index 0000000000..0f99d2aba7 Binary files /dev/null and b/tests/Images/Input/WebP/lossy_alpha4.webp differ diff --git a/tests/Images/Input/WebP/lossy_extreme_probabilities.webp b/tests/Images/Input/WebP/lossy_extreme_probabilities.webp new file mode 100644 index 0000000000..90793ac813 Binary files /dev/null and b/tests/Images/Input/WebP/lossy_extreme_probabilities.webp differ diff --git a/tests/Images/Input/WebP/lossy_q0_f100.webp b/tests/Images/Input/WebP/lossy_q0_f100.webp new file mode 100644 index 0000000000..984194e8d4 Binary files /dev/null and b/tests/Images/Input/WebP/lossy_q0_f100.webp differ diff --git a/tests/Images/Input/WebP/near_lossless_75.webp b/tests/Images/Input/WebP/near_lossless_75.webp new file mode 100644 index 0000000000..99b20d8377 Binary files /dev/null and b/tests/Images/Input/WebP/near_lossless_75.webp differ diff --git a/tests/Images/Input/WebP/peak.png b/tests/Images/Input/WebP/peak.png new file mode 100644 index 0000000000..df87825957 Binary files /dev/null and b/tests/Images/Input/WebP/peak.png differ diff --git a/tests/Images/Input/WebP/segment01.webp b/tests/Images/Input/WebP/segment01.webp new file mode 100644 index 0000000000..29ec6b3608 Binary files /dev/null and b/tests/Images/Input/WebP/segment01.webp differ diff --git a/tests/Images/Input/WebP/segment02.webp b/tests/Images/Input/WebP/segment02.webp new file mode 100644 index 0000000000..65d1de4e8c Binary files /dev/null and b/tests/Images/Input/WebP/segment02.webp differ diff --git a/tests/Images/Input/WebP/segment03.webp b/tests/Images/Input/WebP/segment03.webp new file mode 100644 index 0000000000..1c262192db Binary files /dev/null and b/tests/Images/Input/WebP/segment03.webp differ diff --git a/tests/Images/Input/WebP/small_13x1.webp b/tests/Images/Input/WebP/small_13x1.webp new file mode 100644 index 0000000000..632a349aae Binary files /dev/null and b/tests/Images/Input/WebP/small_13x1.webp differ diff --git a/tests/Images/Input/WebP/small_1x1.webp b/tests/Images/Input/WebP/small_1x1.webp new file mode 100644 index 0000000000..47c928274b Binary files /dev/null and b/tests/Images/Input/WebP/small_1x1.webp differ diff --git a/tests/Images/Input/WebP/small_1x13.webp b/tests/Images/Input/WebP/small_1x13.webp new file mode 100644 index 0000000000..8049cac202 Binary files /dev/null and b/tests/Images/Input/WebP/small_1x13.webp differ diff --git a/tests/Images/Input/WebP/small_31x13.webp b/tests/Images/Input/WebP/small_31x13.webp new file mode 100644 index 0000000000..ab2f7e6d8b Binary files /dev/null and b/tests/Images/Input/WebP/small_31x13.webp differ diff --git a/tests/Images/Input/WebP/test-nostrong.webp b/tests/Images/Input/WebP/test-nostrong.webp new file mode 100644 index 0000000000..0b6f81a81c Binary files /dev/null and b/tests/Images/Input/WebP/test-nostrong.webp differ diff --git a/tests/Images/Input/WebP/test.webp b/tests/Images/Input/WebP/test.webp new file mode 100644 index 0000000000..c4a7b16c37 Binary files /dev/null and b/tests/Images/Input/WebP/test.webp differ diff --git a/tests/Images/Input/WebP/very_short.webp b/tests/Images/Input/WebP/very_short.webp new file mode 100644 index 0000000000..128f6e7464 Binary files /dev/null and b/tests/Images/Input/WebP/very_short.webp differ diff --git a/tests/Images/Input/WebP/vp80-00-comprehensive-001.webp b/tests/Images/Input/WebP/vp80-00-comprehensive-001.webp new file mode 100644 index 0000000000..86a2a35f83 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-00-comprehensive-001.webp differ diff --git a/tests/Images/Input/WebP/vp80-00-comprehensive-002.webp b/tests/Images/Input/WebP/vp80-00-comprehensive-002.webp new file mode 100644 index 0000000000..f2914d51fc Binary files /dev/null and b/tests/Images/Input/WebP/vp80-00-comprehensive-002.webp differ diff --git a/tests/Images/Input/WebP/vp80-00-comprehensive-003.webp b/tests/Images/Input/WebP/vp80-00-comprehensive-003.webp new file mode 100644 index 0000000000..90eca21c51 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-00-comprehensive-003.webp differ diff --git a/tests/Images/Input/WebP/vp80-00-comprehensive-004.webp b/tests/Images/Input/WebP/vp80-00-comprehensive-004.webp new file mode 100644 index 0000000000..817d1f41f4 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-00-comprehensive-004.webp differ diff --git a/tests/Images/Input/WebP/vp80-00-comprehensive-005.webp b/tests/Images/Input/WebP/vp80-00-comprehensive-005.webp new file mode 100644 index 0000000000..d20aa12675 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-00-comprehensive-005.webp differ diff --git a/tests/Images/Input/WebP/vp80-00-comprehensive-006.webp b/tests/Images/Input/WebP/vp80-00-comprehensive-006.webp new file mode 100644 index 0000000000..857d77af2f Binary files /dev/null and b/tests/Images/Input/WebP/vp80-00-comprehensive-006.webp differ diff --git a/tests/Images/Input/WebP/vp80-00-comprehensive-007.webp b/tests/Images/Input/WebP/vp80-00-comprehensive-007.webp new file mode 100644 index 0000000000..d08dc6c11b Binary files /dev/null and b/tests/Images/Input/WebP/vp80-00-comprehensive-007.webp differ diff --git a/tests/Images/Input/WebP/vp80-00-comprehensive-008.webp b/tests/Images/Input/WebP/vp80-00-comprehensive-008.webp new file mode 100644 index 0000000000..3e67e82fb4 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-00-comprehensive-008.webp differ diff --git a/tests/Images/Input/WebP/vp80-00-comprehensive-009.webp b/tests/Images/Input/WebP/vp80-00-comprehensive-009.webp new file mode 100644 index 0000000000..603a1c501b Binary files /dev/null and b/tests/Images/Input/WebP/vp80-00-comprehensive-009.webp differ diff --git a/tests/Images/Input/WebP/vp80-00-comprehensive-010.webp b/tests/Images/Input/WebP/vp80-00-comprehensive-010.webp new file mode 100644 index 0000000000..ddbd338580 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-00-comprehensive-010.webp differ diff --git a/tests/Images/Input/WebP/vp80-00-comprehensive-011.webp b/tests/Images/Input/WebP/vp80-00-comprehensive-011.webp new file mode 100644 index 0000000000..cabdf6ddde Binary files /dev/null and b/tests/Images/Input/WebP/vp80-00-comprehensive-011.webp differ diff --git a/tests/Images/Input/WebP/vp80-00-comprehensive-012.webp b/tests/Images/Input/WebP/vp80-00-comprehensive-012.webp new file mode 100644 index 0000000000..a7a458100d Binary files /dev/null and b/tests/Images/Input/WebP/vp80-00-comprehensive-012.webp differ diff --git a/tests/Images/Input/WebP/vp80-00-comprehensive-013.webp b/tests/Images/Input/WebP/vp80-00-comprehensive-013.webp new file mode 100644 index 0000000000..bcda6c22bf Binary files /dev/null and b/tests/Images/Input/WebP/vp80-00-comprehensive-013.webp differ diff --git a/tests/Images/Input/WebP/vp80-00-comprehensive-014.webp b/tests/Images/Input/WebP/vp80-00-comprehensive-014.webp new file mode 100644 index 0000000000..c5d06cc6ff Binary files /dev/null and b/tests/Images/Input/WebP/vp80-00-comprehensive-014.webp differ diff --git a/tests/Images/Input/WebP/vp80-00-comprehensive-015.webp b/tests/Images/Input/WebP/vp80-00-comprehensive-015.webp new file mode 100644 index 0000000000..bd8bd67988 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-00-comprehensive-015.webp differ diff --git a/tests/Images/Input/WebP/vp80-00-comprehensive-016.webp b/tests/Images/Input/WebP/vp80-00-comprehensive-016.webp new file mode 100644 index 0000000000..645e8c2190 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-00-comprehensive-016.webp differ diff --git a/tests/Images/Input/WebP/vp80-00-comprehensive-017.webp b/tests/Images/Input/WebP/vp80-00-comprehensive-017.webp new file mode 100644 index 0000000000..9b4a4e191d Binary files /dev/null and b/tests/Images/Input/WebP/vp80-00-comprehensive-017.webp differ diff --git a/tests/Images/Input/WebP/vp80-01-intra-1400.webp b/tests/Images/Input/WebP/vp80-01-intra-1400.webp new file mode 100644 index 0000000000..29815bf987 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-01-intra-1400.webp differ diff --git a/tests/Images/Input/WebP/vp80-01-intra-1411.webp b/tests/Images/Input/WebP/vp80-01-intra-1411.webp new file mode 100644 index 0000000000..f2e758f9f4 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-01-intra-1411.webp differ diff --git a/tests/Images/Input/WebP/vp80-01-intra-1416.webp b/tests/Images/Input/WebP/vp80-01-intra-1416.webp new file mode 100644 index 0000000000..ce8bd2a0a2 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-01-intra-1416.webp differ diff --git a/tests/Images/Input/WebP/vp80-01-intra-1417.webp b/tests/Images/Input/WebP/vp80-01-intra-1417.webp new file mode 100644 index 0000000000..44155a84f6 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-01-intra-1417.webp differ diff --git a/tests/Images/Input/WebP/vp80-02-inter-1402.webp b/tests/Images/Input/WebP/vp80-02-inter-1402.webp new file mode 100644 index 0000000000..b1f4a955d6 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-02-inter-1402.webp differ diff --git a/tests/Images/Input/WebP/vp80-02-inter-1412.webp b/tests/Images/Input/WebP/vp80-02-inter-1412.webp new file mode 100644 index 0000000000..82303c6676 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-02-inter-1412.webp differ diff --git a/tests/Images/Input/WebP/vp80-02-inter-1418.webp b/tests/Images/Input/WebP/vp80-02-inter-1418.webp new file mode 100644 index 0000000000..46f8c064bd Binary files /dev/null and b/tests/Images/Input/WebP/vp80-02-inter-1418.webp differ diff --git a/tests/Images/Input/WebP/vp80-02-inter-1424.webp b/tests/Images/Input/WebP/vp80-02-inter-1424.webp new file mode 100644 index 0000000000..8591fa25b0 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-02-inter-1424.webp differ diff --git a/tests/Images/Input/WebP/vp80-03-segmentation-1401.webp b/tests/Images/Input/WebP/vp80-03-segmentation-1401.webp new file mode 100644 index 0000000000..82f8b52d99 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-03-segmentation-1401.webp differ diff --git a/tests/Images/Input/WebP/vp80-03-segmentation-1403.webp b/tests/Images/Input/WebP/vp80-03-segmentation-1403.webp new file mode 100644 index 0000000000..826d34c6a9 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-03-segmentation-1403.webp differ diff --git a/tests/Images/Input/WebP/vp80-03-segmentation-1407.webp b/tests/Images/Input/WebP/vp80-03-segmentation-1407.webp new file mode 100644 index 0000000000..ba93287507 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-03-segmentation-1407.webp differ diff --git a/tests/Images/Input/WebP/vp80-03-segmentation-1408.webp b/tests/Images/Input/WebP/vp80-03-segmentation-1408.webp new file mode 100644 index 0000000000..c5b8d9aeee Binary files /dev/null and b/tests/Images/Input/WebP/vp80-03-segmentation-1408.webp differ diff --git a/tests/Images/Input/WebP/vp80-03-segmentation-1409.webp b/tests/Images/Input/WebP/vp80-03-segmentation-1409.webp new file mode 100644 index 0000000000..530868dfb4 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-03-segmentation-1409.webp differ diff --git a/tests/Images/Input/WebP/vp80-03-segmentation-1410.webp b/tests/Images/Input/WebP/vp80-03-segmentation-1410.webp new file mode 100644 index 0000000000..bdd26505ed Binary files /dev/null and b/tests/Images/Input/WebP/vp80-03-segmentation-1410.webp differ diff --git a/tests/Images/Input/WebP/vp80-03-segmentation-1413.webp b/tests/Images/Input/WebP/vp80-03-segmentation-1413.webp new file mode 100644 index 0000000000..34c8633b6a Binary files /dev/null and b/tests/Images/Input/WebP/vp80-03-segmentation-1413.webp differ diff --git a/tests/Images/Input/WebP/vp80-03-segmentation-1414.webp b/tests/Images/Input/WebP/vp80-03-segmentation-1414.webp new file mode 100644 index 0000000000..a183487b82 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-03-segmentation-1414.webp differ diff --git a/tests/Images/Input/WebP/vp80-03-segmentation-1415.webp b/tests/Images/Input/WebP/vp80-03-segmentation-1415.webp new file mode 100644 index 0000000000..d6d0e3d19a Binary files /dev/null and b/tests/Images/Input/WebP/vp80-03-segmentation-1415.webp differ diff --git a/tests/Images/Input/WebP/vp80-03-segmentation-1425.webp b/tests/Images/Input/WebP/vp80-03-segmentation-1425.webp new file mode 100644 index 0000000000..3315390a05 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-03-segmentation-1425.webp differ diff --git a/tests/Images/Input/WebP/vp80-03-segmentation-1426.webp b/tests/Images/Input/WebP/vp80-03-segmentation-1426.webp new file mode 100644 index 0000000000..4821dad02a Binary files /dev/null and b/tests/Images/Input/WebP/vp80-03-segmentation-1426.webp differ diff --git a/tests/Images/Input/WebP/vp80-03-segmentation-1427.webp b/tests/Images/Input/WebP/vp80-03-segmentation-1427.webp new file mode 100644 index 0000000000..62083beae6 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-03-segmentation-1427.webp differ diff --git a/tests/Images/Input/WebP/vp80-03-segmentation-1432.webp b/tests/Images/Input/WebP/vp80-03-segmentation-1432.webp new file mode 100644 index 0000000000..2393fa5920 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-03-segmentation-1432.webp differ diff --git a/tests/Images/Input/WebP/vp80-03-segmentation-1435.webp b/tests/Images/Input/WebP/vp80-03-segmentation-1435.webp new file mode 100644 index 0000000000..5c74d7e6f0 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-03-segmentation-1435.webp differ diff --git a/tests/Images/Input/WebP/vp80-03-segmentation-1436.webp b/tests/Images/Input/WebP/vp80-03-segmentation-1436.webp new file mode 100644 index 0000000000..04a074db7b Binary files /dev/null and b/tests/Images/Input/WebP/vp80-03-segmentation-1436.webp differ diff --git a/tests/Images/Input/WebP/vp80-03-segmentation-1437.webp b/tests/Images/Input/WebP/vp80-03-segmentation-1437.webp new file mode 100644 index 0000000000..5c760a3214 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-03-segmentation-1437.webp differ diff --git a/tests/Images/Input/WebP/vp80-03-segmentation-1441.webp b/tests/Images/Input/WebP/vp80-03-segmentation-1441.webp new file mode 100644 index 0000000000..0a32c73bfb Binary files /dev/null and b/tests/Images/Input/WebP/vp80-03-segmentation-1441.webp differ diff --git a/tests/Images/Input/WebP/vp80-03-segmentation-1442.webp b/tests/Images/Input/WebP/vp80-03-segmentation-1442.webp new file mode 100644 index 0000000000..c0a95309c3 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-03-segmentation-1442.webp differ diff --git a/tests/Images/Input/WebP/vp80-04-partitions-1404.webp b/tests/Images/Input/WebP/vp80-04-partitions-1404.webp new file mode 100644 index 0000000000..91ed1f0851 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-04-partitions-1404.webp differ diff --git a/tests/Images/Input/WebP/vp80-04-partitions-1405.webp b/tests/Images/Input/WebP/vp80-04-partitions-1405.webp new file mode 100644 index 0000000000..2c94ce4c92 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-04-partitions-1405.webp differ diff --git a/tests/Images/Input/WebP/vp80-04-partitions-1406.webp b/tests/Images/Input/WebP/vp80-04-partitions-1406.webp new file mode 100644 index 0000000000..41509957fa Binary files /dev/null and b/tests/Images/Input/WebP/vp80-04-partitions-1406.webp differ diff --git a/tests/Images/Input/WebP/vp80-05-sharpness-1428.webp b/tests/Images/Input/WebP/vp80-05-sharpness-1428.webp new file mode 100644 index 0000000000..b1713a69c2 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-05-sharpness-1428.webp differ diff --git a/tests/Images/Input/WebP/vp80-05-sharpness-1429.webp b/tests/Images/Input/WebP/vp80-05-sharpness-1429.webp new file mode 100644 index 0000000000..a81bfcf5a4 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-05-sharpness-1429.webp differ diff --git a/tests/Images/Input/WebP/vp80-05-sharpness-1430.webp b/tests/Images/Input/WebP/vp80-05-sharpness-1430.webp new file mode 100644 index 0000000000..66806df911 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-05-sharpness-1430.webp differ diff --git a/tests/Images/Input/WebP/vp80-05-sharpness-1431.webp b/tests/Images/Input/WebP/vp80-05-sharpness-1431.webp new file mode 100644 index 0000000000..ac3d0395c7 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-05-sharpness-1431.webp differ diff --git a/tests/Images/Input/WebP/vp80-05-sharpness-1433.webp b/tests/Images/Input/WebP/vp80-05-sharpness-1433.webp new file mode 100644 index 0000000000..818c0b9b29 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-05-sharpness-1433.webp differ diff --git a/tests/Images/Input/WebP/vp80-05-sharpness-1434.webp b/tests/Images/Input/WebP/vp80-05-sharpness-1434.webp new file mode 100644 index 0000000000..cc96d5228c Binary files /dev/null and b/tests/Images/Input/WebP/vp80-05-sharpness-1434.webp differ diff --git a/tests/Images/Input/WebP/vp80-05-sharpness-1438.webp b/tests/Images/Input/WebP/vp80-05-sharpness-1438.webp new file mode 100644 index 0000000000..52fab64fa1 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-05-sharpness-1438.webp differ diff --git a/tests/Images/Input/WebP/vp80-05-sharpness-1439.webp b/tests/Images/Input/WebP/vp80-05-sharpness-1439.webp new file mode 100644 index 0000000000..9db92603eb Binary files /dev/null and b/tests/Images/Input/WebP/vp80-05-sharpness-1439.webp differ diff --git a/tests/Images/Input/WebP/vp80-05-sharpness-1440.webp b/tests/Images/Input/WebP/vp80-05-sharpness-1440.webp new file mode 100644 index 0000000000..2d64746af3 Binary files /dev/null and b/tests/Images/Input/WebP/vp80-05-sharpness-1440.webp differ diff --git a/tests/Images/Input/WebP/vp80-05-sharpness-1443.webp b/tests/Images/Input/WebP/vp80-05-sharpness-1443.webp new file mode 100644 index 0000000000..8be0799ead Binary files /dev/null and b/tests/Images/Input/WebP/vp80-05-sharpness-1443.webp differ