diff --git a/ImageSharp.sln b/ImageSharp.sln index c433d22f5..c188d9315 100644 --- a/ImageSharp.sln +++ b/ImageSharp.sln @@ -1,3 +1,4 @@ + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.28902.138 @@ -378,6 +379,170 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Png", "Png", "{E1C42A6F-913 tests\Images\Input\Png\zlib-ztxt-bad-header.png = tests\Images\Input\Png\zlib-ztxt-bad-header.png EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Webp", "Webp", "{983A31E2-5E26-4058-BD6E-03B4922D4BBF}" + ProjectSection(SolutionItems) = preProject + tests\Images\Input\Webp\1602311202.webp = tests\Images\Input\Webp\1602311202.webp + 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\bike_lossless.webp = tests\Images\Input\Webp\bike_lossless.webp + tests\Images\Input\Webp\bike_lossless_small.webp = tests\Images\Input\Webp\bike_lossless_small.webp + tests\Images\Input\Webp\bike_lossy.webp = tests\Images\Input\Webp\bike_lossy.webp + tests\Images\Input\Webp\bike_lossy_complex_filter.webp = tests\Images\Input\Webp\bike_lossy_complex_filter.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\earth_lossless.webp = tests\Images\Input\Webp\earth_lossless.webp + tests\Images\Input\Webp\earth_lossy.webp = tests\Images\Input\Webp\earth_lossy.webp + tests\Images\Input\Webp\exif_lossless.webp = tests\Images\Input\Webp\exif_lossless.webp + tests\Images\Input\Webp\exif_lossy.webp = tests\Images\Input\Webp\exif_lossy.webp + tests\Images\Input\Webp\flag_of_germany.png = tests\Images\Input\Webp\flag_of_germany.png + 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_alpha_small.webp = tests\Images\Input\Webp\lossless_alpha_small.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\lossless_with_iccp.webp = tests\Images\Input\Webp\lossless_with_iccp.webp + 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\lossy_with_iccp.webp = tests\Images\Input\Webp\lossy_with_iccp.webp + tests\Images\Input\Webp\near_lossless_75.webp = tests\Images\Input\Webp\near_lossless_75.webp + tests\Images\Input\Webp\peak.png = tests\Images\Input\Webp\peak.png + tests\Images\Input\Webp\rgb_pattern_100x100.png = tests\Images\Input\Webp\rgb_pattern_100x100.png + tests\Images\Input\Webp\rgb_pattern_63x63.png = tests\Images\Input\Webp\rgb_pattern_63x63.png + tests\Images\Input\Webp\rgb_pattern_80x80.png = tests\Images\Input\Webp\rgb_pattern_80x80.png + 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\sticker.webp = tests\Images\Input\Webp\sticker.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\testpattern_opaque.png = tests\Images\Input\Webp\testpattern_opaque.png + tests\Images\Input\Webp\testpattern_opaque_small.png = tests\Images\Input\Webp\testpattern_opaque_small.png + 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 + tests\Images\Input\Webp\yuv_test.png = tests\Images\Input\Webp\yuv_test.png + EndProjectSection +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp.Tests", "tests\ImageSharp.Tests\ImageSharp.Tests.csproj", "{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp.Benchmarks", "tests\ImageSharp.Benchmarks\ImageSharp.Benchmarks.csproj", "{2BF743D8-2A06-412D-96D7-F448F00C5EA5}" @@ -539,6 +704,7 @@ Global {6458AFCB-A159-47D5-8F2B-50C95C0915E0} = {DB21FED7-E8CB-4B00-9EB2-9144D32A590A} {39F5197B-CF6C-41A5-9739-7F97E78BB104} = {6458AFCB-A159-47D5-8F2B-50C95C0915E0} {E1C42A6F-913B-4A7B-B1A8-2BB62843B254} = {9DA226A1-8656-49A8-A58A-A8B5C081AD66} + {983A31E2-5E26-4058-BD6E-03B4922D4BBF} = {9DA226A1-8656-49A8-A58A-A8B5C081AD66} {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC} {2BF743D8-2A06-412D-96D7-F448F00C5EA5} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC} {C0D7754B-5277-438E-ABEB-2BA34401B5A7} = {1799C43E-5C54-4A8F-8D64-B1475241DB0D} diff --git a/src/ImageSharp/Advanced/AotCompilerTools.cs b/src/ImageSharp/Advanced/AotCompilerTools.cs index be2e964fc..3961cc6c5 100644 --- a/src/ImageSharp/Advanced/AotCompilerTools.cs +++ b/src/ImageSharp/Advanced/AotCompilerTools.cs @@ -13,6 +13,7 @@ using SixLabors.ImageSharp.Formats.Jpeg.Components; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -195,6 +196,7 @@ namespace SixLabors.ImageSharp.Advanced private static void AotCompileImageEncoderInternals() where TPixel : unmanaged, IPixel { + default(WebpEncoderCore).Encode(default, default, default); default(BmpEncoderCore).Encode(default, default, default); default(GifEncoderCore).Encode(default, default, default); default(JpegEncoderCore).Encode(default, default, default); @@ -211,6 +213,7 @@ namespace SixLabors.ImageSharp.Advanced private static void AotCompileImageDecoderInternals() where TPixel : unmanaged, IPixel { + default(WebpDecoderCore).Decode(default, default, default); default(BmpDecoderCore).Decode(default, default, default); default(GifDecoderCore).Decode(default, default, default); default(JpegDecoderCore).Decode(default, default, default); @@ -227,6 +230,7 @@ namespace SixLabors.ImageSharp.Advanced private static void AotCompileImageEncoders() where TPixel : unmanaged, IPixel { + AotCompileImageEncoder(); AotCompileImageEncoder(); AotCompileImageEncoder(); AotCompileImageEncoder(); @@ -243,6 +247,7 @@ namespace SixLabors.ImageSharp.Advanced private static void AotCompileImageDecoders() where TPixel : unmanaged, IPixel { + AotCompileImageDecoder(); AotCompileImageDecoder(); AotCompileImageDecoder(); AotCompileImageDecoder(); diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs index 49b7aa79b..ea9524827 100644 --- a/src/ImageSharp/Configuration.cs +++ b/src/ImageSharp/Configuration.cs @@ -11,6 +11,7 @@ using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Processing; @@ -159,20 +160,17 @@ namespace SixLabors.ImageSharp /// Creates a shallow copy of the . /// /// A new configuration instance. - public Configuration Clone() + public Configuration Clone() => new Configuration { - return new Configuration - { - MaxDegreeOfParallelism = this.MaxDegreeOfParallelism, - StreamProcessingBufferSize = this.StreamProcessingBufferSize, - ImageFormatsManager = this.ImageFormatsManager, - MemoryAllocator = this.MemoryAllocator, - ImageOperationsProvider = this.ImageOperationsProvider, - ReadOrigin = this.ReadOrigin, - FileSystem = this.FileSystem, - WorkingBufferSizeHintInBytes = this.WorkingBufferSizeHintInBytes, - }; - } + MaxDegreeOfParallelism = this.MaxDegreeOfParallelism, + StreamProcessingBufferSize = this.StreamProcessingBufferSize, + ImageFormatsManager = this.ImageFormatsManager, + MemoryAllocator = this.MemoryAllocator, + ImageOperationsProvider = this.ImageOperationsProvider, + ReadOrigin = this.ReadOrigin, + FileSystem = this.FileSystem, + WorkingBufferSizeHintInBytes = this.WorkingBufferSizeHintInBytes, + }; /// /// Creates the default instance with the following s preregistered: @@ -182,17 +180,16 @@ namespace SixLabors.ImageSharp /// . /// . /// . + /// . /// /// The default configuration of . - internal static Configuration CreateDefaultInstance() - { - return new Configuration( + internal static Configuration CreateDefaultInstance() => new Configuration( new PngConfigurationModule(), new JpegConfigurationModule(), new GifConfigurationModule(), new BmpConfigurationModule(), new TgaConfigurationModule(), - new TiffConfigurationModule()); - } + new TiffConfigurationModule(), + new WebpConfigurationModule()); } } diff --git a/src/ImageSharp/Formats/ImageExtensions.Save.cs b/src/ImageSharp/Formats/ImageExtensions.Save.cs index 0f8b1e16d..c5237f2bc 100644 --- a/src/ImageSharp/Formats/ImageExtensions.Save.cs +++ b/src/ImageSharp/Formats/ImageExtensions.Save.cs @@ -12,6 +12,7 @@ using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Tga; +using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Formats.Tiff; namespace SixLabors.ImageSharp @@ -536,6 +537,109 @@ namespace SixLabors.ImageSharp encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance), cancellationToken); + /// + /// Saves the image to the given stream with the Webp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsWebp(this Image source, string path) => SaveAsWebp(source, path, null); + + /// + /// Saves the image to the given stream with the Webp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsWebpAsync(this Image source, string path) => SaveAsWebpAsync(source, path, null); + + /// + /// Saves the image to the given stream with the Webp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsWebpAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsWebpAsync(source, path, null, cancellationToken); + + /// + /// Saves the image to the given stream with the Webp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsWebp(this Image source, string path, WebpEncoder encoder) => + source.Save( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(WebpFormat.Instance)); + + /// + /// Saves the image to the given stream with the Webp format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsWebpAsync(this Image source, string path, WebpEncoder encoder, CancellationToken cancellationToken = default) => + source.SaveAsync( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(WebpFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Webp format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsWebp(this Image source, Stream stream) + => SaveAsWebp(source, stream, null); + + /// + /// Saves the image to the given stream with the Webp format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsWebpAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsWebpAsync(source, stream, null, cancellationToken); + + /// + /// Saves the image to the given stream with the Webp format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static void SaveAsWebp(this Image source, Stream stream, WebpEncoder encoder) + => source.Save( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(WebpFormat.Instance)); + + /// + /// Saves the image to the given stream with the Webp format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsWebpAsync(this Image source, Stream stream, WebpEncoder encoder, CancellationToken cancellationToken = default) => + source.SaveAsync( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(WebpFormat.Instance), + cancellationToken); + /// /// Saves the image to the given stream with the Tiff format. /// diff --git a/src/ImageSharp/Formats/ImageExtensions.Save.tt b/src/ImageSharp/Formats/ImageExtensions.Save.tt index af9531225..874f3ab0d 100644 --- a/src/ImageSharp/Formats/ImageExtensions.Save.tt +++ b/src/ImageSharp/Formats/ImageExtensions.Save.tt @@ -17,6 +17,7 @@ using SixLabors.ImageSharp.Advanced; "Jpeg", "Png", "Tga", + "Webp", "Tiff", }; diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs index 6f116f4fb..fc6e3189f 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs @@ -29,7 +29,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg where TPixel : unmanaged, IPixel { var encoder = new JpegEncoderCore(this); - this.InitializeColorType(image); encoder.Encode(image, stream); } @@ -45,31 +44,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg where TPixel : unmanaged, IPixel { var encoder = new JpegEncoderCore(this); - this.InitializeColorType(image); return encoder.EncodeAsync(image, stream, cancellationToken); } - - /// - /// If ColorType was not set, set it based on the given image. - /// - private void InitializeColorType(Image image) - where TPixel : unmanaged, IPixel - { - // First inspect the image metadata. - if (this.ColorType == null) - { - JpegMetadata metadata = image.Metadata.GetJpegMetadata(); - this.ColorType = metadata.ColorType; - } - - // Secondly, inspect the pixel type. - if (this.ColorType == null) - { - bool isGrayscale = - typeof(TPixel) == typeof(L8) || typeof(TPixel) == typeof(L16) || - typeof(TPixel) == typeof(La16) || typeof(TPixel) == typeof(La32); - this.ColorType = isGrayscale ? JpegColorType.Luminance : JpegColorType.YCbCrRatio420; - } - } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 6ff887667..d9d42e061 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -86,10 +86,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg ImageMetadata metadata = image.Metadata; JpegMetadata jpegMetadata = metadata.GetJpegMetadata(); - // If the color type was not specified by the user, preserve the color type of the input image, if it's a supported color type. - if (!this.colorType.HasValue && IsSupportedColorType(jpegMetadata.ColorType)) + // If the color type was not specified by the user, preserve the color type of the input image. + if (!this.colorType.HasValue) { - this.colorType = jpegMetadata.ColorType; + this.colorType = GetFallbackColorType(image); } // Compute number of components based on color type in options. @@ -156,6 +156,39 @@ namespace SixLabors.ImageSharp.Formats.Jpeg stream.Flush(); } + /// + /// If color type was not set, set it based on the given image. + /// Note, if there is no metadata and the image has multiple components this method + /// returns defering the field assignment + /// to . + /// + private static JpegColorType? GetFallbackColorType(Image image) + where TPixel : unmanaged, IPixel + { + // First inspect the image metadata. + JpegColorType? colorType = null; + JpegMetadata metadata = image.Metadata.GetJpegMetadata(); + if (IsSupportedColorType(metadata.ColorType)) + { + return metadata.ColorType; + } + + // Secondly, inspect the pixel type. + // TODO: PixelTypeInfo should contain a component count! + bool isGrayscale = + typeof(TPixel) == typeof(L8) || typeof(TPixel) == typeof(L16) || + typeof(TPixel) == typeof(La16) || typeof(TPixel) == typeof(La32); + + // We don't set multi-component color types here since we can set it based upon + // the quality in InitQuantizationTables. + if (isGrayscale) + { + colorType = JpegColorType.Luminance; + } + + return colorType; + } + /// /// Returns true, if the color type is supported by the encoder. /// diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 4f6fb7356..f10db7a6c 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -268,35 +268,27 @@ namespace SixLabors.ImageSharp.Formats.Png if (this.use16Bit) { // 16 bit grayscale + alpha - // TODO: Should we consider in the future a GrayAlpha32 type. - using (IMemoryOwner rgbaBuffer = this.memoryAllocator.Allocate(rowSpan.Length)) - { - Span rgbaSpan = rgbaBuffer.GetSpan(); - ref Rgba64 rgbaRef = ref MemoryMarshal.GetReference(rgbaSpan); - PixelOperations.Instance.ToRgba64(this.configuration, rowSpan, rgbaSpan); + using IMemoryOwner laBuffer = this.memoryAllocator.Allocate(rowSpan.Length); + Span laSpan = laBuffer.GetSpan(); + ref La32 laRef = ref MemoryMarshal.GetReference(laSpan); + PixelOperations.Instance.ToLa32(this.configuration, rowSpan, laSpan); - // Can't map directly to byte array as it's big endian. - for (int x = 0, o = 0; x < rgbaSpan.Length; x++, o += 4) - { - Rgba64 rgba = Unsafe.Add(ref rgbaRef, x); - ushort luminance = ColorNumerics.Get16BitBT709Luminance(rgba.R, rgba.G, rgba.B); - BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), luminance); - BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 2, 2), rgba.A); - } + // Can't map directly to byte array as it's big endian. + for (int x = 0, o = 0; x < laSpan.Length; x++, o += 4) + { + La32 la = Unsafe.Add(ref laRef, x); + BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), la.L); + BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 2, 2), la.A); } } else { // 8 bit grayscale + alpha - // TODO: Should we consider in the future a GrayAlpha16 type. - Rgba32 rgba = default; - for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 2) - { - Unsafe.Add(ref rowSpanRef, x).ToRgba32(ref rgba); - Unsafe.Add(ref rawScanlineSpanRef, o) = - ColorNumerics.Get8BitBT709Luminance(rgba.R, rgba.G, rgba.B); - Unsafe.Add(ref rawScanlineSpanRef, o + 1) = rgba.A; - } + PixelOperations.Instance.ToLa16Bytes( + this.configuration, + rowSpan, + rawScanlineSpan, + rowSpan.Length); } } } diff --git a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs index 3c17c2463..0bcea037a 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs @@ -18,11 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Png { this.BitDepth = source.BitDepth; this.ColorType = source.ColorType; - - // Specification recommends default filter method None for paletted images and Paeth for others. - this.FilterMethod = source.FilterMethod ?? (source.ColorType == PngColorType.Palette - ? PngFilterMethod.None - : PngFilterMethod.Paeth); + this.FilterMethod = source.FilterMethod; this.CompressionLevel = source.CompressionLevel; this.TextCompressionThreshold = source.TextCompressionThreshold; this.Gamma = source.Gamma; @@ -41,7 +37,7 @@ namespace SixLabors.ImageSharp.Formats.Png public PngColorType? ColorType { get; set; } /// - public PngFilterMethod? FilterMethod { get; } + public PngFilterMethod? FilterMethod { get; set; } /// public PngCompressionLevel CompressionLevel { get; } = PngCompressionLevel.DefaultCompression; diff --git a/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs b/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs index 23ca86993..1250db6fe 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs @@ -34,6 +34,18 @@ namespace SixLabors.ImageSharp.Formats.Png // a sensible default based upon the pixel format. options.ColorType ??= pngMetadata.ColorType ?? SuggestColorType(); options.BitDepth ??= pngMetadata.BitDepth ?? SuggestBitDepth(); + if (!options.FilterMethod.HasValue) + { + // Specification recommends default filter method None for paletted images and Paeth for others. + if (options.ColorType == PngColorType.Palette) + { + options.FilterMethod = PngFilterMethod.None; + } + else + { + options.FilterMethod = PngFilterMethod.Paeth; + } + } // Ensure bit depth and color type are a supported combination. // Bit8 is the only bit depth supported by all color types. diff --git a/src/ImageSharp/Formats/Webp/AlphaDecoder.cs b/src/ImageSharp/Formats/Webp/AlphaDecoder.cs new file mode 100644 index 000000000..e63cd27b5 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/AlphaDecoder.cs @@ -0,0 +1,431 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Webp.BitReader; +using SixLabors.ImageSharp.Formats.Webp.Lossless; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Webp +{ + /// + /// Implements decoding for lossy alpha chunks which may be compressed. + /// + internal class AlphaDecoder : IDisposable + { + private readonly MemoryAllocator memoryAllocator; + + /// + /// 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 how to decode the stream. + /// Used for allocating memory during decoding. + /// The configuration. + public AlphaDecoder(int width, int height, IMemoryOwner data, byte alphaChunkHeader, MemoryAllocator memoryAllocator, Configuration configuration) + { + this.Width = width; + this.Height = height; + this.Data = data; + this.memoryAllocator = memoryAllocator; + this.LastRow = 0; + int totalPixels = width * height; + + var compression = (WebpAlphaCompressionMethod)(alphaChunkHeader & 0x03); + if (compression is not WebpAlphaCompressionMethod.NoCompression and not WebpAlphaCompressionMethod.WebpLosslessCompression) + { + WebpThrowHelper.ThrowImageFormatException($"unexpected alpha compression method {compression} found"); + } + + this.Compressed = compression == WebpAlphaCompressionMethod.WebpLosslessCompression; + + // The filtering method used. Only values between 0 and 3 are valid. + int filter = (alphaChunkHeader >> 2) & 0x03; + if (filter is < (int)WebpAlphaFilterType.None or > (int)WebpAlphaFilterType.Gradient) + { + WebpThrowHelper.ThrowImageFormatException($"unexpected alpha filter method {filter} found"); + } + + this.Alpha = memoryAllocator.Allocate(totalPixels); + this.AlphaFilterType = (WebpAlphaFilterType)filter; + this.Vp8LDec = new Vp8LDecoder(width, height, memoryAllocator); + + if (this.Compressed) + { + var bitReader = new Vp8LBitReader(data); + this.LosslessDecoder = new WebpLosslessDecoder(bitReader, memoryAllocator, configuration); + this.LosslessDecoder.DecodeImageStream(this.Vp8LDec, width, height, true); + this.Use8BDecode = this.Vp8LDec.Transforms.Count > 0 && Is8BOptimizable(this.Vp8LDec.Metadata); + } + } + + /// + /// Gets 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; } + + /// + /// Gets a value indicating whether the alpha channel uses compression. + /// + private bool Compressed { get; } + + /// + /// Gets the (maybe compressed) alpha data. + /// + private IMemoryOwner Data { get; } + + /// + /// Gets the Vp8L decoder which is used to de compress the alpha channel, if needed. + /// + private WebpLosslessDecoder LosslessDecoder { get; } + + /// + /// Gets 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; } + + /// + /// Decodes and filters the maybe compressed alpha data. + /// + public void Decode() + { + if (!this.Compressed) + { + Span dataSpan = this.Data.Memory.Span; + int pixelCount = this.Width * this.Height; + if (dataSpan.Length < pixelCount) + { + WebpThrowHelper.ThrowImageFormatException("not enough data in the ALPH chunk"); + } + + Span alphaSpan = this.Alpha.Memory.Span; + if (this.AlphaFilterType == WebpAlphaFilterType.None) + { + dataSpan.Slice(0, pixelCount).CopyTo(alphaSpan); + return; + } + + Span deltas = dataSpan; + Span dst = alphaSpan; + Span prev = default; + 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 + { + if (this.Use8BDecode) + { + this.LosslessDecoder.DecodeAlphaData(this); + } + else + { + this.LosslessDecoder.DecodeImageData(this.Vp8LDec, this.Vp8LDec.Pixels.Memory.Span); + this.ExtractAlphaRows(this.Vp8LDec); + } + } + } + + /// + /// 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 == 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; + } + + public void ExtractPalettedAlphaRows(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 = this.AlphaFilterType is WebpAlphaFilterType.None or WebpAlphaFilterType.Horizontal ? 0 : this.LastRow; + int firstRow = this.LastRow < topRow ? topRow : this.LastRow; + if (lastRow > firstRow) + { + // Special method for paletted alpha data. + Span output = this.Alpha.Memory.Span; + Span pixelData = this.Vp8LDec.Pixels.Memory.Span; + Span pixelDataAsBytes = MemoryMarshal.Cast(pixelData); + Span dst = output.Slice(this.Width * firstRow); + Span input = pixelDataAsBytes.Slice(this.Vp8LDec.Width * firstRow); + + if (this.Vp8LDec.Transforms.Count == 0 || this.Vp8LDec.Transforms[0].TransformType != Vp8LTransformType.ColorIndexingTransform) + { + WebpThrowHelper.ThrowImageFormatException("error while decoding alpha channel, expected color index transform data is missing"); + } + + Vp8LTransform transform = this.Vp8LDec.Transforms[0]; + ColorIndexInverseTransformAlpha(transform, firstRow, lastRow, input, dst); + this.AlphaApplyFilter(firstRow, lastRow, dst, this.Width); + } + + this.LastRow = lastRow; + } + + /// + /// Once the image-stream is decoded into ARGB color values, the transparency information will be extracted from the green channel of the ARGB quadruplet. + /// + /// The VP8L decoder. + private void ExtractAlphaRows(Vp8LDecoder dec) + { + int numRowsToProcess = dec.Height; + int width = dec.Width; + Span pixels = dec.Pixels.Memory.Span; + Span input = pixels; + Span output = this.Alpha.Memory.Span; + + // Extract alpha (which is stored in the green plane). + int pixelCount = width * numRowsToProcess; + WebpLosslessDecoder.ApplyInverseTransforms(dec, input, this.memoryAllocator); + ExtractGreen(input, output, pixelCount); + this.AlphaApplyFilter(0, numRowsToProcess, output, width); + } + + private static 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; + if (bitsPerPixel < 8) + { + int srcOffset = 0; + int dstOffset = 0; + 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) == 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 HorizontalUnfilter(Span prev, Span input, Span dst, int width) + { + byte pred = (byte)(prev == null ? 0 : prev[0]); + + for (int i = 0; i < width; i++) + { + byte val = (byte)(pred + input[i]); + pred = val; + dst[i] = val; + } + } + + 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 prev0 = prev[0]; + byte topLeft = prev0; + byte left = prev0; + for (int i = 0; i < width; i++) + { + byte top = prev[i]; + left = (byte)(input[i] + GradientPredictor(left, top, topLeft)); + topLeft = top; + dst[i] = left; + } + } + } + + /// + /// Row-processing for the special case when alpha data contains only one + /// transform (color indexing), and trivial non-green literals. + /// + /// The VP8L meta data. + /// True, if alpha channel needs one byte per pixel, otherwise 4. + private static bool Is8BOptimizable(Vp8LMetadata hdr) + { + if (hdr.ColorCacheSize > 0) + { + return false; + } + + for (int i = 0; i < hdr.NumHTreeGroups; i++) + { + List htrees = hdr.HTreeGroups[i].HTrees; + if (htrees[HuffIndex.Red][0].BitsUsed > 0) + { + return false; + } + + if (htrees[HuffIndex.Blue][0].BitsUsed > 0) + { + return false; + } + + if (htrees[HuffIndex.Alpha][0].BitsUsed > 0) + { + return false; + } + } + + return true; + } + + 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++; + } + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static byte GetAlphaValue(int val) => (byte)((val >> 8) & 0xff); + + [MethodImpl(InliningOptions.ShortMethod)] + private static int GradientPredictor(byte a, byte b, byte c) + { + int g = a + b - c; + return (g & ~0xff) == 0 ? g : g < 0 ? 0 : 255; // clip to 8bit. + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void ExtractGreen(Span argb, Span alpha, int size) + { + for (int i = 0; i < size; i++) + { + alpha[i] = (byte)(argb[i] >> 8); + } + } + + /// + public void Dispose() + { + this.Vp8LDec?.Dispose(); + this.Data.Dispose(); + this.Alpha?.Dispose(); + } + } +} diff --git a/src/ImageSharp/Formats/Webp/BitReader/BitReaderBase.cs b/src/ImageSharp/Formats/Webp/BitReader/BitReaderBase.cs new file mode 100644 index 000000000..f11f2a110 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/BitReader/BitReaderBase.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Webp.BitReader +{ + /// + /// Base class for VP8 and VP8L bitreader. + /// + internal abstract class BitReaderBase : IDisposable + { + private bool isDisposed; + + /// + /// Gets or sets the raw encoded image data. + /// + public IMemoryOwner 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) + { + this.Data = memoryAllocator.Allocate(bytesToRead); + Span dataSpan = this.Data.Memory.Span; + input.Read(dataSpan.Slice(0, bytesToRead), 0, bytesToRead); + } + + protected virtual void Dispose(bool disposing) + { + if (this.isDisposed) + { + return; + } + + if (disposing) + { + this.Data?.Dispose(); + } + + this.isDisposed = true; + } + + /// + public void Dispose() + { + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/ImageSharp/Formats/Webp/BitReader/Vp8BitReader.cs b/src/ImageSharp/Formats/Webp/BitReader/Vp8BitReader.cs new file mode 100644 index 000000000..abf44127a --- /dev/null +++ b/src/ImageSharp/Formats/Webp/BitReader/Vp8BitReader.cs @@ -0,0 +1,229 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Buffers; +using System.Buffers.Binary; +using System.IO; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Webp.BitReader +{ + /// + /// 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) + { + Guard.MustBeLessThan(imageDataSize, int.MaxValue, nameof(imageDataSize)); + + 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(IMemoryOwner imageData, uint partitionLength, int startPos = 0) + { + this.Data = imageData; + this.ImageDataSize = (uint)imageData.Memory.Length; + this.PartitionLength = partitionLength; + this.InitBitreader(partitionLength, startPos); + } + + public int Pos => (int)this.pos; + + public uint ImageDataSize { get; } + + public uint PartitionLength { get; } + + public uint Remaining { get; set; } + + [MethodImpl(InliningOptions.ShortMethod)] + 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 ^ Numerics.Log2(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 = (this.range + (uint)mask) | 1; + this.value -= ((split + 1) & mask) << pos; + + return (v ^ (int)mask) - (int)mask; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public bool ReadBool() => this.ReadValue(1) is 1; + + public uint ReadValue(int nBits) + { + Guard.MustBeGreaterThan(nBits, 0, nameof(nBits)); + Guard.MustBeLessThanOrEqualTo(nBits, 32, 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)); + Guard.MustBeLessThanOrEqualTo(nBits, 32, nameof(nBits)); + + int value = (int)this.ReadValue(nBits); + return this.ReadValue(1) != 0 ? -value : value; + } + + private void InitBitreader(uint size, int pos = 0) + { + long posPlusSize = pos + size; + 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)posPlusSize; + this.bufferMax = (uint)(size > 8 ? posPlusSize - 8 + 1 : pos); + + this.LoadNewBytes(); + } + + [MethodImpl(InliningOptions.ColdPath)] + private void LoadNewBytes() + { + if (this.pos < this.bufferMax) + { + ulong inBits = BinaryPrimitives.ReadUInt64LittleEndian(this.Data.Memory.Span.Slice((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.Memory.Span[(int)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. + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + 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; + } + } +} diff --git a/src/ImageSharp/Formats/Webp/BitReader/Vp8LBitReader.cs b/src/ImageSharp/Formats/Webp/BitReader/Vp8LBitReader.cs new file mode 100644 index 000000000..601336fa4 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/BitReader/Vp8LBitReader.cs @@ -0,0 +1,215 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Buffers; +using System.IO; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Webp.BitReader +{ + /// + /// 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(IMemoryOwner data) + { + this.Data = data; + this.len = data.Memory.Length; + this.value = 0; + this.bitPos = 0; + this.Eos = false; + + ulong currentValue = 0; + System.Span dataSpan = this.Data.Memory.Span; + for (int i = 0; i < 8; i++) + { + currentValue |= (ulong)dataSpan[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; + System.Span dataSpan = this.Data.Memory.Span; + for (int i = 0; i < length; i++) + { + currentValue |= (ulong)dataSpan[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]; + this.bitPos += nBits; + 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. + [MethodImpl(InliningOptions.ShortMethod)] + 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. + [MethodImpl(InliningOptions.ShortMethod)] + public void AdvanceBitPosition(int numberOfBits) => this.bitPos += numberOfBits; + + /// + /// Return the pre-fetched bits, so they can be looked up. + /// + /// The pre-fetched bits. + [MethodImpl(InliningOptions.ShortMethod)] + public ulong PrefetchBits() => 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() => this.Eos || ((this.pos == this.len) && (this.bitPos > Lbits)); + + [MethodImpl(InliningOptions.ShortMethod)] + private void DoFillBitWindow() => this.ShiftBytes(); + + /// + /// If not at EOS, reload up to Vp8LLbits byte-by-byte. + /// + private void ShiftBytes() + { + System.Span dataSpan = this.Data.Memory.Span; + while (this.bitPos >= 8 && this.pos < this.len) + { + this.value >>= 8; + this.value |= (ulong)dataSpan[(int)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/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs new file mode 100644 index 000000000..41623f287 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -0,0 +1,148 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers.Binary; +using System.IO; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; + +namespace SixLabors.ImageSharp.Formats.Webp.BitWriter +{ + internal abstract class BitWriterBase + { + /// + /// Buffer to write to. + /// + private byte[] buffer; + + /// + /// Initializes a new instance of the class. + /// + /// The expected size in bytes. + protected BitWriterBase(int expectedSize) => this.buffer = new byte[expectedSize]; + + /// + /// Initializes a new instance of the class. + /// Used internally for cloning. + /// + private protected BitWriterBase(byte[] buffer) => this.buffer = buffer; + + public byte[] Buffer => this.buffer; + + /// + /// Writes the encoded bytes of the image to the stream. Call Finish() before this. + /// + /// The stream to write to. + public void WriteToStream(Stream stream) => stream.Write(this.Buffer.AsSpan(0, this.NumBytes())); + + /// + /// Resizes the buffer to write to. + /// + /// The extra size in bytes needed. + public abstract void BitWriterResize(int extraSize); + + /// + /// Returns the number of bytes of the encoded image data. + /// + /// The number of bytes of the image data. + public abstract int NumBytes(); + + /// + /// Flush leftover bits. + /// + public abstract void Finish(); + + /// + /// Writes the encoded image to the stream. + /// + /// The stream to write to. + /// The exif profile. + /// The width of the image. + /// The height of the image. + public abstract void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, uint width, uint height); + + protected void ResizeBuffer(int maxBytes, int sizeRequired) + { + int newSize = (3 * maxBytes) >> 1; + if (newSize < sizeRequired) + { + newSize = sizeRequired; + } + + // Make new size multiple of 1k. + newSize = ((newSize >> 10) + 1) << 10; + Array.Resize(ref this.buffer, newSize); + } + + /// + /// Writes the RIFF header to the stream. + /// + /// The stream to write to. + /// The block length. + protected void WriteRiffHeader(Stream stream, uint riffSize) + { + Span buf = stackalloc byte[4]; + stream.Write(WebpConstants.RiffFourCc); + BinaryPrimitives.WriteUInt32LittleEndian(buf, riffSize); + stream.Write(buf); + stream.Write(WebpConstants.WebpHeader); + } + + /// + /// Writes the Exif profile to the stream. + /// + /// The stream to write to. + /// The exif profile bytes. + protected void WriteExifProfile(Stream stream, byte[] exifBytes) + { + DebugGuard.NotNull(exifBytes, nameof(exifBytes)); + + Span buf = stackalloc byte[4]; + BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Exif); + stream.Write(buf); + BinaryPrimitives.WriteUInt32LittleEndian(buf, (uint)exifBytes.Length); + stream.Write(buf); + stream.Write(exifBytes); + } + + /// + /// Writes a VP8X header to the stream. + /// + /// The stream to write to. + /// A exif profile or null, if it does not exist. + /// The width of the image. + /// The height of the image. + protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, uint width, uint height) + { + int maxDimension = 16777215; + if (width > maxDimension || height > maxDimension) + { + WebpThrowHelper.ThrowInvalidImageDimensions($"Image width or height exceeds maximum allowed dimension of {maxDimension}"); + } + + // The spec states that the product of Canvas Width and Canvas Height MUST be at most 2^32 - 1. + if (width * height > 4294967295ul) + { + WebpThrowHelper.ThrowInvalidImageDimensions("The product of image width and height MUST be at most 2^32 - 1"); + } + + uint flags = 0; + if (exifProfile != null) + { + // Set exif bit. + flags |= 8; + } + + Span buf = stackalloc byte[4]; + stream.Write(WebpConstants.Vp8XMagicBytes); + BinaryPrimitives.WriteUInt32LittleEndian(buf, WebpConstants.Vp8XChunkSize); + stream.Write(buf); + BinaryPrimitives.WriteUInt32LittleEndian(buf, flags); + stream.Write(buf); + BinaryPrimitives.WriteUInt32LittleEndian(buf, width - 1); + stream.Write(buf.Slice(0, 3)); + BinaryPrimitives.WriteUInt32LittleEndian(buf, height - 1); + stream.Write(buf.Slice(0, 3)); + } + } +} diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs new file mode 100644 index 000000000..7628247fd --- /dev/null +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -0,0 +1,674 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers.Binary; +using System.IO; +using SixLabors.ImageSharp.Formats.Webp.Lossy; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; + +namespace SixLabors.ImageSharp.Formats.Webp.BitWriter +{ + /// + /// A bit writer for writing lossy webp streams. + /// + internal class Vp8BitWriter : BitWriterBase + { +#pragma warning disable SA1310 // Field names should not contain underscore + private const int DC_PRED = 0; + private const int TM_PRED = 1; + private const int V_PRED = 2; + private const int H_PRED = 3; + + // 4x4 modes + private const int B_DC_PRED = 0; + private const int B_TM_PRED = 1; + private const int B_VE_PRED = 2; + private const int B_HE_PRED = 3; + private const int B_RD_PRED = 4; + private const int B_VR_PRED = 5; + private const int B_LD_PRED = 6; + private const int B_VL_PRED = 7; + private const int B_HD_PRED = 8; + private const int B_HU_PRED = 9; +#pragma warning restore SA1310 // Field names should not contain underscore + + private readonly Vp8Encoder enc; + + private int range; + + private int value; + + /// + /// Number of outstanding bits. + /// + private int run; + + /// + /// Number of pending bits. + /// + private int nbBits; + + private uint pos; + + private readonly int maxPos; + + /// + /// Initializes a new instance of the class. + /// + /// The expected size in bytes. + public Vp8BitWriter(int expectedSize) + : base(expectedSize) + { + this.range = 255 - 1; + this.value = 0; + this.run = 0; + this.nbBits = -8; + this.pos = 0; + this.maxPos = 0; + } + + /// + /// Initializes a new instance of the class. + /// + /// The expected size in bytes. + /// The Vp8Encoder. + public Vp8BitWriter(int expectedSize, Vp8Encoder enc) + : this(expectedSize) => this.enc = enc; + + /// + public override int NumBytes() => (int)this.pos; + + public int PutCoeffs(int ctx, Vp8Residual residual) + { + int n = residual.First; + Vp8ProbaArray p = residual.Prob[n].Probabilities[ctx]; + if (!this.PutBit(residual.Last >= 0, p.Probabilities[0])) + { + return 0; + } + + while (n < 16) + { + int c = residual.Coeffs[n++]; + bool sign = c < 0; + int v = sign ? -c : c; + if (!this.PutBit(v != 0, p.Probabilities[1])) + { + p = residual.Prob[WebpConstants.Vp8EncBands[n]].Probabilities[0]; + continue; + } + + if (!this.PutBit(v > 1, p.Probabilities[2])) + { + p = residual.Prob[WebpConstants.Vp8EncBands[n]].Probabilities[1]; + } + else + { + if (!this.PutBit(v > 4, p.Probabilities[3])) + { + if (this.PutBit(v != 2, p.Probabilities[4])) + { + this.PutBit(v == 4, p.Probabilities[5]); + } + } + else if (!this.PutBit(v > 10, p.Probabilities[6])) + { + if (!this.PutBit(v > 6, p.Probabilities[7])) + { + this.PutBit(v == 6, 159); + } + else + { + this.PutBit(v >= 9, 165); + this.PutBit(!((v & 1) != 0), 145); + } + } + else + { + int mask; + byte[] tab; + if (v < 3 + (8 << 1)) + { + // VP8Cat3 (3b) + this.PutBit(0, p.Probabilities[8]); + this.PutBit(0, p.Probabilities[9]); + v -= 3 + (8 << 0); + mask = 1 << 2; + tab = WebpConstants.Cat3; + } + else if (v < 3 + (8 << 2)) + { + // VP8Cat4 (4b) + this.PutBit(0, p.Probabilities[8]); + this.PutBit(1, p.Probabilities[9]); + v -= 3 + (8 << 1); + mask = 1 << 3; + tab = WebpConstants.Cat4; + } + else if (v < 3 + (8 << 3)) + { + // VP8Cat5 (5b) + this.PutBit(1, p.Probabilities[8]); + this.PutBit(0, p.Probabilities[10]); + v -= 3 + (8 << 2); + mask = 1 << 4; + tab = WebpConstants.Cat5; + } + else + { + // VP8Cat6 (11b) + this.PutBit(1, p.Probabilities[8]); + this.PutBit(1, p.Probabilities[10]); + v -= 3 + (8 << 3); + mask = 1 << 10; + tab = WebpConstants.Cat6; + } + + int tabIdx = 0; + while (mask != 0) + { + this.PutBit(v & mask, tab[tabIdx++]); + mask >>= 1; + } + } + + p = residual.Prob[WebpConstants.Vp8EncBands[n]].Probabilities[2]; + } + + this.PutBitUniform(sign ? 1 : 0); + if (n == 16 || !this.PutBit(n <= residual.Last, p.Probabilities[0])) + { + return 1; // EOB + } + } + + return 1; + } + + /// + /// Resizes the buffer to write to. + /// + /// The extra size in bytes needed. + public override void BitWriterResize(int extraSize) + { + long neededSize = this.pos + extraSize; + if (neededSize <= this.maxPos) + { + return; + } + + this.ResizeBuffer(this.maxPos, (int)neededSize); + } + + /// + public override void Finish() + { + this.PutBits(0, 9 - this.nbBits); + this.nbBits = 0; // pad with zeroes. + this.Flush(); + } + + public void PutSegment(int s, Span p) + { + if (this.PutBit(s >= 2, p[0])) + { + p = p.Slice(1); + } + + this.PutBit(s & 1, p[1]); + } + + public void PutI16Mode(int mode) + { + if (this.PutBit(mode is TM_PRED or H_PRED, 156)) + { + this.PutBit(mode == TM_PRED, 128); // TM or HE + } + else + { + this.PutBit(mode == V_PRED, 163); // VE or DC + } + } + + public int PutI4Mode(int mode, Span prob) + { + if (this.PutBit(mode != B_DC_PRED, prob[0])) + { + if (this.PutBit(mode != B_TM_PRED, prob[1])) + { + if (this.PutBit(mode != B_VE_PRED, prob[2])) + { + if (!this.PutBit(mode >= B_LD_PRED, prob[3])) + { + if (this.PutBit(mode != B_HE_PRED, prob[4])) + { + this.PutBit(mode != B_RD_PRED, prob[5]); + } + } + else + { + if (this.PutBit(mode != B_LD_PRED, prob[6])) + { + if (this.PutBit(mode != B_VL_PRED, prob[7])) + { + this.PutBit(mode != B_HD_PRED, prob[8]); + } + } + } + } + } + } + + return mode; + } + + public void PutUvMode(int uvMode) + { + // DC_PRED + if (this.PutBit(uvMode != DC_PRED, 142)) + { + // V_PRED + if (this.PutBit(uvMode != V_PRED, 114)) + { + // H_PRED + this.PutBit(uvMode != H_PRED, 183); + } + } + } + + private void PutBits(uint value, int nbBits) + { + for (uint mask = 1u << (nbBits - 1); mask != 0; mask >>= 1) + { + this.PutBitUniform((int)(value & mask)); + } + } + + private bool PutBit(bool bit, int prob) => this.PutBit(bit ? 1 : 0, prob); + + private bool PutBit(int bit, int prob) + { + int split = (this.range * prob) >> 8; + if (bit != 0) + { + this.value += split + 1; + this.range -= split + 1; + } + else + { + this.range = split; + } + + if (this.range < 127) + { + // emit 'shift' bits out and renormalize. + int shift = WebpLookupTables.Norm[this.range]; + this.range = WebpLookupTables.NewRange[this.range]; + this.value <<= shift; + this.nbBits += shift; + if (this.nbBits > 0) + { + this.Flush(); + } + } + + return bit != 0; + } + + private int PutBitUniform(int bit) + { + int split = this.range >> 1; + if (bit != 0) + { + this.value += split + 1; + this.range -= split + 1; + } + else + { + this.range = split; + } + + if (this.range < 127) + { + this.range = WebpLookupTables.NewRange[this.range]; + this.value <<= 1; + this.nbBits += 1; + if (this.nbBits > 0) + { + this.Flush(); + } + } + + return bit; + } + + private void PutSignedBits(int value, int nbBits) + { + if (this.PutBitUniform(value != 0 ? 1 : 0) == 0) + { + return; + } + + if (value < 0) + { + int valueToWrite = (-value << 1) | 1; + this.PutBits((uint)valueToWrite, nbBits + 1); + } + else + { + this.PutBits((uint)(value << 1), nbBits + 1); + } + } + + private void Flush() + { + int s = 8 + this.nbBits; + int bits = this.value >> s; + this.value -= bits << s; + this.nbBits -= 8; + if ((bits & 0xff) != 0xff) + { + uint pos = this.pos; + this.BitWriterResize(this.run + 1); + + if ((bits & 0x100) != 0) + { + // overflow -> propagate carry over pending 0xff's + if (pos > 0) + { + this.Buffer[pos - 1]++; + } + } + + if (this.run > 0) + { + int value = (bits & 0x100) != 0 ? 0x00 : 0xff; + for (; this.run > 0; --this.run) + { + this.Buffer[pos++] = (byte)value; + } + } + + this.Buffer[pos++] = (byte)(bits & 0xff); + this.pos = pos; + } + else + { + this.run++; // Delay writing of bytes 0xff, pending eventual carry. + } + } + + /// + public override void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, uint width, uint height) + { + bool isVp8X = false; + byte[] exifBytes = null; + uint riffSize = 0; + if (exifProfile != null) + { + isVp8X = true; + riffSize += WebpConstants.ChunkHeaderSize + WebpConstants.Vp8XChunkSize; + exifBytes = exifProfile.ToByteArray(); + riffSize += WebpConstants.ChunkHeaderSize + (uint)exifBytes.Length; + } + + this.Finish(); + uint numBytes = (uint)this.NumBytes(); + int mbSize = this.enc.Mbw * this.enc.Mbh; + int expectedSize = mbSize * 7 / 8; + + var bitWriterPartZero = new Vp8BitWriter(expectedSize); + + // Partition #0 with header and partition sizes + uint size0 = this.GeneratePartition0(bitWriterPartZero); + + uint vp8Size = WebpConstants.Vp8FrameHeaderSize + size0; + vp8Size += numBytes; + uint pad = vp8Size & 1; + vp8Size += pad; + + // Compute RIFF size + // At the minimum it is: "WEBPVP8 nnnn" + VP8 data size. + riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + vp8Size; + + // Emit headers and partition #0 + this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile); + bitWriterPartZero.WriteToStream(stream); + + // Write the encoded image to the stream. + this.WriteToStream(stream); + if (pad == 1) + { + stream.WriteByte(0); + } + + if (exifProfile != null) + { + this.WriteExifProfile(stream, exifBytes); + } + } + + private uint GeneratePartition0(Vp8BitWriter bitWriter) + { + bitWriter.PutBitUniform(0); // colorspace + bitWriter.PutBitUniform(0); // clamp type + + this.WriteSegmentHeader(bitWriter); + this.WriteFilterHeader(bitWriter); + + bitWriter.PutBits(0, 2); + + this.WriteQuant(bitWriter); + bitWriter.PutBitUniform(0); + this.WriteProbas(bitWriter); + this.CodeIntraModes(bitWriter); + + bitWriter.Finish(); + + return (uint)bitWriter.NumBytes(); + } + + private void WriteSegmentHeader(Vp8BitWriter bitWriter) + { + Vp8EncSegmentHeader hdr = this.enc.SegmentHeader; + Vp8EncProba proba = this.enc.Proba; + if (bitWriter.PutBitUniform(hdr.NumSegments > 1 ? 1 : 0) != 0) + { + // We always 'update' the quant and filter strength values. + int updateData = 1; + bitWriter.PutBitUniform(hdr.UpdateMap ? 1 : 0); + if (bitWriter.PutBitUniform(updateData) != 0) + { + // We always use absolute values, not relative ones. + bitWriter.PutBitUniform(1); // (segment_feature_mode = 1. Paragraph 9.3.) + for (int s = 0; s < WebpConstants.NumMbSegments; ++s) + { + bitWriter.PutSignedBits(this.enc.SegmentInfos[s].Quant, 7); + } + + for (int s = 0; s < WebpConstants.NumMbSegments; ++s) + { + bitWriter.PutSignedBits(this.enc.SegmentInfos[s].FStrength, 6); + } + } + + if (hdr.UpdateMap) + { + for (int s = 0; s < 3; ++s) + { + if (bitWriter.PutBitUniform(proba.Segments[s] != 255 ? 1 : 0) != 0) + { + bitWriter.PutBits(proba.Segments[s], 8); + } + } + } + } + } + + private void WriteFilterHeader(Vp8BitWriter bitWriter) + { + Vp8FilterHeader hdr = this.enc.FilterHeader; + bool useLfDelta = hdr.I4x4LfDelta != 0; + bitWriter.PutBitUniform(hdr.Simple ? 1 : 0); + bitWriter.PutBits((uint)hdr.FilterLevel, 6); + bitWriter.PutBits((uint)hdr.Sharpness, 3); + if (bitWriter.PutBitUniform(useLfDelta ? 1 : 0) != 0) + { + // '0' is the default value for i4x4LfDelta at frame #0. + bool needUpdate = hdr.I4x4LfDelta != 0; + if (bitWriter.PutBitUniform(needUpdate ? 1 : 0) != 0) + { + // we don't use refLfDelta => emit four 0 bits. + bitWriter.PutBits(0, 4); + + // we use modeLfDelta for i4x4 + bitWriter.PutSignedBits(hdr.I4x4LfDelta, 6); + bitWriter.PutBits(0, 3); // all others unused. + } + } + } + + // Nominal quantization parameters + private void WriteQuant(Vp8BitWriter bitWriter) + { + bitWriter.PutBits((uint)this.enc.BaseQuant, 7); + bitWriter.PutSignedBits(this.enc.DqY1Dc, 4); + bitWriter.PutSignedBits(this.enc.DqY2Dc, 4); + bitWriter.PutSignedBits(this.enc.DqY2Ac, 4); + bitWriter.PutSignedBits(this.enc.DqUvDc, 4); + bitWriter.PutSignedBits(this.enc.DqUvAc, 4); + } + + private void WriteProbas(Vp8BitWriter bitWriter) + { + Vp8EncProba probas = this.enc.Proba; + 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 p0 = probas.Coeffs[t][b].Probabilities[c].Probabilities[p]; + bool update = p0 != WebpLookupTables.DefaultCoeffsProba[t, b, c, p]; + if (bitWriter.PutBit(update, WebpLookupTables.CoeffsUpdateProba[t, b, c, p])) + { + bitWriter.PutBits(p0, 8); + } + } + } + } + } + + if (bitWriter.PutBitUniform(probas.UseSkipProba ? 1 : 0) != 0) + { + bitWriter.PutBits(probas.SkipProba, 8); + } + } + + // Writes the partition #0 modes (that is: all intra modes) + private void CodeIntraModes(Vp8BitWriter bitWriter) + { + var it = new Vp8EncIterator(this.enc.YTop, this.enc.UvTop, this.enc.Nz, this.enc.MbInfo, this.enc.Preds, this.enc.TopDerr, this.enc.Mbw, this.enc.Mbh); + int predsWidth = this.enc.PredsWidth; + + do + { + Vp8MacroBlockInfo mb = it.CurrentMacroBlockInfo; + int predIdx = it.PredIdx; + Span preds = it.Preds.AsSpan(predIdx); + if (this.enc.SegmentHeader.UpdateMap) + { + bitWriter.PutSegment(mb.Segment, this.enc.Proba.Segments); + } + + if (this.enc.Proba.UseSkipProba) + { + bitWriter.PutBit(mb.Skip, this.enc.Proba.SkipProba); + } + + if (bitWriter.PutBit(mb.MacroBlockType != 0, 145)) + { + // i16x16 + bitWriter.PutI16Mode(preds[0]); + } + else + { + Span topPred = it.Preds.AsSpan(predIdx - predsWidth); + for (int y = 0; y < 4; y++) + { + int left = it.Preds[predIdx - 1]; + for (int x = 0; x < 4; x++) + { + byte[] probas = WebpLookupTables.ModesProba[topPred[x], left]; + left = bitWriter.PutI4Mode(it.Preds[predIdx + x], probas); + } + + topPred = it.Preds.AsSpan(predIdx); + predIdx += predsWidth; + } + } + + bitWriter.PutUvMode(mb.UvMode); + } + while (it.Next()); + } + + private void WriteWebpHeaders(Stream stream, uint size0, uint vp8Size, uint riffSize, bool isVp8X, uint width, uint height, ExifProfile exifProfile) + { + this.WriteRiffHeader(stream, riffSize); + + // Write VP8X, header if necessary. + if (isVp8X) + { + this.WriteVp8XHeader(stream, exifProfile, width, height); + } + + this.WriteVp8Header(stream, vp8Size); + this.WriteFrameHeader(stream, size0); + } + + private void WriteVp8Header(Stream stream, uint size) + { + Span vp8ChunkHeader = stackalloc byte[WebpConstants.ChunkHeaderSize]; + + WebpConstants.Vp8MagicBytes.AsSpan().CopyTo(vp8ChunkHeader); + BinaryPrimitives.WriteUInt32LittleEndian(vp8ChunkHeader.Slice(4), size); + + stream.Write(vp8ChunkHeader); + } + + private void WriteFrameHeader(Stream stream, uint size0) + { + uint profile = 0; + int width = this.enc.Width; + int height = this.enc.Height; + byte[] vp8FrameHeader = new byte[WebpConstants.Vp8FrameHeaderSize]; + + // Paragraph 9.1. + uint bits = 0 // keyframe (1b) + | (profile << 1) // profile (3b) + | (1 << 4) // visible (1b) + | (size0 << 5); // partition length (19b) + + vp8FrameHeader[0] = (byte)((bits >> 0) & 0xff); + vp8FrameHeader[1] = (byte)((bits >> 8) & 0xff); + vp8FrameHeader[2] = (byte)((bits >> 16) & 0xff); + + // signature + vp8FrameHeader[3] = WebpConstants.Vp8HeaderMagicBytes[0]; + vp8FrameHeader[4] = WebpConstants.Vp8HeaderMagicBytes[1]; + vp8FrameHeader[5] = WebpConstants.Vp8HeaderMagicBytes[2]; + + // dimensions + vp8FrameHeader[6] = (byte)(width & 0xff); + vp8FrameHeader[7] = (byte)(width >> 8); + vp8FrameHeader[8] = (byte)(height & 0xff); + vp8FrameHeader[9] = (byte)(height >> 8); + + stream.Write(vp8FrameHeader); + } + } +} diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs new file mode 100644 index 000000000..2f942231f --- /dev/null +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -0,0 +1,212 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers.Binary; +using System.IO; +using SixLabors.ImageSharp.Formats.Webp.Lossless; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; + +namespace SixLabors.ImageSharp.Formats.Webp.BitWriter +{ + /// + /// A bit writer for writing lossless webp streams. + /// + internal class Vp8LBitWriter : BitWriterBase + { + /// + /// A scratch buffer to reduce allocations. + /// + private readonly byte[] scratchBuffer = new byte[8]; + + /// + /// This is the minimum amount of size the memory buffer is guaranteed to grow when extra space is needed. + /// + private const int MinExtraSize = 32768; + + private const int WriterBytes = 4; + + private const int WriterBits = 32; + + /// + /// Bit accumulator. + /// + private ulong bits; + + /// + /// Number of bits used in accumulator. + /// + private int used; + + /// + /// Current write position. + /// + private int cur; + + /// + /// Initializes a new instance of the class. + /// + /// The expected size in bytes. + public Vp8LBitWriter(int expectedSize) + : base(expectedSize) + { + } + + /// + /// Initializes a new instance of the class. + /// Used internally for cloning. + /// + private Vp8LBitWriter(byte[] buffer, ulong bits, int used, int cur) + : base(buffer) + { + this.bits = bits; + this.used = used; + this.cur = cur; + } + + /// + /// This function writes bits into bytes in increasing addresses (little endian), + /// and within a byte least-significant-bit first. This function can write up to 32 bits in one go. + /// + public void PutBits(uint bits, int nBits) + { + if (nBits > 0) + { + if (this.used >= 32) + { + this.PutBitsFlushBits(); + } + + this.bits |= (ulong)bits << this.used; + this.used += nBits; + } + } + + public void Reset(Vp8LBitWriter bwInit) + { + this.bits = bwInit.bits; + this.used = bwInit.used; + this.cur = bwInit.cur; + } + + public void WriteHuffmanCode(HuffmanTreeCode code, int codeIndex) + { + int depth = code.CodeLengths[codeIndex]; + int symbol = code.Codes[codeIndex]; + this.PutBits((uint)symbol, depth); + } + + public void WriteHuffmanCodeWithExtraBits(HuffmanTreeCode code, int codeIndex, int bits, int nBits) + { + int depth = code.CodeLengths[codeIndex]; + int symbol = code.Codes[codeIndex]; + this.PutBits((uint)((bits << depth) | symbol), depth + nBits); + } + + /// + public override int NumBytes() => this.cur + ((this.used + 7) >> 3); + + public Vp8LBitWriter Clone() + { + byte[] clonedBuffer = new byte[this.Buffer.Length]; + System.Buffer.BlockCopy(this.Buffer, 0, clonedBuffer, 0, this.cur); + return new Vp8LBitWriter(clonedBuffer, this.bits, this.used, this.cur); + } + + /// + public override void Finish() + { + this.BitWriterResize((this.used + 7) >> 3); + while (this.used > 0) + { + this.Buffer[this.cur++] = (byte)this.bits; + this.bits >>= 8; + this.used -= 8; + } + + this.used = 0; + } + + /// + public override void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, uint width, uint height) + { + Span buffer = stackalloc byte[4]; + bool isVp8X = false; + byte[] exifBytes = null; + uint riffSize = 0; + if (exifProfile != null) + { + isVp8X = true; + riffSize += WebpConstants.ChunkHeaderSize + WebpConstants.Vp8XChunkSize; + exifBytes = exifProfile.ToByteArray(); + riffSize += WebpConstants.ChunkHeaderSize + (uint)exifBytes.Length; + } + + this.Finish(); + uint size = (uint)this.NumBytes(); + size++; // One byte extra for the VP8L signature. + + // Write RIFF header. + uint pad = size & 1; + riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + size + pad; + this.WriteRiffHeader(stream, riffSize); + + // Write VP8X, header if necessary. + if (isVp8X) + { + this.WriteVp8XHeader(stream, exifProfile, width, height); + } + + // Write magic bytes indicating its a lossless webp. + stream.Write(WebpConstants.Vp8LMagicBytes); + + // Write Vp8 Header. + BinaryPrimitives.WriteUInt32LittleEndian(buffer, size); + stream.Write(buffer); + stream.WriteByte(WebpConstants.Vp8LHeaderMagicByte); + + // Write the encoded bytes of the image to the stream. + this.WriteToStream(stream); + if (pad == 1) + { + stream.WriteByte(0); + } + + if (exifProfile != null) + { + this.WriteExifProfile(stream, exifBytes); + } + } + + /// + /// Internal function for PutBits flushing 32 bits from the written state. + /// + private void PutBitsFlushBits() + { + // If needed, make some room by flushing some bits out. + if (this.cur + WriterBytes > this.Buffer.Length) + { + int extraSize = this.Buffer.Length - this.cur + MinExtraSize; + this.BitWriterResize(extraSize); + } + + BinaryPrimitives.WriteUInt64LittleEndian(this.scratchBuffer, this.bits); + this.scratchBuffer.AsSpan(0, 4).CopyTo(this.Buffer.AsSpan(this.cur)); + + this.cur += WriterBytes; + this.bits >>= WriterBits; + this.used -= WriterBits; + } + + /// + /// Resizes the buffer to write to. + /// + /// The extra size in bytes needed. + public override void BitWriterResize(int extraSize) + { + int maxBytes = this.Buffer.Length + this.Buffer.Length; + int sizeRequired = this.cur + extraSize; + this.ResizeBuffer(maxBytes, sizeRequired); + } + } +} diff --git a/src/ImageSharp/Formats/Webp/EntropyIx.cs b/src/ImageSharp/Formats/Webp/EntropyIx.cs new file mode 100644 index 000000000..c72ddeb42 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/EntropyIx.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp +{ + /// + /// These five modes are evaluated and their respective entropy is computed. + /// + internal enum EntropyIx + { + Direct = 0, + + Spatial = 1, + + SubGreen = 2, + + SpatialSubGreen = 3, + + Palette = 4, + + PaletteAndSpatial = 5, + + NumEntropyIx = 6 + } +} diff --git a/src/ImageSharp/Formats/Webp/HistoIx.cs b/src/ImageSharp/Formats/Webp/HistoIx.cs new file mode 100644 index 000000000..68b00394b --- /dev/null +++ b/src/ImageSharp/Formats/Webp/HistoIx.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp +{ + internal enum HistoIx + { + HistoAlpha = 0, + + HistoAlphaPred, + + HistoGreen, + + HistoGreenPred, + + HistoRed, + + HistoRedPred, + + HistoBlue, + + HistoBluePred, + + HistoRedSubGreen, + + HistoRedPredSubGreen, + + HistoBlueSubGreen, + + HistoBluePredSubGreen, + + HistoPalette, + + HistoTotal + } +} diff --git a/src/ImageSharp/Formats/Webp/IWebpDecoderOptions.cs b/src/ImageSharp/Formats/Webp/IWebpDecoderOptions.cs new file mode 100644 index 000000000..7bd78da3d --- /dev/null +++ b/src/ImageSharp/Formats/Webp/IWebpDecoderOptions.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp +{ + /// + /// Image decoder options 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/IWebpEncoderOptions.cs b/src/ImageSharp/Formats/Webp/IWebpEncoderOptions.cs new file mode 100644 index 000000000..7dbf49d45 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/IWebpEncoderOptions.cs @@ -0,0 +1,77 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp +{ + /// + /// Configuration options for use during webp encoding. + /// + internal interface IWebpEncoderOptions + { + /// + /// Gets the webp file format used. Either lossless or lossy. + /// + WebpFileFormatType? FileFormat { get; } + + /// + /// Gets the compression quality. Between 0 and 100. + /// For lossy, 0 gives the smallest size and 100 the largest. For lossless, + /// this parameter is the amount of effort put into the compression: 0 is the fastest but gives larger + /// files compared to the slowest, but best, 100. + /// Defaults to 75. + /// + int Quality { get; } + + /// + /// Gets the encoding method to use. Its a quality/speed trade-off (0=fast, 6=slower-better). + /// Defaults to 4. + /// + WebpEncodingMethod Method { get; } + + /// + /// Gets a value indicating whether the alpha plane should be compressed with Webp lossless format. + /// + bool UseAlphaCompression { get; } + + /// + /// Gets the number of entropy-analysis passes (in [1..10]). + /// + int EntropyPasses { get; } + + /// + /// Gets the amplitude of the spatial noise shaping. Spatial noise shaping (or sns for short) refers to a general collection of built-in algorithms + /// used to decide which area of the picture should use relatively less bits, and where else to better transfer these bits. + /// The possible range goes from 0 (algorithm is off) to 100 (the maximal effect). + /// Defaults to 50. + /// + int SpatialNoiseShaping { get; } + + /// + /// Gets the strength of the deblocking filter, between 0 (no filtering) and 100 (maximum filtering). + /// A value of 0 will turn off any filtering. Higher value will increase the strength of the filtering process applied after decoding the picture. + /// The higher the value the smoother the picture will appear. + /// Typical values are usually in the range of 20 to 50. + /// Defaults to 60. + /// + int FilterStrength { get; } + + /// + /// Gets a value indicating whether to preserve the exact RGB values under transparent area. Otherwise, discard this invisible + /// RGB information for better compression. + /// The default value is Clear. + /// + WebpTransparentColorMode TransparentColorMode { get; } + + /// + /// Gets a value indicating whether near lossless mode should be used. + /// This option adjusts pixel values to help compressibility, but has minimal impact on the visual quality. + /// + bool NearLossless { get; } + + /// + /// Gets the quality of near-lossless image preprocessing. The range is 0 (maximum preprocessing) to 100 (no preprocessing, the default). + /// The typical value is around 60. Note that lossy with -q 100 can at times yield better results. + /// + int NearLosslessQuality { get; } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs new file mode 100644 index 000000000..70c4efb99 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs @@ -0,0 +1,854 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + internal class BackwardReferenceEncoder + { + /// + /// Maximum bit length. + /// + public const int MaxLengthBits = 12; + + private const float MaxEntropy = 1e30f; + + private const int WindowOffsetsSizeMax = 32; + + /// + /// We want the max value to be attainable and stored in MaxLengthBits bits. + /// + public const int MaxLength = (1 << MaxLengthBits) - 1; + + /// + /// Minimum number of pixels for which it is cheaper to encode a + /// distance + length instead of each pixel as a literal. + /// + private const int MinLength = 4; + + /// + /// Evaluates best possible backward references for specified quality. The input cacheBits to 'GetBackwardReferences' + /// sets the maximum cache bits to use (passing 0 implies disabling the local color cache). + /// The optimal cache bits is evaluated and set for the cacheBits parameter. + /// The return value is the pointer to the best of the two backward refs viz, refs[0] or refs[1]. + /// + public static Vp8LBackwardRefs GetBackwardReferences( + int width, + int height, + ReadOnlySpan bgra, + int quality, + int lz77TypesToTry, + ref int cacheBits, + Vp8LHashChain hashChain, + Vp8LBackwardRefs best, + Vp8LBackwardRefs worst) + { + int lz77TypeBest = 0; + double bitCostBest = -1; + int cacheBitsInitial = cacheBits; + Vp8LHashChain hashChainBox = null; + for (int lz77Type = 1; lz77TypesToTry > 0; lz77TypesToTry &= ~lz77Type, lz77Type <<= 1) + { + int cacheBitsTmp = cacheBitsInitial; + if ((lz77TypesToTry & lz77Type) == 0) + { + continue; + } + + switch ((Vp8LLz77Type)lz77Type) + { + case Vp8LLz77Type.Lz77Rle: + BackwardReferencesRle(width, height, bgra, 0, worst); + break; + case Vp8LLz77Type.Lz77Standard: + // Compute LZ77 with no cache (0 bits), as the ideal LZ77 with a color cache is not that different in practice. + BackwardReferencesLz77(width, height, bgra, 0, hashChain, worst); + break; + case Vp8LLz77Type.Lz77Box: + hashChainBox = new Vp8LHashChain(width * height); + BackwardReferencesLz77Box(width, height, bgra, 0, hashChain, hashChainBox, worst); + break; + } + + // Next, try with a color cache and update the references. + cacheBitsTmp = CalculateBestCacheSize(bgra, quality, worst, cacheBitsTmp); + if (cacheBitsTmp > 0) + { + BackwardRefsWithLocalCache(bgra, cacheBitsTmp, worst); + } + + // Keep the best backward references. + var histo = new Vp8LHistogram(worst, cacheBitsTmp); + double bitCost = histo.EstimateBits(); + + if (lz77TypeBest == 0 || bitCost < bitCostBest) + { + Vp8LBackwardRefs tmp = worst; + worst = best; + best = tmp; + bitCostBest = bitCost; + cacheBits = cacheBitsTmp; + lz77TypeBest = lz77Type; + } + } + + // Improve on simple LZ77 but only for high quality (TraceBackwards is costly). + if ((lz77TypeBest == (int)Vp8LLz77Type.Lz77Standard || lz77TypeBest == (int)Vp8LLz77Type.Lz77Box) && quality >= 25) + { + Vp8LHashChain hashChainTmp = lz77TypeBest == (int)Vp8LLz77Type.Lz77Standard ? hashChain : hashChainBox; + BackwardReferencesTraceBackwards(width, height, bgra, cacheBits, hashChainTmp, best, worst); + var histo = new Vp8LHistogram(worst, cacheBits); + double bitCostTrace = histo.EstimateBits(); + if (bitCostTrace < bitCostBest) + { + best = worst; + } + } + + BackwardReferences2DLocality(width, best); + + return best; + } + + /// + /// Evaluate optimal cache bits for the local color cache. + /// The input bestCacheBits sets the maximum cache bits to use (passing 0 implies disabling the local color cache). + /// The local color cache is also disabled for the lower (smaller then 25) quality. + /// + /// Best cache size. + private static int CalculateBestCacheSize(ReadOnlySpan bgra, int quality, Vp8LBackwardRefs refs, int bestCacheBits) + { + int cacheBitsMax = quality <= 25 ? 0 : bestCacheBits; + if (cacheBitsMax == 0) + { + // Local color cache is disabled. + return 0; + } + + double entropyMin = MaxEntropy; + int pos = 0; + var colorCache = new ColorCache[WebpConstants.MaxColorCacheBits + 1]; + var histos = new Vp8LHistogram[WebpConstants.MaxColorCacheBits + 1]; + for (int i = 0; i <= WebpConstants.MaxColorCacheBits; i++) + { + histos[i] = new Vp8LHistogram(paletteCodeBits: i); + colorCache[i] = new ColorCache(); + colorCache[i].Init(i); + } + + // Find the cacheBits giving the lowest entropy. + for (int idx = 0; idx < refs.Refs.Count; idx++) + { + PixOrCopy v = refs.Refs[idx]; + if (v.IsLiteral()) + { + uint pix = bgra[pos++]; + uint a = (pix >> 24) & 0xff; + uint r = (pix >> 16) & 0xff; + uint g = (pix >> 8) & 0xff; + uint b = (pix >> 0) & 0xff; + + // The keys of the caches can be derived from the longest one. + int key = ColorCache.HashPix(pix, 32 - cacheBitsMax); + + // Do not use the color cache for cacheBits = 0. + ++histos[0].Blue[b]; + ++histos[0].Literal[g]; + ++histos[0].Red[r]; + ++histos[0].Alpha[a]; + + // Deal with cacheBits > 0. + for (int i = cacheBitsMax; i >= 1; --i, key >>= 1) + { + if (colorCache[i].Lookup(key) == pix) + { + ++histos[i].Literal[WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes + key]; + } + else + { + colorCache[i].Set((uint)key, pix); + ++histos[i].Blue[b]; + ++histos[i].Literal[g]; + ++histos[i].Red[r]; + ++histos[i].Alpha[a]; + } + } + } + else + { + // We should compute the contribution of the (distance, length) + // histograms but those are the same independently from the cache size. + // As those constant contributions are in the end added to the other + // histogram contributions, we can ignore them, except for the length + // prefix that is part of the literal_ histogram. + int len = v.Len; + uint bgraPrev = bgra[pos] ^ 0xffffffffu; + + int extraBits = 0, extraBitsValue = 0; + int code = LosslessUtils.PrefixEncode(len, ref extraBits, ref extraBitsValue); + for (int i = 0; i <= cacheBitsMax; i++) + { + ++histos[i].Literal[WebpConstants.NumLiteralCodes + code]; + } + + // Update the color caches. + do + { + if (bgra[pos] != bgraPrev) + { + // Efficiency: insert only if the color changes. + int key = ColorCache.HashPix(bgra[pos], 32 - cacheBitsMax); + for (int i = cacheBitsMax; i >= 1; --i, key >>= 1) + { + colorCache[i].Colors[key] = bgra[pos]; + } + + bgraPrev = bgra[pos]; + } + + pos++; + } + while (--len != 0); + } + } + + for (int i = 0; i <= cacheBitsMax; i++) + { + double entropy = histos[i].EstimateBits(); + if (i == 0 || entropy < entropyMin) + { + entropyMin = entropy; + bestCacheBits = i; + } + } + + return bestCacheBits; + } + + private static void BackwardReferencesTraceBackwards( + int xSize, + int ySize, + ReadOnlySpan bgra, + int cacheBits, + Vp8LHashChain hashChain, + Vp8LBackwardRefs refsSrc, + Vp8LBackwardRefs refsDst) + { + int distArraySize = xSize * ySize; + ushort[] distArray = new ushort[distArraySize]; + + BackwardReferencesHashChainDistanceOnly(xSize, ySize, bgra, cacheBits, hashChain, refsSrc, distArray); + int chosenPathSize = TraceBackwards(distArray, distArraySize); + Span chosenPath = distArray.AsSpan(distArraySize - chosenPathSize); + BackwardReferencesHashChainFollowChosenPath(bgra, cacheBits, chosenPath, chosenPathSize, hashChain, refsDst); + } + + private static void BackwardReferencesHashChainDistanceOnly( + int xSize, + int ySize, + ReadOnlySpan bgra, + int cacheBits, + Vp8LHashChain hashChain, + Vp8LBackwardRefs refs, + ushort[] distArray) + { + int pixCount = xSize * ySize; + bool useColorCache = cacheBits > 0; + int literalArraySize = WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes + (cacheBits > 0 ? 1 << cacheBits : 0); + var costModel = new CostModel(literalArraySize); + int offsetPrev = -1; + int lenPrev = -1; + double offsetCost = -1; + int firstOffsetIsConstant = -1; // initialized with 'impossible' value. + int reach = 0; + var colorCache = new ColorCache(); + + if (useColorCache) + { + colorCache.Init(cacheBits); + } + + costModel.Build(xSize, cacheBits, refs); + var costManager = new CostManager(distArray, pixCount, costModel); + + // We loop one pixel at a time, but store all currently best points to non-processed locations from this point. + distArray[0] = 0; + + // Add first pixel as literal. + AddSingleLiteralWithCostModel(bgra, colorCache, costModel, 0, useColorCache, 0.0f, costManager.Costs, distArray); + + for (int i = 1; i < pixCount; i++) + { + float prevCost = costManager.Costs[i - 1]; + int offset = hashChain.FindOffset(i); + int len = hashChain.FindLength(i); + + // Try adding the pixel as a literal. + AddSingleLiteralWithCostModel(bgra, colorCache, costModel, i, useColorCache, prevCost, costManager.Costs, distArray); + + // If we are dealing with a non-literal. + if (len >= 2) + { + if (offset != offsetPrev) + { + int code = DistanceToPlaneCode(xSize, offset); + offsetCost = costModel.GetDistanceCost(code); + firstOffsetIsConstant = 1; + costManager.PushInterval(prevCost + offsetCost, i, len); + } + else + { + // Instead of considering all contributions from a pixel i by calling: + // costManager.PushInterval(prevCost + offsetCost, i, len); + // we optimize these contributions in case offsetCost stays the same + // for consecutive pixels. This describes a set of pixels similar to a + // previous set (e.g. constant color regions). + if (firstOffsetIsConstant != 0) + { + reach = i - 1 + lenPrev - 1; + firstOffsetIsConstant = 0; + } + + if (i + len - 1 > reach) + { + int lenJ = 0; + int j; + for (j = i; j <= reach; j++) + { + int offsetJ = hashChain.FindOffset(j + 1); + lenJ = hashChain.FindLength(j + 1); + if (offsetJ != offset) + { + lenJ = hashChain.FindLength(j); + break; + } + } + + // Update the cost at j - 1 and j. + costManager.UpdateCostAtIndex(j - 1, false); + costManager.UpdateCostAtIndex(j, false); + + costManager.PushInterval(costManager.Costs[j - 1] + offsetCost, j, lenJ); + reach = j + lenJ - 1; + } + } + } + + costManager.UpdateCostAtIndex(i, true); + offsetPrev = offset; + lenPrev = len; + } + } + + private static int TraceBackwards(ushort[] distArray, int distArraySize) + { + int chosenPathSize = 0; + int pathPos = distArraySize; + int curPos = distArraySize - 1; + while (curPos >= 0) + { + ushort cur = distArray[curPos]; + pathPos--; + chosenPathSize++; + distArray[pathPos] = cur; + curPos -= cur; + } + + return chosenPathSize; + } + + private static void BackwardReferencesHashChainFollowChosenPath(ReadOnlySpan bgra, int cacheBits, Span chosenPath, int chosenPathSize, Vp8LHashChain hashChain, Vp8LBackwardRefs backwardRefs) + { + bool useColorCache = cacheBits > 0; + var colorCache = new ColorCache(); + int i = 0; + + if (useColorCache) + { + colorCache.Init(cacheBits); + } + + backwardRefs.Refs.Clear(); + for (int ix = 0; ix < chosenPathSize; ix++) + { + int len = chosenPath[ix]; + if (len != 1) + { + int offset = hashChain.FindOffset(i); + backwardRefs.Add(PixOrCopy.CreateCopy((uint)offset, (ushort)len)); + + if (useColorCache) + { + for (int k = 0; k < len; k++) + { + colorCache.Insert(bgra[i + k]); + } + } + + i += len; + } + else + { + PixOrCopy v; + int idx = useColorCache ? colorCache.Contains(bgra[i]) : -1; + if (idx >= 0) + { + // useColorCache is true and color cache contains bgra[i] + // Push pixel as a color cache index. + v = PixOrCopy.CreateCacheIdx(idx); + } + else + { + if (useColorCache) + { + colorCache.Insert(bgra[i]); + } + + v = PixOrCopy.CreateLiteral(bgra[i]); + } + + backwardRefs.Add(v); + i++; + } + } + } + + private static void AddSingleLiteralWithCostModel( + ReadOnlySpan bgra, + ColorCache colorCache, + CostModel costModel, + int idx, + bool useColorCache, + float prevCost, + float[] cost, + ushort[] distArray) + { + double costVal = prevCost; + uint color = bgra[idx]; + int ix = useColorCache ? colorCache.Contains(color) : -1; + if (ix >= 0) + { + double mul0 = 0.68; + costVal += costModel.GetCacheCost((uint)ix) * mul0; + } + else + { + double mul1 = 0.82; + if (useColorCache) + { + colorCache.Insert(color); + } + + costVal += costModel.GetLiteralCost(color) * mul1; + } + + if (cost[idx] > costVal) + { + cost[idx] = (float)costVal; + distArray[idx] = 1; // only one is inserted. + } + } + + private static void BackwardReferencesLz77(int xSize, int ySize, ReadOnlySpan bgra, int cacheBits, Vp8LHashChain hashChain, Vp8LBackwardRefs refs) + { + int iLastCheck = -1; + bool useColorCache = cacheBits > 0; + int pixCount = xSize * ySize; + var colorCache = new ColorCache(); + if (useColorCache) + { + colorCache.Init(cacheBits); + } + + refs.Refs.Clear(); + for (int i = 0; i < pixCount;) + { + // Alternative #1: Code the pixels starting at 'i' using backward reference. + int j; + int offset = hashChain.FindOffset(i); + int len = hashChain.FindLength(i); + if (len >= MinLength) + { + int lenIni = len; + int maxReach = 0; + int jMax = i + lenIni >= pixCount ? pixCount - 1 : i + lenIni; + + // Only start from what we have not checked already. + iLastCheck = i > iLastCheck ? i : iLastCheck; + + // We know the best match for the current pixel but we try to find the + // best matches for the current pixel AND the next one combined. + // The naive method would use the intervals: + // [i,i+len) + [i+len, length of best match at i+len) + // while we check if we can use: + // [i,j) (where j<=i+len) + [j, length of best match at j) + for (j = iLastCheck + 1; j <= jMax; j++) + { + int lenJ = hashChain.FindLength(j); + int reach = j + (lenJ >= MinLength ? lenJ : 1); // 1 for single literal. + if (reach > maxReach) + { + len = j - i; + maxReach = reach; + if (maxReach >= pixCount) + { + break; + } + } + } + } + else + { + len = 1; + } + + // Go with literal or backward reference. + if (len == 1) + { + AddSingleLiteral(bgra[i], useColorCache, colorCache, refs); + } + else + { + refs.Add(PixOrCopy.CreateCopy((uint)offset, (ushort)len)); + if (useColorCache) + { + for (j = i; j < i + len; j++) + { + colorCache.Insert(bgra[j]); + } + } + } + + i += len; + } + } + + /// + /// Compute an LZ77 by forcing matches to happen within a given distance cost. + /// We therefore limit the algorithm to the lowest 32 values in the PlaneCode definition. + /// + private static void BackwardReferencesLz77Box(int xSize, int ySize, ReadOnlySpan bgra, int cacheBits, Vp8LHashChain hashChainBest, Vp8LHashChain hashChain, Vp8LBackwardRefs refs) + { + int pixelCount = xSize * ySize; + int[] windowOffsets = new int[WindowOffsetsSizeMax]; + int[] windowOffsetsNew = new int[WindowOffsetsSizeMax]; + int windowOffsetsSize = 0; + int windowOffsetsNewSize = 0; + short[] counts = new short[xSize * ySize]; + int bestOffsetPrev = -1; + int bestLengthPrev = -1; + + // counts[i] counts how many times a pixel is repeated starting at position i. + int i = pixelCount - 2; + int countsPos = i; + counts[countsPos + 1] = 1; + for (; i >= 0; --i, --countsPos) + { + if (bgra[i] == bgra[i + 1]) + { + // Max out the counts to MaxLength. + counts[countsPos] = counts[countsPos + 1]; + if (counts[countsPos + 1] != MaxLength) + { + counts[countsPos]++; + } + } + else + { + counts[countsPos] = 1; + } + } + + // Figure out the window offsets around a pixel. They are stored in a + // spiraling order around the pixel as defined by DistanceToPlaneCode. + for (int y = 0; y <= 6; y++) + { + for (int x = -6; x <= 6; x++) + { + int offset = (y * xSize) + x; + + // Ignore offsets that bring us after the pixel. + if (offset <= 0) + { + continue; + } + + int planeCode = DistanceToPlaneCode(xSize, offset) - 1; + if (planeCode >= WindowOffsetsSizeMax) + { + continue; + } + + windowOffsets[planeCode] = offset; + } + } + + // For narrow images, not all plane codes are reached, so remove those. + for (i = 0; i < WindowOffsetsSizeMax; i++) + { + if (windowOffsets[i] == 0) + { + continue; + } + + windowOffsets[windowOffsetsSize++] = windowOffsets[i]; + } + + // Given a pixel P, find the offsets that reach pixels unreachable from P-1 + // with any of the offsets in windowOffsets[]. + for (i = 0; i < windowOffsetsSize; i++) + { + bool isReachable = false; + for (int j = 0; j < windowOffsetsSize && !isReachable; j++) + { + isReachable |= windowOffsets[i] == windowOffsets[j] + 1; + } + + if (!isReachable) + { + windowOffsetsNew[windowOffsetsNewSize] = windowOffsets[i]; + ++windowOffsetsNewSize; + } + } + + hashChain.OffsetLength[0] = 0; + for (i = 1; i < pixelCount; i++) + { + int ind; + int bestLength = hashChainBest.FindLength(i); + int bestOffset = 0; + bool doCompute = true; + + if (bestLength >= MaxLength) + { + // Do not recompute the best match if we already have a maximal one in the window. + bestOffset = hashChainBest.FindOffset(i); + for (ind = 0; ind < windowOffsetsSize; ind++) + { + if (bestOffset == windowOffsets[ind]) + { + doCompute = false; + break; + } + } + } + + if (doCompute) + { + // Figure out if we should use the offset/length from the previous pixel + // as an initial guess and therefore only inspect the offsets in windowOffsetsNew[]. + bool usePrev = bestLengthPrev is > 1 and < MaxLength; + int numInd = usePrev ? windowOffsetsNewSize : windowOffsetsSize; + bestLength = usePrev ? bestLengthPrev - 1 : 0; + bestOffset = usePrev ? bestOffsetPrev : 0; + + // Find the longest match in a window around the pixel. + for (ind = 0; ind < numInd; ind++) + { + int currLength = 0; + int j = i; + int jOffset = usePrev ? i - windowOffsetsNew[ind] : i - windowOffsets[ind]; + if (jOffset < 0 || bgra[jOffset] != bgra[i]) + { + continue; + } + + // The longest match is the sum of how many times each pixel is repeated. + do + { + int countsJOffset = counts[jOffset]; + int countsJ = counts[j]; + if (countsJOffset != countsJ) + { + currLength += countsJOffset < countsJ ? countsJOffset : countsJ; + break; + } + + // The same color is repeated counts_pos times at jOffset and j. + currLength += countsJOffset; + jOffset += countsJOffset; + j += countsJOffset; + } + while (currLength <= MaxLength && j < pixelCount && bgra[jOffset] == bgra[j]); + + if (bestLength < currLength) + { + bestOffset = usePrev ? windowOffsetsNew[ind] : windowOffsets[ind]; + if (currLength >= MaxLength) + { + bestLength = MaxLength; + break; + } + else + { + bestLength = currLength; + } + } + } + } + + if (bestLength <= MinLength) + { + hashChain.OffsetLength[i] = 0; + bestOffsetPrev = 0; + bestLengthPrev = 0; + } + else + { + hashChain.OffsetLength[i] = (uint)((bestOffset << MaxLengthBits) | bestLength); + bestOffsetPrev = bestOffset; + bestLengthPrev = bestLength; + } + } + + hashChain.OffsetLength[0] = 0; + BackwardReferencesLz77(xSize, ySize, bgra, cacheBits, hashChain, refs); + } + + private static void BackwardReferencesRle(int xSize, int ySize, ReadOnlySpan bgra, int cacheBits, Vp8LBackwardRefs refs) + { + int pixelCount = xSize * ySize; + bool useColorCache = cacheBits > 0; + var colorCache = new ColorCache(); + + if (useColorCache) + { + colorCache.Init(cacheBits); + } + + refs.Refs.Clear(); + + // Add first pixel as literal. + AddSingleLiteral(bgra[0], useColorCache, colorCache, refs); + int i = 1; + while (i < pixelCount) + { + int maxLen = LosslessUtils.MaxFindCopyLength(pixelCount - i); + int rleLen = LosslessUtils.FindMatchLength(bgra.Slice(i), bgra.Slice(i - 1), 0, maxLen); + int prevRowLen = i < xSize ? 0 : LosslessUtils.FindMatchLength(bgra.Slice(i), bgra.Slice(i - xSize), 0, maxLen); + if (rleLen >= prevRowLen && rleLen >= MinLength) + { + refs.Add(PixOrCopy.CreateCopy(1, (ushort)rleLen)); + + // We don't need to update the color cache here since it is always the + // same pixel being copied, and that does not change the color cache state. + i += rleLen; + } + else if (prevRowLen >= MinLength) + { + refs.Add(PixOrCopy.CreateCopy((uint)xSize, (ushort)prevRowLen)); + if (useColorCache) + { + for (int k = 0; k < prevRowLen; ++k) + { + colorCache.Insert(bgra[i + k]); + } + } + + i += prevRowLen; + } + else + { + AddSingleLiteral(bgra[i], useColorCache, colorCache, refs); + i++; + } + } + } + + /// + /// Update (in-place) backward references for the specified cacheBits. + /// + private static void BackwardRefsWithLocalCache(ReadOnlySpan bgra, int cacheBits, Vp8LBackwardRefs refs) + { + int pixelIndex = 0; + var colorCache = new ColorCache(); + colorCache.Init(cacheBits); + for (int idx = 0; idx < refs.Refs.Count; idx++) + { + PixOrCopy v = refs.Refs[idx]; + if (v.IsLiteral()) + { + uint bgraLiteral = v.BgraOrDistance; + int ix = colorCache.Contains(bgraLiteral); + if (ix >= 0) + { + // Color cache contains bgraLiteral + v.Mode = PixOrCopyMode.CacheIdx; + v.BgraOrDistance = (uint)ix; + v.Len = 1; + } + else + { + colorCache.Insert(bgraLiteral); + } + + pixelIndex++; + } + else + { + // refs was created without local cache, so it can not have cache indexes. + for (int k = 0; k < v.Len; ++k) + { + colorCache.Insert(bgra[pixelIndex++]); + } + } + } + } + + private static void BackwardReferences2DLocality(int xSize, Vp8LBackwardRefs refs) + { + using List.Enumerator c = refs.Refs.GetEnumerator(); + while (c.MoveNext()) + { + if (c.Current.IsCopy()) + { + int dist = (int)c.Current.BgraOrDistance; + int transformedDist = DistanceToPlaneCode(xSize, dist); + c.Current.BgraOrDistance = (uint)transformedDist; + } + } + } + + private static void AddSingleLiteral(uint pixel, bool useColorCache, ColorCache colorCache, Vp8LBackwardRefs refs) + { + PixOrCopy v; + if (useColorCache) + { + int key = colorCache.GetIndex(pixel); + if (colorCache.Lookup(key) == pixel) + { + v = PixOrCopy.CreateCacheIdx(key); + } + else + { + v = PixOrCopy.CreateLiteral(pixel); + colorCache.Set((uint)key, pixel); + } + } + else + { + v = PixOrCopy.CreateLiteral(pixel); + } + + refs.Add(v); + } + + public static int DistanceToPlaneCode(int xSize, int dist) + { + int yOffset = dist / xSize; + int xOffset = dist - (yOffset * xSize); + if (xOffset <= 8 && yOffset < 8) + { + return (int)WebpLookupTables.PlaneToCodeLut[(yOffset * 16) + 8 - xOffset] + 1; + } + else if (xOffset > xSize - 8 && yOffset < 7) + { + return (int)WebpLookupTables.PlaneToCodeLut[((yOffset + 1) * 16) + 8 + (xSize - xOffset)] + 1; + } + + return dist + 120; + } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/ColorCache.cs b/src/ImageSharp/Formats/Webp/Lossless/ColorCache.cs new file mode 100644 index 000000000..8596d8555 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/ColorCache.cs @@ -0,0 +1,84 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + /// + /// 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 bgra) + { + int key = HashPix(bgra, this.HashShift); + this.Colors[key] = bgra; + } + + /// + /// Gets a color for a given key. + /// + /// The key to lookup. + /// The color for the key. + public uint Lookup(int key) => this.Colors[key]; + + /// + /// Returns the index of the given color. + /// + /// The color to check. + /// The index of the color in the cache or -1 if its not present. + public int Contains(uint bgra) + { + int key = HashPix(bgra, this.HashShift); + return (this.Colors[key] == bgra) ? key : -1; + } + + /// + /// Gets the index of a color. + /// + /// The color. + /// The index for the color. + public int GetIndex(uint bgra) => HashPix(bgra, this.HashShift); + + /// + /// Adds a new color to the cache. + /// + /// The key. + /// The color to add. + public void Set(uint key, uint bgra) => this.Colors[key] = bgra; + + public static int HashPix(uint argb, int shift) => (int)((argb * HashMul) >> shift); + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/CostCacheInterval.cs b/src/ImageSharp/Formats/Webp/Lossless/CostCacheInterval.cs new file mode 100644 index 000000000..b4038b141 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/CostCacheInterval.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + /// + /// The GetLengthCost(costModel, k) are cached in a CostCacheInterval. + /// + [DebuggerDisplay("Start: {Start}, End: {End}, Cost: {Cost}")] + internal class CostCacheInterval + { + public double Cost { get; set; } + + public int Start { get; set; } + + public int End { get; set; } // Exclusive. + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/CostInterval.cs b/src/ImageSharp/Formats/Webp/Lossless/CostInterval.cs new file mode 100644 index 000000000..828487eb4 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/CostInterval.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + /// + /// To perform backward reference every pixel at index index_ is considered and + /// the cost for the MAX_LENGTH following pixels computed. Those following pixels + /// at index index_ + k (k from 0 to MAX_LENGTH) have a cost of: + /// cost = distance cost at index + GetLengthCost(costModel, k) + /// and the minimum value is kept. GetLengthCost(costModel, k) is cached in an + /// array of size MAX_LENGTH. + /// Instead of performing MAX_LENGTH comparisons per pixel, we keep track of the + /// minimal values using intervals of constant cost. + /// An interval is defined by the index_ of the pixel that generated it and + /// is only useful in a range of indices from start to end (exclusive), i.e. + /// it contains the minimum value for pixels between start and end. + /// Intervals are stored in a linked list and ordered by start. When a new + /// interval has a better value, old intervals are split or removed. There are + /// therefore no overlapping intervals. + /// + [DebuggerDisplay("Start: {Start}, End: {End}, Cost: {Cost}")] + internal class CostInterval + { + public float Cost { get; set; } + + public int Start { get; set; } + + public int End { get; set; } + + public int Index { get; set; } + + public CostInterval Previous { get; set; } + + public CostInterval Next { get; set; } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/CostManager.cs b/src/ImageSharp/Formats/Webp/Lossless/CostManager.cs new file mode 100644 index 000000000..94c7bd847 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/CostManager.cs @@ -0,0 +1,308 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + /// + /// The CostManager is in charge of managing intervals and costs. + /// It caches the different CostCacheInterval, caches the different + /// GetLengthCost(costModel, k) in costCache and the CostInterval's. + /// + internal class CostManager + { + private CostInterval head; + + public CostManager(ushort[] distArray, int pixCount, CostModel costModel) + { + int costCacheSize = pixCount > BackwardReferenceEncoder.MaxLength ? BackwardReferenceEncoder.MaxLength : pixCount; + + this.CacheIntervals = new List(); + this.CostCache = new List(); + this.Costs = new float[pixCount]; + this.DistArray = distArray; + this.Count = 0; + + // Fill in the cost cache. + this.CacheIntervalsSize++; + this.CostCache.Add(costModel.GetLengthCost(0)); + for (int i = 1; i < costCacheSize; i++) + { + this.CostCache.Add(costModel.GetLengthCost(i)); + + // Get the number of bound intervals. + if (this.CostCache[i] != this.CostCache[i - 1]) + { + this.CacheIntervalsSize++; + } + } + + // Fill in the cache intervals. + var cur = new CostCacheInterval() + { + Start = 0, + End = 1, + Cost = this.CostCache[0] + }; + this.CacheIntervals.Add(cur); + + for (int i = 1; i < costCacheSize; i++) + { + double costVal = this.CostCache[i]; + if (costVal != cur.Cost) + { + cur = new CostCacheInterval() + { + Start = i, + Cost = costVal + }; + this.CacheIntervals.Add(cur); + } + + cur.End = i + 1; + } + + // Set the initial costs high for every pixel as we will keep the minimum. + for (int i = 0; i < pixCount; i++) + { + this.Costs[i] = 1e38f; + } + } + + /// + /// Gets or sets the number of stored intervals. + /// + public int Count { get; set; } + + /// + /// Gets the costs cache. Contains the GetLengthCost(costModel, k). + /// + public List CostCache { get; } + + public int CacheIntervalsSize { get; } + + public float[] Costs { get; } + + public ushort[] DistArray { get; } + + public List CacheIntervals { get; } + + /// + /// Update the cost at index i by going over all the stored intervals that overlap with i. + /// + /// The index to update. + /// If 'doCleanIntervals' is true, intervals that end before 'i' will be popped. + public void UpdateCostAtIndex(int i, bool doCleanIntervals) + { + CostInterval current = this.head; + while (current != null && current.Start <= i) + { + CostInterval next = current.Next; + if (current.End <= i) + { + if (doCleanIntervals) + { + // We have an outdated interval, remove it. + this.PopInterval(current); + } + } + else + { + this.UpdateCost(i, current.Index, current.Cost); + } + + current = next; + } + } + + /// + /// Given a new cost interval defined by its start at position, its length value + /// and distanceCost, add its contributions to the previous intervals and costs. + /// If handling the interval or one of its sub-intervals becomes to heavy, its + /// contribution is added to the costs right away. + /// + public void PushInterval(double distanceCost, int position, int len) + { + // If the interval is small enough, no need to deal with the heavy + // interval logic, just serialize it right away. This constant is empirical. + int skipDistance = 10; + + if (len < skipDistance) + { + for (int j = position; j < position + len; j++) + { + int k = j - position; + float costTmp = (float)(distanceCost + this.CostCache[k]); + + if (this.Costs[j] > costTmp) + { + this.Costs[j] = costTmp; + this.DistArray[j] = (ushort)(k + 1); + } + } + + return; + } + + CostInterval interval = this.head; + for (int i = 0; i < this.CacheIntervalsSize && this.CacheIntervals[i].Start < len; i++) + { + // Define the intersection of the ith interval with the new one. + int start = position + this.CacheIntervals[i].Start; + int end = position + (this.CacheIntervals[i].End > len ? len : this.CacheIntervals[i].End); + float cost = (float)(distanceCost + this.CacheIntervals[i].Cost); + + CostInterval intervalNext; + for (; interval != null && interval.Start < end; interval = intervalNext) + { + intervalNext = interval.Next; + + // Make sure we have some overlap. + if (start >= interval.End) + { + continue; + } + + if (cost >= interval.Cost) + { + // If we are worse than what we already have, add whatever we have so far up to interval. + int startNew = interval.End; + this.InsertInterval(interval, cost, position, start, interval.Start); + start = startNew; + if (start >= end) + { + break; + } + + continue; + } + + if (start <= interval.Start) + { + if (interval.End <= end) + { + // We can safely remove the old interval as it is fully included. + this.PopInterval(interval); + } + else + { + interval.Start = end; + break; + } + } + else + { + if (end < interval.End) + { + // We have to split the old interval as it fully contains the new one. + int endOriginal = interval.End; + interval.End = start; + this.InsertInterval(interval, interval.Cost, interval.Index, end, endOriginal); + break; + } + else + { + interval.End = start; + } + } + } + + // Insert the remaining interval from start to end. + this.InsertInterval(interval, cost, position, start, end); + } + } + + /// + /// Pop an interval from the manager. + /// + /// The interval to remove. + private void PopInterval(CostInterval interval) + { + if (interval == null) + { + return; + } + + this.ConnectIntervals(interval.Previous, interval.Next); + this.Count--; + } + + private void InsertInterval(CostInterval intervalIn, float cost, int position, int start, int end) + { + if (start >= end) + { + return; + } + + // TODO: should we use COST_CACHE_INTERVAL_SIZE_MAX? + var intervalNew = new CostInterval() + { + Cost = cost, + Start = start, + End = end, + Index = position + }; + + this.PositionOrphanInterval(intervalNew, intervalIn); + this.Count++; + } + + /// + /// Given a current orphan interval and its previous interval, before + /// it was orphaned (which can be NULL), set it at the right place in the list + /// of intervals using the start_ ordering and the previous interval as a hint. + /// + private void PositionOrphanInterval(CostInterval current, CostInterval previous) + { + previous ??= this.head; + + while (previous != null && current.Start < previous.Start) + { + previous = previous.Previous; + } + + while (previous?.Next != null && previous.Next.Start < current.Start) + { + previous = previous.Next; + } + + this.ConnectIntervals(current, previous != null ? previous.Next : this.head); + this.ConnectIntervals(previous, current); + } + + /// + /// Given two intervals, make 'prev' be the previous one of 'next' in 'manager'. + /// + private void ConnectIntervals(CostInterval prev, CostInterval next) + { + if (prev != null) + { + prev.Next = next; + } + else + { + this.head = next; + } + + if (next != null) + { + next.Previous = prev; + } + } + + /// + /// Given the cost and the position that define an interval, update the cost at + /// pixel 'i' if it is smaller than the previously computed value. + /// + private void UpdateCost(int i, int position, float cost) + { + int k = i - position; + if (this.Costs[i] > cost) + { + this.Costs[i] = cost; + this.DistArray[i] = (ushort)(k + 1); + } + } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs b/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs new file mode 100644 index 000000000..7f4d0307b --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/CostModel.cs @@ -0,0 +1,102 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + internal class CostModel + { + private const int ValuesInBytes = 256; + + /// + /// Initializes a new instance of the class. + /// + /// The literal array size. + public CostModel(int literalArraySize) + { + this.Alpha = new double[ValuesInBytes]; + this.Red = new double[ValuesInBytes]; + this.Blue = new double[ValuesInBytes]; + this.Distance = new double[WebpConstants.NumDistanceCodes]; + this.Literal = new double[literalArraySize]; + } + + public double[] Alpha { get; } + + public double[] Red { get; } + + public double[] Blue { get; } + + public double[] Distance { get; } + + public double[] Literal { get; } + + public void Build(int xSize, int cacheBits, Vp8LBackwardRefs backwardRefs) + { + var histogram = new Vp8LHistogram(cacheBits); + using System.Collections.Generic.List.Enumerator refsEnumerator = backwardRefs.Refs.GetEnumerator(); + + // The following code is similar to HistogramCreate but converts the distance to plane code. + while (refsEnumerator.MoveNext()) + { + histogram.AddSinglePixOrCopy(refsEnumerator.Current, true, xSize); + } + + ConvertPopulationCountTableToBitEstimates(histogram.NumCodes(), histogram.Literal, this.Literal); + ConvertPopulationCountTableToBitEstimates(ValuesInBytes, histogram.Red, this.Red); + ConvertPopulationCountTableToBitEstimates(ValuesInBytes, histogram.Blue, this.Blue); + ConvertPopulationCountTableToBitEstimates(ValuesInBytes, histogram.Alpha, this.Alpha); + ConvertPopulationCountTableToBitEstimates(WebpConstants.NumDistanceCodes, histogram.Distance, this.Distance); + } + + public double GetLengthCost(int length) + { + int extraBits = 0; + int code = LosslessUtils.PrefixEncodeBits(length, ref extraBits); + return this.Literal[ValuesInBytes + code] + extraBits; + } + + public double GetDistanceCost(int distance) + { + int extraBits = 0; + int code = LosslessUtils.PrefixEncodeBits(distance, ref extraBits); + return this.Distance[code] + extraBits; + } + + public double GetCacheCost(uint idx) + { + int literalIdx = (int)(ValuesInBytes + WebpConstants.NumLengthCodes + idx); + return this.Literal[literalIdx]; + } + + public double GetLiteralCost(uint v) => this.Alpha[v >> 24] + this.Red[(v >> 16) & 0xff] + this.Literal[(v >> 8) & 0xff] + this.Blue[v & 0xff]; + + private static void ConvertPopulationCountTableToBitEstimates(int numSymbols, uint[] populationCounts, double[] output) + { + uint sum = 0; + int nonzeros = 0; + for (int i = 0; i < numSymbols; i++) + { + sum += populationCounts[i]; + if (populationCounts[i] > 0) + { + nonzeros++; + } + } + + if (nonzeros <= 1) + { + output.AsSpan(0, numSymbols).Fill(0); + } + else + { + double logsum = LosslessUtils.FastLog2(sum); + for (int i = 0; i < numSymbols; i++) + { + output[i] = logsum - LosslessUtils.FastLog2(populationCounts[i]); + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/CrunchConfig.cs b/src/ImageSharp/Formats/Webp/Lossless/CrunchConfig.cs new file mode 100644 index 000000000..a36c70bca --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/CrunchConfig.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + internal class CrunchConfig + { + public EntropyIx EntropyIdx { get; set; } + + public List SubConfigs { get; } = new List(); + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/CrunchSubConfig.cs b/src/ImageSharp/Formats/Webp/Lossless/CrunchSubConfig.cs new file mode 100644 index 000000000..22fbcdcf8 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/CrunchSubConfig.cs @@ -0,0 +1,12 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + internal class CrunchSubConfig + { + public int Lz77 { get; set; } + + public bool DoNotCache { get; set; } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/DominantCostRange.cs b/src/ImageSharp/Formats/Webp/Lossless/DominantCostRange.cs new file mode 100644 index 000000000..2c5850142 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/DominantCostRange.cs @@ -0,0 +1,92 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + /// + /// Data container to keep track of cost range for the three dominant entropy symbols. + /// + internal class DominantCostRange + { + /// + /// Initializes a new instance of the class. + /// + public DominantCostRange() + { + this.LiteralMax = 0.0d; + this.LiteralMin = double.MaxValue; + this.RedMax = 0.0d; + this.RedMin = double.MaxValue; + this.BlueMax = 0.0d; + this.BlueMin = double.MaxValue; + } + + public double LiteralMax { get; set; } + + public double LiteralMin { get; set; } + + public double RedMax { get; set; } + + public double RedMin { get; set; } + + public double BlueMax { get; set; } + + public double BlueMin { get; set; } + + public void UpdateDominantCostRange(Vp8LHistogram h) + { + if (this.LiteralMax < h.LiteralCost) + { + this.LiteralMax = h.LiteralCost; + } + + if (this.LiteralMin > h.LiteralCost) + { + this.LiteralMin = h.LiteralCost; + } + + if (this.RedMax < h.RedCost) + { + this.RedMax = h.RedCost; + } + + if (this.RedMin > h.RedCost) + { + this.RedMin = h.RedCost; + } + + if (this.BlueMax < h.BlueCost) + { + this.BlueMax = h.BlueCost; + } + + if (this.BlueMin > h.BlueCost) + { + this.BlueMin = h.BlueCost; + } + } + + public int GetHistoBinIndex(Vp8LHistogram h, int numPartitions) + { + int binId = GetBinIdForEntropy(this.LiteralMin, this.LiteralMax, h.LiteralCost, numPartitions); + binId = (binId * numPartitions) + GetBinIdForEntropy(this.RedMin, this.RedMax, h.RedCost, numPartitions); + binId = (binId * numPartitions) + GetBinIdForEntropy(this.BlueMin, this.BlueMax, h.BlueCost, numPartitions); + + return binId; + } + + private static int GetBinIdForEntropy(double min, double max, double val, int numPartitions) + { + double range = max - min; + if (range > 0.0d) + { + double delta = val - min; + return (int)((numPartitions - 1e-6) * delta / range); + } + else + { + return 0; + } + } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/HTreeGroup.cs b/src/ImageSharp/Formats/Webp/Lossless/HTreeGroup.cs new file mode 100644 index 000000000..a038248f1 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/HTreeGroup.cs @@ -0,0 +1,59 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + /// + /// 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 (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/Lossless/HistogramBinInfo.cs b/src/ImageSharp/Formats/Webp/Lossless/HistogramBinInfo.cs new file mode 100644 index 000000000..5f5f5d874 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/HistogramBinInfo.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + internal struct HistogramBinInfo + { + /// + /// Position of the histogram that accumulates all histograms with the same binId. + /// + public short First; + + /// + /// Number of combine failures per binId. + /// + public ushort NumCombineFailures; + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs new file mode 100644 index 000000000..f2d4fb189 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs @@ -0,0 +1,685 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + internal class HistogramEncoder + { + /// + /// Number of partitions for the three dominant (literal, red and blue) symbol costs. + /// + private const int NumPartitions = 4; + + /// + /// The size of the bin-hash corresponding to the three dominant costs. + /// + private const int BinSize = NumPartitions * NumPartitions * NumPartitions; + + /// + /// Maximum number of histograms allowed in greedy combining algorithm. + /// + private const int MaxHistoGreedy = 100; + + private const uint NonTrivialSym = 0xffffffff; + + private const ushort InvalidHistogramSymbol = ushort.MaxValue; + + public static void GetHistoImageSymbols(int xSize, int ySize, Vp8LBackwardRefs refs, int quality, int histoBits, int cacheBits, List imageHisto, Vp8LHistogram tmpHisto, ushort[] histogramSymbols) + { + int histoXSize = histoBits > 0 ? LosslessUtils.SubSampleSize(xSize, histoBits) : 1; + int histoYSize = histoBits > 0 ? LosslessUtils.SubSampleSize(ySize, histoBits) : 1; + int imageHistoRawSize = histoXSize * histoYSize; + int entropyCombineNumBins = BinSize; + ushort[] mapTmp = new ushort[imageHistoRawSize]; + ushort[] clusterMappings = new ushort[imageHistoRawSize]; + var origHisto = new List(imageHistoRawSize); + for (int i = 0; i < imageHistoRawSize; i++) + { + origHisto.Add(new Vp8LHistogram(cacheBits)); + } + + // Construct the histograms from the backward references. + HistogramBuild(xSize, histoBits, refs, origHisto); + + // Copies the histograms and computes its bitCost. histogramSymbols is optimized. + int numUsed = HistogramCopyAndAnalyze(origHisto, imageHisto, histogramSymbols); + + bool entropyCombine = numUsed > entropyCombineNumBins * 2 && quality < 100; + if (entropyCombine) + { + ushort[] binMap = mapTmp; + int numClusters = numUsed; + double combineCostFactor = GetCombineCostFactor(imageHistoRawSize, quality); + HistogramAnalyzeEntropyBin(imageHisto, binMap); + + // Collapse histograms with similar entropy. + HistogramCombineEntropyBin(imageHisto, histogramSymbols, clusterMappings, tmpHisto, binMap, entropyCombineNumBins, combineCostFactor); + + OptimizeHistogramSymbols(clusterMappings, numClusters, mapTmp, histogramSymbols); + } + + float x = quality / 100.0f; + + // Cubic ramp between 1 and MaxHistoGreedy: + int thresholdSize = (int)(1 + (x * x * x * (MaxHistoGreedy - 1))); + bool doGreedy = HistogramCombineStochastic(imageHisto, thresholdSize); + if (doGreedy) + { + RemoveEmptyHistograms(imageHisto); + HistogramCombineGreedy(imageHisto); + } + + // Find the optimal map from original histograms to the final ones. + RemoveEmptyHistograms(imageHisto); + HistogramRemap(origHisto, imageHisto, histogramSymbols); + } + + private static void RemoveEmptyHistograms(List histograms) + { + int size = 0; + for (int i = 0; i < histograms.Count; i++) + { + if (histograms[i] == null) + { + continue; + } + + histograms[size++] = histograms[i]; + } + + histograms.RemoveRange(size, histograms.Count - size); + } + + /// + /// Construct the histograms from the backward references. + /// + private static void HistogramBuild(int xSize, int histoBits, Vp8LBackwardRefs backwardRefs, List histograms) + { + int x = 0, y = 0; + int histoXSize = LosslessUtils.SubSampleSize(xSize, histoBits); + using List.Enumerator backwardRefsEnumerator = backwardRefs.Refs.GetEnumerator(); + while (backwardRefsEnumerator.MoveNext()) + { + PixOrCopy v = backwardRefsEnumerator.Current; + int ix = ((y >> histoBits) * histoXSize) + (x >> histoBits); + histograms[ix].AddSinglePixOrCopy(v, false); + x += v.Len; + while (x >= xSize) + { + x -= xSize; + y++; + } + } + } + + /// + /// Partition histograms to different entropy bins for three dominant (literal, + /// red and blue) symbol costs and compute the histogram aggregate bitCost. + /// + private static void HistogramAnalyzeEntropyBin(List histograms, ushort[] binMap) + { + int histoSize = histograms.Count; + var costRange = new DominantCostRange(); + + // Analyze the dominant (literal, red and blue) entropy costs. + for (int i = 0; i < histoSize; i++) + { + if (histograms[i] == null) + { + continue; + } + + costRange.UpdateDominantCostRange(histograms[i]); + } + + // bin-hash histograms on three of the dominant (literal, red and blue) + // symbol costs and store the resulting bin_id for each histogram. + for (int i = 0; i < histoSize; i++) + { + if (histograms[i] == null) + { + continue; + } + + binMap[i] = (ushort)costRange.GetHistoBinIndex(histograms[i], NumPartitions); + } + } + + private static int HistogramCopyAndAnalyze(List origHistograms, List histograms, ushort[] histogramSymbols) + { + for (int clusterId = 0, i = 0; i < origHistograms.Count; i++) + { + Vp8LHistogram origHistogram = origHistograms[i]; + origHistogram.UpdateHistogramCost(); + + // Skip the histogram if it is completely empty, which can happen for tiles with no information (when they are skipped because of LZ77). + if (!origHistogram.IsUsed[0] && !origHistogram.IsUsed[1] && !origHistogram.IsUsed[2] && !origHistogram.IsUsed[3] && !origHistogram.IsUsed[4]) + { + origHistograms[i] = null; + histograms[i] = null; + histogramSymbols[i] = InvalidHistogramSymbol; + } + else + { + histograms[i] = (Vp8LHistogram)origHistogram.DeepClone(); + histogramSymbols[i] = (ushort)clusterId++; + } + } + + int numUsed = histogramSymbols.Count(h => h != InvalidHistogramSymbol); + return numUsed; + } + + private static void HistogramCombineEntropyBin(List histograms, ushort[] clusters, ushort[] clusterMappings, Vp8LHistogram curCombo, ushort[] binMap, int numBins, double combineCostFactor) + { + var binInfo = new HistogramBinInfo[BinSize]; + for (int idx = 0; idx < numBins; idx++) + { + binInfo[idx].First = -1; + binInfo[idx].NumCombineFailures = 0; + } + + // By default, a cluster matches itself. + for (int idx = 0; idx < histograms.Count; idx++) + { + clusterMappings[idx] = (ushort)idx; + } + + var indicesToRemove = new List(); + for (int idx = 0; idx < histograms.Count; idx++) + { + if (histograms[idx] == null) + { + continue; + } + + int binId = binMap[idx]; + int first = binInfo[binId].First; + if (first == -1) + { + binInfo[binId].First = (short)idx; + } + else + { + // Try to merge #idx into #first (both share the same binId) + double bitCost = histograms[idx].BitCost; + double bitCostThresh = -bitCost * combineCostFactor; + double currCostDiff = histograms[first].AddEval(histograms[idx], bitCostThresh, curCombo); + + if (currCostDiff < bitCostThresh) + { + // Try to merge two histograms only if the combo is a trivial one or + // the two candidate histograms are already non-trivial. + // For some images, 'tryCombine' turns out to be false for a lot of + // histogram pairs. In that case, we fallback to combining + // histograms as usual to avoid increasing the header size. + bool tryCombine = curCombo.TrivialSymbol != NonTrivialSym || (histograms[idx].TrivialSymbol == NonTrivialSym && histograms[first].TrivialSymbol == NonTrivialSym); + int maxCombineFailures = 32; + if (tryCombine || binInfo[binId].NumCombineFailures >= maxCombineFailures) + { + // Move the (better) merged histogram to its final slot. + Vp8LHistogram tmp = curCombo; + curCombo = histograms[first]; + histograms[first] = tmp; + + histograms[idx] = null; + indicesToRemove.Add(idx); + clusterMappings[clusters[idx]] = clusters[first]; + } + else + { + binInfo[binId].NumCombineFailures++; + } + } + } + } + + foreach (int index in indicesToRemove.OrderByDescending(i => i)) + { + histograms.RemoveAt(index); + } + } + + /// + /// Given a Histogram set, the mapping of clusters 'clusterMapping' and the + /// current assignment of the cells in 'symbols', merge the clusters and assign the smallest possible clusters values. + /// + private static void OptimizeHistogramSymbols(ushort[] clusterMappings, int numClusters, ushort[] clusterMappingsTmp, ushort[] symbols) + { + bool doContinue = true; + + // First, assign the lowest cluster to each pixel. + while (doContinue) + { + doContinue = false; + for (int i = 0; i < numClusters; i++) + { + int k = clusterMappings[i]; + while (k != clusterMappings[k]) + { + clusterMappings[k] = clusterMappings[clusterMappings[k]]; + k = clusterMappings[k]; + } + + if (k != clusterMappings[i]) + { + doContinue = true; + clusterMappings[i] = (ushort)k; + } + } + } + + // Create a mapping from a cluster id to its minimal version. + int clusterMax = 0; + clusterMappingsTmp.AsSpan().Fill(0); + + // Re-map the ids. + for (int i = 0; i < symbols.Length; i++) + { + if (symbols[i] == InvalidHistogramSymbol) + { + continue; + } + + int cluster = clusterMappings[symbols[i]]; + if (cluster > 0 && clusterMappingsTmp[cluster] == 0) + { + clusterMax++; + clusterMappingsTmp[cluster] = (ushort)clusterMax; + } + + symbols[i] = clusterMappingsTmp[cluster]; + } + } + + /// + /// Perform histogram aggregation using a stochastic approach. + /// + /// true if a greedy approach needs to be performed afterwards, false otherwise. + private static bool HistogramCombineStochastic(List histograms, int minClusterSize) + { + uint seed = 1; + int triesWithNoSuccess = 0; + int numUsed = histograms.Count(h => h != null); + int outerIters = numUsed; + int numTriesNoSuccess = outerIters / 2; + + if (numUsed < minClusterSize) + { + return true; + } + + // Priority list of histogram pairs. Its size impacts the quality of the compression and the speed: + // the smaller the faster but the worse for the compression. + var histoPriorityList = new List(); + int maxSize = 9; + + // Fill the initial mapping. + int[] mappings = new int[histograms.Count]; + for (int j = 0, iter = 0; iter < histograms.Count; iter++) + { + if (histograms[iter] == null) + { + continue; + } + + mappings[j++] = iter; + } + + // Collapse similar histograms. + for (int iter = 0; iter < outerIters && numUsed >= minClusterSize && ++triesWithNoSuccess < numTriesNoSuccess; iter++) + { + double bestCost = histoPriorityList.Count == 0 ? 0.0d : histoPriorityList[0].CostDiff; + int numTries = numUsed / 2; + uint randRange = (uint)((numUsed - 1) * numUsed); + + // Pick random samples. + for (int j = 0; numUsed >= 2 && j < numTries; j++) + { + // Choose two different histograms at random and try to combine them. + uint tmp = MyRand(ref seed) % randRange; + int idx1 = (int)(tmp / (numUsed - 1)); + int idx2 = (int)(tmp % (numUsed - 1)); + if (idx2 >= idx1) + { + idx2++; + } + + idx1 = mappings[idx1]; + idx2 = mappings[idx2]; + + // Calculate cost reduction on combination. + double currCost = HistoPriorityListPush(histoPriorityList, maxSize, histograms, idx1, idx2, bestCost); + + // Found a better pair? + if (currCost < 0) + { + bestCost = currCost; + + if (histoPriorityList.Count == maxSize) + { + break; + } + } + } + + if (histoPriorityList.Count == 0) + { + continue; + } + + // Get the best histograms. + int bestIdx1 = histoPriorityList[0].Idx1; + int bestIdx2 = histoPriorityList[0].Idx2; + + int mappingIndex = Array.IndexOf(mappings, bestIdx2); + Span src = mappings.AsSpan(mappingIndex + 1, numUsed - mappingIndex - 1); + Span dst = mappings.AsSpan(mappingIndex); + src.CopyTo(dst); + + // Merge the histograms and remove bestIdx2 from the list. + HistogramAdd(histograms[bestIdx2], histograms[bestIdx1], histograms[bestIdx1]); + histograms.ElementAt(bestIdx1).BitCost = histoPriorityList[0].CostCombo; + histograms[bestIdx2] = null; + numUsed--; + + for (int j = 0; j < histoPriorityList.Count;) + { + HistogramPair p = histoPriorityList[j]; + bool isIdx1Best = p.Idx1 == bestIdx1 || p.Idx1 == bestIdx2; + bool isIdx2Best = p.Idx2 == bestIdx1 || p.Idx2 == bestIdx2; + bool doEval = false; + + // The front pair could have been duplicated by a random pick so + // check for it all the time nevertheless. + if (isIdx1Best && isIdx2Best) + { + histoPriorityList[j] = histoPriorityList[histoPriorityList.Count - 1]; + histoPriorityList.RemoveAt(histoPriorityList.Count - 1); + continue; + } + + // Any pair containing one of the two best indices should only refer to + // bestIdx1. Its cost should also be updated. + if (isIdx1Best) + { + p.Idx1 = bestIdx1; + doEval = true; + } + else if (isIdx2Best) + { + p.Idx2 = bestIdx1; + doEval = true; + } + + // Make sure the index order is respected. + if (p.Idx1 > p.Idx2) + { + int tmp = p.Idx2; + p.Idx2 = p.Idx1; + p.Idx1 = tmp; + } + + if (doEval) + { + // Re-evaluate the cost of an updated pair. + HistoListUpdatePair(histograms[p.Idx1], histograms[p.Idx2], 0.0d, p); + if (p.CostDiff >= 0.0d) + { + histoPriorityList[j] = histoPriorityList[histoPriorityList.Count - 1]; + histoPriorityList.RemoveAt(histoPriorityList.Count - 1); + continue; + } + } + + HistoListUpdateHead(histoPriorityList, p); + j++; + } + + triesWithNoSuccess = 0; + } + + bool doGreedy = numUsed <= minClusterSize; + + return doGreedy; + } + + private static void HistogramCombineGreedy(List histograms) + { + int histoSize = histograms.Count(h => h != null); + + // Priority list of histogram pairs. + var histoPriorityList = new List(); + int maxSize = histoSize * histoSize; + + for (int i = 0; i < histoSize; i++) + { + if (histograms[i] == null) + { + continue; + } + + for (int j = i + 1; j < histoSize; j++) + { + if (histograms[j] == null) + { + continue; + } + + HistoPriorityListPush(histoPriorityList, maxSize, histograms, i, j, 0.0d); + } + } + + while (histoPriorityList.Count > 0) + { + int idx1 = histoPriorityList[0].Idx1; + int idx2 = histoPriorityList[0].Idx2; + HistogramAdd(histograms[idx2], histograms[idx1], histograms[idx1]); + histograms[idx1].BitCost = histoPriorityList[0].CostCombo; + + // Remove merged histogram. + histograms[idx2] = null; + + // Remove pairs intersecting the just combined best pair. + for (int i = 0; i < histoPriorityList.Count;) + { + HistogramPair p = histoPriorityList.ElementAt(i); + if (p.Idx1 == idx1 || p.Idx2 == idx1 || p.Idx1 == idx2 || p.Idx2 == idx2) + { + // Replace item at pos i with the last one and shrinking the list. + histoPriorityList[i] = histoPriorityList[histoPriorityList.Count - 1]; + histoPriorityList.RemoveAt(histoPriorityList.Count - 1); + } + else + { + HistoListUpdateHead(histoPriorityList, p); + i++; + } + } + + // Push new pairs formed with combined histogram to the list. + for (int i = 0; i < histoSize; i++) + { + if (i == idx1 || histograms[i] == null) + { + continue; + } + + HistoPriorityListPush(histoPriorityList, maxSize, histograms, idx1, i, 0.0d); + } + } + } + + private static void HistogramRemap(List input, List output, ushort[] symbols) + { + int inSize = input.Count; + int outSize = output.Count; + if (outSize > 1) + { + for (int i = 0; i < inSize; i++) + { + if (input[i] == null) + { + // Arbitrarily set to the previous value if unused to help future LZ77. + symbols[i] = symbols[i - 1]; + continue; + } + + int bestOut = 0; + double bestBits = double.MaxValue; + for (int k = 0; k < outSize; k++) + { + double curBits = output[k].AddThresh(input[i], bestBits); + if (k == 0 || curBits < bestBits) + { + bestBits = curBits; + bestOut = k; + } + } + + symbols[i] = (ushort)bestOut; + } + } + else + { + for (int i = 0; i < inSize; i++) + { + symbols[i] = 0; + } + } + + // Recompute each output. + int paletteCodeBits = output.First().PaletteCodeBits; + output.Clear(); + for (int i = 0; i < outSize; i++) + { + output.Add(new Vp8LHistogram(paletteCodeBits)); + } + + for (int i = 0; i < inSize; i++) + { + if (input[i] == null) + { + continue; + } + + int idx = symbols[i]; + input[i].Add(output[idx], output[idx]); + } + } + + /// + /// Create a pair from indices "idx1" and "idx2" provided its cost is inferior to "threshold", a negative entropy. + /// + /// The cost of the pair, or 0 if it superior to threshold. + private static double HistoPriorityListPush(List histoList, int maxSize, List histograms, int idx1, int idx2, double threshold) + { + var pair = new HistogramPair(); + + if (histoList.Count == maxSize) + { + return 0.0d; + } + + if (idx1 > idx2) + { + int tmp = idx2; + idx2 = idx1; + idx1 = tmp; + } + + pair.Idx1 = idx1; + pair.Idx2 = idx2; + Vp8LHistogram h1 = histograms[idx1]; + Vp8LHistogram h2 = histograms[idx2]; + + HistoListUpdatePair(h1, h2, threshold, pair); + + // Do not even consider the pair if it does not improve the entropy. + if (pair.CostDiff >= threshold) + { + return 0.0d; + } + + histoList.Add(pair); + + HistoListUpdateHead(histoList, pair); + + return pair.CostDiff; + } + + /// + /// Update the cost diff and combo of a pair of histograms. This needs to be called when the the histograms have been merged with a third one. + /// + private static void HistoListUpdatePair(Vp8LHistogram h1, Vp8LHistogram h2, double threshold, HistogramPair pair) + { + double sumCost = h1.BitCost + h2.BitCost; + pair.CostCombo = 0.0d; + h1.GetCombinedHistogramEntropy(h2, sumCost + threshold, costInitial: pair.CostCombo, out double cost); + pair.CostCombo = cost; + pair.CostDiff = pair.CostCombo - sumCost; + } + + /// + /// Check whether a pair in the list should be updated as head or not. + /// + private static void HistoListUpdateHead(List histoList, HistogramPair pair) + { + if (pair.CostDiff < histoList[0].CostDiff) + { + // Replace the best pair. + int oldIdx = histoList.IndexOf(pair); + histoList[oldIdx] = histoList[0]; + histoList[0] = pair; + } + } + + private static void HistogramAdd(Vp8LHistogram a, Vp8LHistogram b, Vp8LHistogram output) + { + a.Add(b, output); + output.TrivialSymbol = a.TrivialSymbol == b.TrivialSymbol ? a.TrivialSymbol : NonTrivialSym; + } + + private static double GetCombineCostFactor(int histoSize, int quality) + { + double combineCostFactor = 0.16d; + if (quality < 90) + { + if (histoSize > 256) + { + combineCostFactor /= 2.0d; + } + + if (histoSize > 512) + { + combineCostFactor /= 2.0d; + } + + if (histoSize > 1024) + { + combineCostFactor /= 2.0d; + } + + if (quality <= 50) + { + combineCostFactor /= 2.0d; + } + } + + return combineCostFactor; + } + + // Implement a Lehmer random number generator with a multiplicative constant of 48271 and a modulo constant of 2^31 - 1. + [MethodImpl(InliningOptions.ShortMethod)] + private static uint MyRand(ref uint seed) + { + seed = (uint)(((ulong)seed * 48271u) % 2147483647u); + return seed; + } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/HistogramPair.cs b/src/ImageSharp/Formats/Webp/Lossless/HistogramPair.cs new file mode 100644 index 000000000..3cbc2062a --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/HistogramPair.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + /// + /// Pair of histograms. Negative Idx1 value means that pair is out-of-date. + /// + [DebuggerDisplay("Idx1: {Idx1}, Idx2: {Idx2}, CostDiff: {CostDiff}, CostCombo: {CostCombo}")] + internal class HistogramPair + { + public int Idx1 { get; set; } + + public int Idx2 { get; set; } + + public double CostDiff { get; set; } + + public double CostCombo { get; set; } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/HuffIndex.cs b/src/ImageSharp/Formats/Webp/Lossless/HuffIndex.cs new file mode 100644 index 000000000..c5b6aaec7 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/HuffIndex.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + /// + /// Five Huffman codes are used at each meta code. + /// + internal 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/Lossless/HuffmanCode.cs b/src/ImageSharp/Formats/Webp/Lossless/HuffmanCode.cs new file mode 100644 index 000000000..f75c64de1 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/HuffmanCode.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + /// + /// 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/Lossless/HuffmanTree.cs b/src/ImageSharp/Formats/Webp/Lossless/HuffmanTree.cs new file mode 100644 index 000000000..cd8be9aac --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/HuffmanTree.cs @@ -0,0 +1,64 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + /// + /// Represents the Huffman tree. + /// + [DebuggerDisplay("TotalCount = {TotalCount}, Value = {Value}, Left = {PoolIndexLeft}, Right = {PoolIndexRight}")] + internal struct HuffmanTree : IDeepCloneable + { + /// + /// Initializes a new instance of the struct. + /// + /// The HuffmanTree to create an instance from. + private HuffmanTree(HuffmanTree other) + { + this.TotalCount = other.TotalCount; + this.Value = other.Value; + this.PoolIndexLeft = other.PoolIndexLeft; + this.PoolIndexRight = other.PoolIndexRight; + } + + /// + /// Gets or sets the symbol frequency. + /// + public int TotalCount { get; set; } + + /// + /// Gets or sets the symbol value. + /// + public int Value { get; set; } + + /// + /// Gets or sets the index for the left sub-tree. + /// + public int PoolIndexLeft { get; set; } + + /// + /// Gets or sets the index for the right sub-tree. + /// + public int PoolIndexRight { get; set; } + + public static int Compare(HuffmanTree t1, HuffmanTree t2) + { + if (t1.TotalCount > t2.TotalCount) + { + return -1; + } + else if (t1.TotalCount < t2.TotalCount) + { + return 1; + } + else + { + return t1.Value < t2.Value ? -1 : 1; + } + } + + public IDeepCloneable DeepClone() => new HuffmanTree(this); + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/HuffmanTreeCode.cs b/src/ImageSharp/Formats/Webp/Lossless/HuffmanTreeCode.cs new file mode 100644 index 000000000..1b5173c63 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/HuffmanTreeCode.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + /// + /// Represents the tree codes (depth and bits array). + /// + internal struct HuffmanTreeCode + { + /// + /// Gets or sets the number of symbols. + /// + public int NumSymbols { get; set; } + + /// + /// Gets or sets the code lengths of the symbols. + /// + public byte[] CodeLengths { get; set; } + + /// + /// Gets or sets the symbol Codes. + /// + public short[] Codes { get; set; } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/HuffmanTreeToken.cs b/src/ImageSharp/Formats/Webp/Lossless/HuffmanTreeToken.cs new file mode 100644 index 000000000..159e9cd9c --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/HuffmanTreeToken.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + /// + /// Holds the tree header in coded form. + /// + [DebuggerDisplay("Code = {Code}, ExtraBits = {ExtraBits}")] + internal class HuffmanTreeToken + { + /// + /// Gets or sets the code. Value (0..15) or escape code (16, 17, 18). + /// + public byte Code { get; set; } + + /// + /// Gets or sets the extra bits for escape codes. + /// + public byte ExtraBits { get; set; } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs b/src/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs new file mode 100644 index 000000000..f2321d681 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs @@ -0,0 +1,656 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + /// + /// 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; + + // Pre-reversed 4-bit values. + private static readonly byte[] ReversedBits = + { + 0x0, 0x8, 0x4, 0xc, 0x2, 0xa, 0x6, 0xe, + 0x1, 0x9, 0x5, 0xd, 0x3, 0xb, 0x7, 0xf + }; + + public static void CreateHuffmanTree(uint[] histogram, int treeDepthLimit, bool[] bufRle, HuffmanTree[] huffTree, HuffmanTreeCode huffCode) + { + int numSymbols = huffCode.NumSymbols; + bufRle.AsSpan().Fill(false); + OptimizeHuffmanForRle(numSymbols, bufRle, histogram); + GenerateOptimalTree(huffTree, histogram, numSymbols, treeDepthLimit, huffCode.CodeLengths); + + // Create the actual bit codes for the bit lengths. + ConvertBitDepthsToSymbols(huffCode); + } + + /// + /// Change the population counts in a way that the consequent + /// Huffman tree compression, especially its RLE-part, give smaller output. + /// + public static void OptimizeHuffmanForRle(int length, bool[] goodForRle, uint[] counts) + { + // 1) Let's make the Huffman code more compatible with rle encoding. + for (; length >= 0; --length) + { + if (length == 0) + { + return; // All zeros. + } + + if (counts[length - 1] != 0) + { + // Now counts[0..length - 1] does not have trailing zeros. + break; + } + } + + // 2) Let's mark all population counts that already can be encoded with an rle code. + // Let's not spoil any of the existing good rle codes. + // Mark any seq of 0's that is longer as 5 as a goodForRle. + // Mark any seq of non-0's that is longer as 7 as a goodForRle. + uint symbol = counts[0]; + int stride = 0; + for (int i = 0; i < length + 1; i++) + { + if (i == length || counts[i] != symbol) + { + if ((symbol == 0 && stride >= 5) || (symbol != 0 && stride >= 7)) + { + for (int k = 0; k < stride; k++) + { + goodForRle[i - k - 1] = true; + } + } + + stride = 1; + if (i != length) + { + symbol = counts[i]; + } + } + else + { + ++stride; + } + } + + // 3) Let's replace those population counts that lead to more rle codes. + stride = 0; + uint limit = counts[0]; + uint sum = 0; + for (int i = 0; i < length + 1; i++) + { + if (i == length || goodForRle[i] || (i != 0 && goodForRle[i - 1]) || !ValuesShouldBeCollapsedToStrideAverage((int)counts[i], (int)limit)) + { + if (stride >= 4 || (stride >= 3 && sum == 0)) + { + uint k; + + // The stride must end, collapse what we have, if we have enough (4). + uint count = (uint)((sum + (stride / 2)) / stride); + if (count < 1) + { + count = 1; + } + + if (sum == 0) + { + // Don't make an all zeros stride to be upgraded to ones. + count = 0; + } + + for (k = 0; k < stride; k++) + { + // We don't want to change value at counts[i], + // that is already belonging to the next stride. Thus - 1. + counts[i - k - 1] = count; + } + } + + stride = 0; + sum = 0; + if (i < length - 3) + { + // All interesting strides have a count of at least 4, at least when non-zeros. + limit = (counts[i] + counts[i + 1] + + counts[i + 2] + counts[i + 3] + 2) / 4; + } + else if (i < length) + { + limit = counts[i]; + } + else + { + limit = 0; + } + } + + ++stride; + if (i != length) + { + sum += counts[i]; + if (stride >= 4) + { + limit = (uint)((sum + (stride / 2)) / stride); + } + } + } + } + + /// + /// Create an optimal Huffman tree. + /// + /// + /// The huffman tree. + /// The histogram. + /// The size of the histogram. + /// The tree depth limit. + /// How many bits are used for the symbol. + public static void GenerateOptimalTree(HuffmanTree[] tree, uint[] histogram, int histogramSize, int treeDepthLimit, byte[] bitDepths) + { + uint countMin; + int treeSizeOrig = 0; + + for (int i = 0; i < histogramSize; i++) + { + if (histogram[i] != 0) + { + ++treeSizeOrig; + } + } + + if (treeSizeOrig == 0) + { + return; + } + + Span treePool = tree.AsSpan(treeSizeOrig); + + // For block sizes with less than 64k symbols we never need to do a + // second iteration of this loop. + for (countMin = 1; ; countMin *= 2) + { + int treeSize = treeSizeOrig; + + // We need to pack the Huffman tree in treeDepthLimit bits. + // So, we try by faking histogram entries to be at least 'countMin'. + int idx = 0; + for (int j = 0; j < histogramSize; j++) + { + if (histogram[j] != 0) + { + uint count = histogram[j] < countMin ? countMin : histogram[j]; + tree[idx].TotalCount = (int)count; + tree[idx].Value = j; + tree[idx].PoolIndexLeft = -1; + tree[idx].PoolIndexRight = -1; + idx++; + } + } + + // Build the Huffman tree. + HuffmanTree[] treeCopy = tree.AsSpan().Slice(0, treeSize).ToArray(); + Array.Sort(treeCopy, HuffmanTree.Compare); + treeCopy.AsSpan().CopyTo(tree); + + if (treeSize > 1) + { + // Normal case. + int treePoolSize = 0; + while (treeSize > 1) + { + // Finish when we have only one root. + treePool[treePoolSize++] = (HuffmanTree)tree[treeSize - 1].DeepClone(); + treePool[treePoolSize++] = (HuffmanTree)tree[treeSize - 2].DeepClone(); + int count = treePool[treePoolSize - 1].TotalCount + treePool[treePoolSize - 2].TotalCount; + treeSize -= 2; + + // Search for the insertion point. + int k; + for (k = 0; k < treeSize; k++) + { + if (tree[k].TotalCount <= count) + { + break; + } + } + + int endIdx = k + 1; + int num = treeSize - k; + int startIdx = endIdx + num - 1; + for (int i = startIdx; i >= endIdx; i--) + { + tree[i] = (HuffmanTree)tree[i - 1].DeepClone(); + } + + tree[k].TotalCount = count; + tree[k].Value = -1; + tree[k].PoolIndexLeft = treePoolSize - 1; + tree[k].PoolIndexRight = treePoolSize - 2; + treeSize++; + } + + SetBitDepths(tree, treePool, bitDepths, 0); + } + else if (treeSize == 1) + { + // Trivial case: only one element. + bitDepths[tree[0].Value] = 1; + } + + // Test if this Huffman tree satisfies our 'treeDepthLimit' criteria. + int maxDepth = bitDepths[0]; + for (int j = 1; j < histogramSize; j++) + { + if (maxDepth < bitDepths[j]) + { + maxDepth = bitDepths[j]; + } + } + + if (maxDepth <= treeDepthLimit) + { + break; + } + } + } + + public static int CreateCompressedHuffmanTree(HuffmanTreeCode tree, HuffmanTreeToken[] tokensArray) + { + int depthSize = tree.NumSymbols; + int prevValue = 8; // 8 is the initial value for rle. + int i = 0; + int tokenPos = 0; + while (i < depthSize) + { + int value = tree.CodeLengths[i]; + int k = i + 1; + while (k < depthSize && tree.CodeLengths[k] == value) + { + k++; + } + + int runs = k - i; + if (value == 0) + { + tokenPos += CodeRepeatedZeros(runs, tokensArray.AsSpan(tokenPos)); + } + else + { + tokenPos += CodeRepeatedValues(runs, tokensArray.AsSpan(tokenPos), value, prevValue); + prevValue = value; + } + + i += runs; + } + + return tokenPos; + } + + 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. + int[] 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. + int[] counts = new int[WebpConstants.MaxAllowedCodeLength + 1]; // number of codes of each length. + int[] offsets = new int[WebpConstants.MaxAllowedCodeLength + 1]; // offsets in sorted table for each length. + + // Build histogram of code lengths. + for (symbol = 0; symbol < codeLengthsSize; ++symbol) + { + int codeLengthOfSymbol = codeLengths[symbol]; + if (codeLengthOfSymbol > WebpConstants.MaxAllowedCodeLength) + { + return 0; + } + + counts[codeLengthOfSymbol]++; + } + + // Error, all code lengths are zeros. + if (counts[0] == codeLengthsSize) + { + return 0; + } + + // Generate offsets into sorted symbol table by code length. + offsets[1] = 0; + for (len = 1; len < WebpConstants.MaxAllowedCodeLength; ++len) + { + int codesOfLength = counts[len]; + if (codesOfLength > 1 << len) + { + return 0; + } + + offsets[len + 1] = offsets[len] + codesOfLength; + } + + // Sort symbols by length, by symbol order within each length. + for (symbol = 0; symbol < codeLengthsSize; ++symbol) + { + int symbolCodeLength = codeLengths[symbol]; + if (symbolCodeLength > 0) + { + sorted[offsets[symbolCodeLength]++] = symbol; + } + } + + // Special case code with only one value. + if (offsets[WebpConstants.MaxAllowedCodeLength] == 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) + { + int countsLen = counts[len]; + numOpen <<= 1; + numNodes += numOpen; + numOpen -= counts[len]; + if (numOpen < 0) + { + return 0; + } + + for (; countsLen > 0; countsLen--) + { + var huffmanCode = new HuffmanCode() + { + BitsUsed = len, + Value = (uint)sorted[symbol++] + }; + ReplicateValue(table.Slice(key), step, tableSize, huffmanCode); + key = GetNextKey(key, len); + } + + counts[len] = countsLen; + } + + // 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 -= counts[len]; + if (numOpen < 0) + { + return 0; + } + + for (; counts[len] > 0; --counts[len]) + { + if ((key & mask) != low) + { + tableSpan = tableSpan.Slice(tableSize); + tablePos += tableSize; + tableBits = NextTableBitSize(counts, len, rootBits); + tableSize = 1 << tableBits; + totalSize += tableSize; + low = key & mask; + 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; + } + + private static int CodeRepeatedZeros(int repetitions, Span tokens) + { + int pos = 0; + while (repetitions >= 1) + { + if (repetitions < 3) + { + for (int i = 0; i < repetitions; i++) + { + tokens[pos].Code = 0; // 0-value + tokens[pos].ExtraBits = 0; + pos++; + } + + break; + } + else if (repetitions < 11) + { + tokens[pos].Code = 17; + tokens[pos].ExtraBits = (byte)(repetitions - 3); + pos++; + break; + } + else if (repetitions < 139) + { + tokens[pos].Code = 18; + tokens[pos].ExtraBits = (byte)(repetitions - 11); + pos++; + break; + } + else + { + tokens[pos].Code = 18; + tokens[pos].ExtraBits = 0x7f; // 138 repeated 0s + pos++; + repetitions -= 138; + } + } + + return pos; + } + + private static int CodeRepeatedValues(int repetitions, Span tokens, int value, int prevValue) + { + int pos = 0; + + if (value != prevValue) + { + tokens[pos].Code = (byte)value; + tokens[pos].ExtraBits = 0; + pos++; + repetitions--; + } + + while (repetitions >= 1) + { + if (repetitions < 3) + { + int i; + for (i = 0; i < repetitions; i++) + { + tokens[pos].Code = (byte)value; + tokens[pos].ExtraBits = 0; + pos++; + } + + break; + } + else if (repetitions < 7) + { + tokens[pos].Code = 16; + tokens[pos].ExtraBits = (byte)(repetitions - 3); + pos++; + break; + } + else + { + tokens[pos].Code = 16; + tokens[pos].ExtraBits = 3; + pos++; + repetitions -= 6; + } + } + + return pos; + } + + /// + /// Get the actual bit values for a tree of bit depths. + /// + /// The hiffman tree. + private static void ConvertBitDepthsToSymbols(HuffmanTreeCode tree) + { + // 0 bit-depth means that the symbol does not exist. + uint[] nextCode = new uint[WebpConstants.MaxAllowedCodeLength + 1]; + int[] depthCount = new int[WebpConstants.MaxAllowedCodeLength + 1]; + + int len = tree.NumSymbols; + for (int i = 0; i < len; i++) + { + int codeLength = tree.CodeLengths[i]; + depthCount[codeLength]++; + } + + depthCount[0] = 0; // ignore unused symbol. + nextCode[0] = 0; + + uint code = 0; + for (int i = 1; i <= WebpConstants.MaxAllowedCodeLength; i++) + { + code = (uint)((code + depthCount[i - 1]) << 1); + nextCode[i] = code; + } + + for (int i = 0; i < len; i++) + { + int codeLength = tree.CodeLengths[i]; + tree.Codes[i] = (short)ReverseBits(codeLength, nextCode[codeLength]++); + } + } + + private static void SetBitDepths(Span tree, Span pool, byte[] bitDepths, int level) + { + if (tree[0].PoolIndexLeft >= 0) + { + SetBitDepths(pool.Slice(tree[0].PoolIndexLeft), pool, bitDepths, level + 1); + SetBitDepths(pool.Slice(tree[0].PoolIndexRight), pool, bitDepths, level + 1); + } + else + { + bitDepths[tree[0].Value] = (byte)level; + } + } + + private static uint ReverseBits(int numBits, uint bits) + { + uint retval = 0; + int i = 0; + while (i < numBits) + { + i += 4; + retval |= (uint)(ReversedBits[bits & 0xf] << (WebpConstants.MaxAllowedCodeLength + 1 - i)); + bits >>= 4; + } + + retval >>= WebpConstants.MaxAllowedCodeLength + 1 - numBits; + return retval; + } + + /// + /// 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-step]. + /// 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; + } + + /// + /// Heuristics for selecting the stride ranges to collapse. + /// + private static bool ValuesShouldBeCollapsedToStrideAverage(int a, int b) => Math.Abs(a - b) < 4; + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs b/src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs new file mode 100644 index 000000000..b7f94415b --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs @@ -0,0 +1,1279 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; +#if SUPPORTS_RUNTIME_INTRINSICS +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +#endif + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + /// + /// Utility functions for the lossless decoder. + /// + internal static unsafe class LosslessUtils + { + private const int PrefixLookupIdxMax = 512; + + private const int LogLookupIdxMax = 256; + + private const int ApproxLogMax = 4096; + + private const int ApproxLogWithCorrectionMax = 65536; + + private const double Log2Reciprocal = 1.44269504088896338700465094007086; + + /// + /// Returns the exact index where array1 and array2 are different. For an index + /// inferior or equal to bestLenMatch, the return value just has to be strictly + /// inferior to bestLenMatch match. The current behavior is to return 0 if this index + /// is bestLenMatch, and the index itself otherwise. + /// If no two elements are the same, it returns maxLimit. + /// + public static int FindMatchLength(ReadOnlySpan array1, ReadOnlySpan array2, int bestLenMatch, int maxLimit) + { + // Before 'expensive' linear match, check if the two arrays match at the + // current best length index. + if (array1[bestLenMatch] != array2[bestLenMatch]) + { + return 0; + } + + return VectorMismatch(array1, array2, maxLimit); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static int VectorMismatch(ReadOnlySpan array1, ReadOnlySpan array2, int length) + { + int matchLen = 0; + + while (matchLen < length && array1[matchLen] == array2[matchLen]) + { + matchLen++; + } + + return matchLen; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static int MaxFindCopyLength(int len) => len < BackwardReferenceEncoder.MaxLength ? len : BackwardReferenceEncoder.MaxLength; + + public static int PrefixEncodeBits(int distance, ref int extraBits) + { + if (distance < PrefixLookupIdxMax) + { + (int Code, int ExtraBits) prefixCode = WebpLookupTables.PrefixEncodeCode[distance]; + extraBits = prefixCode.ExtraBits; + return prefixCode.Code; + } + + return PrefixEncodeBitsNoLut(distance, ref extraBits); + } + + public static int PrefixEncode(int distance, ref int extraBits, ref int extraBitsValue) + { + if (distance < PrefixLookupIdxMax) + { + (int Code, int ExtraBits) prefixCode = WebpLookupTables.PrefixEncodeCode[distance]; + extraBits = prefixCode.ExtraBits; + extraBitsValue = WebpLookupTables.PrefixEncodeExtraBitsValue[distance]; + + return prefixCode.Code; + } + + return PrefixEncodeNoLut(distance, ref extraBits, ref extraBitsValue); + } + + /// + /// 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) + { +#if SUPPORTS_RUNTIME_INTRINSICS + if (Avx2.IsSupported) + { + var mask = Vector256.Create(1, 255, 1, 255, 5, 255, 5, 255, 9, 255, 9, 255, 13, 255, 13, 255, 17, 255, 17, 255, 21, 255, 21, 255, 25, 255, 25, 255, 29, 255, 29, 255); + int numPixels = pixelData.Length; + fixed (uint* p = pixelData) + { + int i; + for (i = 0; i + 8 <= numPixels; i += 8) + { + uint* idx = p + i; + Vector256 input = Avx.LoadVector256((ushort*)idx).AsByte(); + Vector256 in0g0g = Avx2.Shuffle(input, mask); + Vector256 output = Avx2.Add(input, in0g0g); + Avx.Store((byte*)idx, output); + } + + if (i != numPixels) + { + AddGreenToBlueAndRedNoneVectorized(pixelData.Slice(i)); + } + } + } + else if (Ssse3.IsSupported) + { + var mask = Vector128.Create(1, 255, 1, 255, 5, 255, 5, 255, 9, 255, 9, 255, 13, 255, 13, 255); + int numPixels = pixelData.Length; + fixed (uint* p = pixelData) + { + int i; + for (i = 0; i + 4 <= numPixels; i += 4) + { + uint* idx = p + i; + Vector128 input = Sse2.LoadVector128((ushort*)idx).AsByte(); + Vector128 in0g0g = Ssse3.Shuffle(input, mask); + Vector128 output = Sse2.Add(input, in0g0g); + Sse2.Store((byte*)idx, output.AsByte()); + } + + if (i != numPixels) + { + AddGreenToBlueAndRedNoneVectorized(pixelData.Slice(i)); + } + } + } + else if (Sse2.IsSupported) + { + byte mask = SimdUtils.Shuffle.MmShuffle(2, 2, 0, 0); + int numPixels = pixelData.Length; + fixed (uint* p = pixelData) + { + int i; + for (i = 0; i + 4 <= numPixels; i += 4) + { + uint* idx = p + i; + Vector128 input = Sse2.LoadVector128((ushort*)idx); + Vector128 a = Sse2.ShiftRightLogical(input.AsUInt16(), 8); // 0 a 0 g + Vector128 b = Sse2.ShuffleLow(a, mask); + Vector128 c = Sse2.ShuffleHigh(b, mask); // 0g0g + Vector128 output = Sse2.Add(input.AsByte(), c.AsByte()); + Sse2.Store((byte*)idx, output); + } + + if (i != numPixels) + { + AddGreenToBlueAndRedNoneVectorized(pixelData.Slice(i)); + } + } + } + else +#endif + { + AddGreenToBlueAndRedNoneVectorized(pixelData); + } + } + + private static void AddGreenToBlueAndRedNoneVectorized(Span pixelData) + { + int numPixels = pixelData.Length; + for (int i = 0; i < numPixels; 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; + } + } + + public static void SubtractGreenFromBlueAndRed(Span pixelData) + { +#if SUPPORTS_RUNTIME_INTRINSICS + if (Avx2.IsSupported) + { + var mask = Vector256.Create(1, 255, 1, 255, 5, 255, 5, 255, 9, 255, 9, 255, 13, 255, 13, 255, 17, 255, 17, 255, 21, 255, 21, 255, 25, 255, 25, 255, 29, 255, 29, 255); + int numPixels = pixelData.Length; + fixed (uint* p = pixelData) + { + int i; + for (i = 0; i + 8 <= numPixels; i += 8) + { + uint* idx = p + i; + Vector256 input = Avx.LoadVector256((ushort*)idx).AsByte(); + Vector256 in0g0g = Avx2.Shuffle(input, mask); + Vector256 output = Avx2.Subtract(input, in0g0g); + Avx.Store((byte*)idx, output); + } + + if (i != numPixels) + { + SubtractGreenFromBlueAndRedNoneVectorized(pixelData.Slice(i)); + } + } + } + else if (Ssse3.IsSupported) + { + var mask = Vector128.Create(1, 255, 1, 255, 5, 255, 5, 255, 9, 255, 9, 255, 13, 255, 13, 255); + int numPixels = pixelData.Length; + fixed (uint* p = pixelData) + { + int i; + for (i = 0; i + 4 <= numPixels; i += 4) + { + uint* idx = p + i; + Vector128 input = Sse2.LoadVector128((ushort*)idx).AsByte(); + Vector128 in0g0g = Ssse3.Shuffle(input, mask); + Vector128 output = Sse2.Subtract(input, in0g0g); + Sse2.Store((byte*)idx, output.AsByte()); + } + + if (i != numPixels) + { + SubtractGreenFromBlueAndRedNoneVectorized(pixelData.Slice(i)); + } + } + } + else if (Sse2.IsSupported) + { + byte mask = SimdUtils.Shuffle.MmShuffle(2, 2, 0, 0); + int numPixels = pixelData.Length; + fixed (uint* p = pixelData) + { + int i; + for (i = 0; i + 4 <= numPixels; i += 4) + { + uint* idx = p + i; + Vector128 input = Sse2.LoadVector128((ushort*)idx); + Vector128 a = Sse2.ShiftRightLogical(input.AsUInt16(), 8); // 0 a 0 g + Vector128 b = Sse2.ShuffleLow(a, mask); + Vector128 c = Sse2.ShuffleHigh(b, mask); // 0g0g + Vector128 output = Sse2.Subtract(input.AsByte(), c.AsByte()); + Sse2.Store((byte*)idx, output); + } + + if (i != numPixels) + { + SubtractGreenFromBlueAndRedNoneVectorized(pixelData.Slice(i)); + } + } + } + else +#endif + { + SubtractGreenFromBlueAndRedNoneVectorized(pixelData); + } + } + + private static void SubtractGreenFromBlueAndRedNoneVectorized(Span pixelData) + { + int numPixels = pixelData.Length; + for (int i = 0; i < numPixels; i++) + { + uint argb = pixelData[i]; + uint green = (argb >> 8) & 0xff; + uint newR = (((argb >> 16) & 0xff) - green) & 0xff; + uint newB = (((argb >> 0) & 0xff) - green) & 0xff; + pixelData[i] = (argb & 0xff00ff00u) | (newR << 16) | newB; + } + } + + /// + /// If there are not many unique pixel values, it is 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; + + uint[] 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 we can just use a mask for that, instead of + // decrementing a counter. + if ((x & countMask) == 0) + { + packedPixels = GetArgbIndex(pixelData[pixelDataPos++]); + } + + decodedPixelData[decodedPixels++] = colorMap[(int)(packedPixels & bitMask)]; + packedPixels >>= bitsPerPixel; + } + } + + decodedPixelData.AsSpan().CopyTo(pixelData); + } + else + { + 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; + var 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.Slice(pixelPos, tileWidth)); + pixelPos += tileWidth; + } + + if (pixelPos < srcEnd) + { + uint colorCode = transformData[predRowIdx]; + ColorCodeToMultipliers(colorCode, ref m); + TransformColorInverse(m, pixelData.Slice(pixelPos, remainingWidth)); + pixelPos += remainingWidth; + } + + y++; + if ((y & mask) == 0) + { + predRowIdxStart += tilesPerRow; + } + } + } + + /// + /// 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 Vp8LMultipliers. + /// The pixel data to transform. + /// The number of pixels to process. + public static void TransformColor(Vp8LMultipliers m, Span data, int numPixels) + { +#if SUPPORTS_RUNTIME_INTRINSICS + if (Sse2.IsSupported) + { + Vector128 multsrb = MkCst16(Cst5b(m.GreenToRed), Cst5b(m.GreenToBlue)); + Vector128 multsb2 = MkCst16(Cst5b(m.RedToBlue), 0); + var maskalphagreen = Vector128.Create(0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255); + var maskredblue = Vector128.Create(255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0); + byte shufflemask = SimdUtils.Shuffle.MmShuffle(2, 2, 0, 0); + fixed (uint* src = data) + { + int idx; + for (idx = 0; idx + 4 <= numPixels; idx += 4) + { + uint* pos = src + idx; + Vector128 input = Sse2.LoadVector128(pos); + Vector128 a = Sse2.And(input.AsByte(), maskalphagreen); + Vector128 b = Sse2.ShuffleLow(a.AsInt16(), shufflemask); + Vector128 c = Sse2.ShuffleHigh(b.AsInt16(), shufflemask); + Vector128 d = Sse2.MultiplyHigh(c.AsInt16(), multsrb.AsInt16()); + Vector128 e = Sse2.ShiftLeftLogical(input.AsInt16(), 8); + Vector128 f = Sse2.MultiplyHigh(e.AsInt16(), multsb2.AsInt16()); + Vector128 g = Sse2.ShiftRightLogical(f.AsInt32(), 16); + Vector128 h = Sse2.Add(g.AsByte(), d.AsByte()); + Vector128 i = Sse2.And(h, maskredblue); + Vector128 output = Sse2.Subtract(input.AsByte(), i); + Sse2.Store((byte*)pos, output); + } + + if (idx != numPixels) + { + TransformColorNoneVectorized(m, data.Slice(idx), numPixels - idx); + } + } + } + else +#endif + { + TransformColorNoneVectorized(m, data, numPixels); + } + } + + private static void TransformColorNoneVectorized(Vp8LMultipliers m, Span data, int numPixels) + { + for (int i = 0; i < numPixels; i++) + { + uint argb = data[i]; + sbyte green = U32ToS8(argb >> 8); + sbyte red = U32ToS8(argb >> 16); + int newRed = red & 0xff; + int newBlue = (int)(argb & 0xff); + newRed -= ColorTransformDelta((sbyte)m.GreenToRed, green); + newRed &= 0xff; + newBlue -= ColorTransformDelta((sbyte)m.GreenToBlue, green); + newBlue -= ColorTransformDelta((sbyte)m.RedToBlue, red); + newBlue &= 0xff; + data[i] = (argb & 0xff00ff00u) | ((uint)newRed << 16) | (uint)newBlue; + } + } + + /// + /// Reverses the color space transform. + /// + /// The color transform element. + /// The pixel data to apply the inverse transform on. + public static void TransformColorInverse(Vp8LMultipliers m, Span pixelData) + { +#if SUPPORTS_RUNTIME_INTRINSICS + if (Sse2.IsSupported) + { + Vector128 multsrb = MkCst16(Cst5b(m.GreenToRed), Cst5b(m.GreenToBlue)); + Vector128 multsb2 = MkCst16(Cst5b(m.RedToBlue), 0); + var maskalphagreen = Vector128.Create(0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255); + byte shufflemask = SimdUtils.Shuffle.MmShuffle(2, 2, 0, 0); + fixed (uint* src = pixelData) + { + int idx; + for (idx = 0; idx + 4 <= pixelData.Length; idx += 4) + { + uint* pos = src + idx; + Vector128 input = Sse2.LoadVector128(pos); + Vector128 a = Sse2.And(input.AsByte(), maskalphagreen); + Vector128 b = Sse2.ShuffleLow(a.AsInt16(), shufflemask); + Vector128 c = Sse2.ShuffleHigh(b.AsInt16(), shufflemask); + Vector128 d = Sse2.MultiplyHigh(c.AsInt16(), multsrb.AsInt16()); + Vector128 e = Sse2.Add(input.AsByte(), d.AsByte()); + Vector128 f = Sse2.ShiftLeftLogical(e.AsInt16(), 8); + Vector128 g = Sse2.MultiplyHigh(f, multsb2.AsInt16()); + Vector128 h = Sse2.ShiftRightLogical(g.AsInt32(), 8); + Vector128 i = Sse2.Add(h.AsByte(), f.AsByte()); + Vector128 j = Sse2.ShiftRightLogical(i.AsInt16(), 8); + Vector128 output = Sse2.Or(j.AsByte(), a); + Sse2.Store((byte*)pos, output); + } + + if (idx != pixelData.Length) + { + TransformColorInverseNoneVectorized(m, pixelData.Slice(idx)); + } + } + } + else +#endif + { + TransformColorInverseNoneVectorized(m, pixelData); + } + } + + private static void TransformColorInverseNoneVectorized(Vp8LMultipliers m, Span pixelData) + { + for (int i = 0; i < pixelData.Length; 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, green); + newRed &= 0xff; + newBlue += ColorTransformDelta((sbyte)m.GreenToBlue, 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 outputSpan) + { + fixed (uint* inputFixed = pixelData) + { + fixed (uint* outputFixed = outputSpan) + { + uint* input = inputFixed; + uint* output = outputFixed; + + int width = transform.XSize; + Span transformData = transform.Data.GetSpan(); + + // First Row follows the L (mode=1) mode. + PredictorAdd0(input, 1, output); + PredictorAdd1(input + 1, width - 1, output + 1); + input += width; + output += width; + + int y = 1; + 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(input, output - width, 1, output); + + // .. the rest: + 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. + switch (predictorMode) + { + case 0: + PredictorAdd0(input + x, xEnd - x, output + x); + break; + case 1: + PredictorAdd1(input + x, xEnd - x, output + x); + break; + case 2: + PredictorAdd2(input + x, output + x - width, xEnd - x, output + x); + break; + case 3: + PredictorAdd3(input + x, output + x - width, xEnd - x, output + x); + break; + case 4: + PredictorAdd4(input + x, output + x - width, xEnd - x, output + x); + break; + case 5: + PredictorAdd5(input + x, output + x - width, xEnd - x, output + x); + break; + case 6: + PredictorAdd6(input + x, output + x - width, xEnd - x, output + x); + break; + case 7: + PredictorAdd7(input + x, output + x - width, xEnd - x, output + x); + break; + case 8: + PredictorAdd8(input + x, output + x - width, xEnd - x, output + x); + break; + case 9: + PredictorAdd9(input + x, output + x - width, xEnd - x, output + x); + break; + case 10: + PredictorAdd10(input + x, output + x - width, xEnd - x, output + x); + break; + case 11: + PredictorAdd11(input + x, output + x - width, xEnd - x, output + x); + break; + case 12: + PredictorAdd12(input + x, output + x - width, xEnd - x, output + x); + break; + case 13: + PredictorAdd13(input + x, output + x - width, xEnd - x, output + x); + break; + } + + x = xEnd; + } + + input += width; + output += width; + y++; + + if ((y & mask) == 0) + { + // Use the same mask, since tiles are squares. + predictorModeIdxBase += tilesPerRow; + } + } + } + } + + outputSpan.CopyTo(pixelData); + } + + 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 numColorsX4 = 4 * numColors; + int i; + for (i = 4; i < numColorsX4; i++) + { + // Equivalent to AddPixelEq(), on a byte-basis. + newData[i] = (byte)((data[i] + newData[i - 4]) & 0xff); + } + + int colorMapLength4 = 4 * newColorMap.Length; + for (; i < colorMapLength4; i++) + { + newData[i] = 0; // black tail. + } + } + + /// + /// Difference of each component, mod 256. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public 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); + } + + /// + /// Bundles multiple (1, 2, 4 or 8) pixels into a single pixel. + /// + public static void BundleColorMap(Span row, int width, int xBits, Span dst) + { + int x; + if (xBits > 0) + { + int bitDepth = 1 << (3 - xBits); + int mask = (1 << xBits) - 1; + uint code = 0xff000000; + for (x = 0; x < width; x++) + { + int xsub = x & mask; + if (xsub == 0) + { + code = 0xff000000; + } + + code |= (uint)(row[x] << (8 + (bitDepth * xsub))); + dst[x >> xBits] = code; + } + } + else + { + for (x = 0; x < width; x++) + { + dst[x] = (uint)(0xff000000 | (row[x] << 8)); + } + } + } + + /// + /// Compute the combined Shanon's entropy for distribution {X} and {X+Y}. + /// + /// Shanon entropy. + public static float CombinedShannonEntropy(int[] x, int[] y) + { + double retVal = 0.0d; + uint sumX = 0, sumXY = 0; + for (int i = 0; i < 256; i++) + { + uint xi = (uint)x[i]; + if (xi != 0) + { + uint xy = xi + (uint)y[i]; + sumX += xi; + retVal -= FastSLog2(xi); + sumXY += xy; + retVal -= FastSLog2(xy); + } + else if (y[i] != 0) + { + sumXY += (uint)y[i]; + retVal -= FastSLog2((uint)y[i]); + } + } + + retVal += FastSLog2(sumX) + FastSLog2(sumXY); + return (float)retVal; + } + + public static byte TransformColorRed(sbyte greenToRed, uint argb) + { + sbyte green = U32ToS8(argb >> 8); + int newRed = (int)(argb >> 16); + newRed -= ColorTransformDelta(greenToRed, green); + return (byte)(newRed & 0xff); + } + + public static byte TransformColorBlue(sbyte greenToBlue, sbyte redToBlue, uint argb) + { + sbyte green = U32ToS8(argb >> 8); + sbyte red = U32ToS8(argb >> 16); + int newBlue = (int)(argb & 0xff); + newBlue -= ColorTransformDelta(greenToBlue, green); + newBlue -= ColorTransformDelta(redToBlue, red); + return (byte)(newBlue & 0xff); + } + + /// + /// Fast calculation of log2(v) for integer input. + /// + public static float FastLog2(uint v) => v < LogLookupIdxMax ? WebpLookupTables.Log2Table[v] : FastLog2Slow(v); + + /// + /// Fast calculation of v * log2(v) for integer input. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public static float FastSLog2(uint v) => v < LogLookupIdxMax ? WebpLookupTables.SLog2Table[v] : FastSLog2Slow(v); + + [MethodImpl(InliningOptions.ShortMethod)] + public 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); + } + + // Converts near lossless quality into max number of bits shaved off. + // 100 -> 0 + // 80..99 -> 1 + // 60..79 -> 2 + // 40..59 -> 3 + // 20..39 -> 4 + // 0..19 -> 5 + [MethodImpl(InliningOptions.ShortMethod)] + public static int NearLosslessBits(int nearLosslessQuality) => 5 - (nearLosslessQuality / 20); + + private static float FastSLog2Slow(uint v) + { + Guard.MustBeGreaterThanOrEqualTo(v, LogLookupIdxMax, nameof(v)); + if (v < ApproxLogWithCorrectionMax) + { + int logCnt = 0; + uint y = 1; + float vF = v; + uint origV = v; + do + { + ++logCnt; + v >>= 1; + y <<= 1; + } + while (v >= LogLookupIdxMax); + + // vf = (2^log_cnt) * Xf; where y = 2^log_cnt and Xf < 256 + // Xf = floor(Xf) * (1 + (v % y) / v) + // log2(Xf) = log2(floor(Xf)) + log2(1 + (v % y) / v) + // The correction factor: log(1 + d) ~ d; for very small d values, so + // log2(1 + (v % y) / v) ~ LOG_2_RECIPROCAL * (v % y)/v + // LOG_2_RECIPROCAL ~ 23/16 + int correction = (int)((23 * (origV & (y - 1))) >> 4); + return (vF * (WebpLookupTables.Log2Table[v] + logCnt)) + correction; + } + else + { + return (float)(Log2Reciprocal * v * Math.Log(v)); + } + } + + private static float FastLog2Slow(uint v) + { + Guard.MustBeGreaterThanOrEqualTo(v, LogLookupIdxMax, nameof(v)); + if (v < ApproxLogWithCorrectionMax) + { + int logCnt = 0; + uint y = 1; + uint origV = v; + do + { + ++logCnt; + v >>= 1; + y <<= 1; + } + while (v >= LogLookupIdxMax); + + double log2 = WebpLookupTables.Log2Table[v] + logCnt; + if (origV >= ApproxLogMax) + { + // Since the division is still expensive, add this correction factor only + // for large values of 'v'. + int correction = (int)(23 * (origV & (y - 1))) >> 4; + log2 += (double)correction / origV; + } + + return (float)log2; + } + + return (float)(Log2Reciprocal * Math.Log(v)); + } + + /// + /// Splitting of distance and length codes into prefixes and + /// extra bits. The prefixes are encoded with an entropy code + /// while the extra bits are stored just as normal bits. + /// + private static int PrefixEncodeBitsNoLut(int distance, ref int extraBits) + { + int highestBit = Numerics.Log2((uint)--distance); + int secondHighestBit = (distance >> (highestBit - 1)) & 1; + extraBits = highestBit - 1; + int code = (2 * highestBit) + secondHighestBit; + return code; + } + + private static int PrefixEncodeNoLut(int distance, ref int extraBits, ref int extraBitsValue) + { + int highestBit = Numerics.Log2((uint)--distance); + int secondHighestBit = (distance >> (highestBit - 1)) & 1; + extraBits = highestBit - 1; + extraBitsValue = distance & ((1 << extraBits) - 1); + int code = (2 * highestBit) + secondHighestBit; + return code; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd0(uint* input, int numberOfPixels, uint* output) + { + for (int x = 0; x < numberOfPixels; x++) + { + output[x] = AddPixels(input[x], WebpConstants.ArgbBlack); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd1(uint* input, int numberOfPixels, uint* output) + { + uint left = output[-1]; + for (int x = 0; x < numberOfPixels; x++) + { + output[x] = left = AddPixels(input[x], left); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd2(uint* input, uint* upper, int numberOfPixels, uint* output) + { + for (int x = 0; x < numberOfPixels; x++) + { + uint pred = Predictor2(output[x - 1], upper + x); + output[x] = AddPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd3(uint* input, uint* upper, int numberOfPixels, uint* output) + { + for (int x = 0; x < numberOfPixels; x++) + { + uint pred = Predictor3(output[x - 1], upper + x); + output[x] = AddPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd4(uint* input, uint* upper, int numberOfPixels, uint* output) + { + for (int x = 0; x < numberOfPixels; x++) + { + uint pred = Predictor4(output[x - 1], upper + x); + output[x] = AddPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd5(uint* input, uint* upper, int numberOfPixels, uint* output) + { + for (int x = 0; x < numberOfPixels; x++) + { + uint pred = Predictor5(output[x - 1], upper + x); + output[x] = AddPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd6(uint* input, uint* upper, int numberOfPixels, uint* output) + { + for (int x = 0; x < numberOfPixels; x++) + { + uint pred = Predictor6(output[x - 1], upper + x); + output[x] = AddPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd7(uint* input, uint* upper, int numberOfPixels, uint* output) + { + for (int x = 0; x < numberOfPixels; x++) + { + uint pred = Predictor7(output[x - 1], upper + x); + output[x] = AddPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd8(uint* input, uint* upper, int numberOfPixels, uint* output) + { + for (int x = 0; x < numberOfPixels; x++) + { + uint pred = Predictor8(output[x - 1], upper + x); + output[x] = AddPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd9(uint* input, uint* upper, int numberOfPixels, uint* output) + { + for (int x = 0; x < numberOfPixels; x++) + { + uint pred = Predictor9(output[x - 1], upper + x); + output[x] = AddPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd10(uint* input, uint* upper, int numberOfPixels, uint* output) + { + for (int x = 0; x < numberOfPixels; x++) + { + uint pred = Predictor10(output[x - 1], upper + x); + output[x] = AddPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd11(uint* input, uint* upper, int numberOfPixels, uint* output) + { + for (int x = 0; x < numberOfPixels; x++) + { + uint pred = Predictor11(output[x - 1], upper + x); + output[x] = AddPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd12(uint* input, uint* upper, int numberOfPixels, uint* output) + { + for (int x = 0; x < numberOfPixels; x++) + { + uint pred = Predictor12(output[x - 1], upper + x); + output[x] = AddPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void PredictorAdd13(uint* input, uint* upper, int numberOfPixels, uint* output) + { + for (int x = 0; x < numberOfPixels; x++) + { + uint pred = Predictor13(output[x - 1], upper + x); + output[x] = AddPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static uint Predictor2(uint left, uint* top) => top[0]; + + [MethodImpl(InliningOptions.ShortMethod)] + public static uint Predictor3(uint left, uint* top) => top[1]; + + [MethodImpl(InliningOptions.ShortMethod)] + public static uint Predictor4(uint left, uint* top) => top[-1]; + + [MethodImpl(InliningOptions.ShortMethod)] + public static uint Predictor5(uint left, uint* top) => Average3(left, top[0], top[1]); + + [MethodImpl(InliningOptions.ShortMethod)] + public static uint Predictor6(uint left, uint* top) => Average2(left, top[-1]); + + [MethodImpl(InliningOptions.ShortMethod)] + public static uint Predictor7(uint left, uint* top) => Average2(left, top[0]); + + [MethodImpl(InliningOptions.ShortMethod)] + public static uint Predictor8(uint left, uint* top) => Average2(top[-1], top[0]); + + [MethodImpl(InliningOptions.ShortMethod)] + public static uint Predictor9(uint left, uint* top) => Average2(top[0], top[1]); + + [MethodImpl(InliningOptions.ShortMethod)] + public static uint Predictor10(uint left, uint* top) => Average4(left, top[-1], top[0], top[1]); + + [MethodImpl(InliningOptions.ShortMethod)] + public static uint Predictor11(uint left, uint* top) => Select(top[0], left, top[-1]); + + [MethodImpl(InliningOptions.ShortMethod)] + public static uint Predictor12(uint left, uint* top) => ClampedAddSubtractFull(left, top[0], top[-1]); + + [MethodImpl(InliningOptions.ShortMethod)] + public static uint Predictor13(uint left, uint* top) => ClampedAddSubtractHalf(left, top[0], top[-1]); + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub0(uint* input, int numPixels, uint* output) + { + for (int i = 0; i < numPixels; i++) + { + output[i] = SubPixels(input[i], WebpConstants.ArgbBlack); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub1(uint* input, int numPixels, uint* output) + { + for (int i = 0; i < numPixels; i++) + { + output[i] = SubPixels(input[i], input[i - 1]); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub2(uint* input, uint* upper, int numPixels, uint* output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor2(input[x - 1], upper + x); + output[x] = SubPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub3(uint* input, uint* upper, int numPixels, uint* output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor3(input[x - 1], upper + x); + output[x] = SubPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub4(uint* input, uint* upper, int numPixels, uint* output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor4(input[x - 1], upper + x); + output[x] = SubPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub5(uint* input, uint* upper, int numPixels, uint* output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor5(input[x - 1], upper + x); + output[x] = SubPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub6(uint* input, uint* upper, int numPixels, uint* output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor6(input[x - 1], upper + x); + output[x] = SubPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub7(uint* input, uint* upper, int numPixels, uint* output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor7(input[x - 1], upper + x); + output[x] = SubPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub8(uint* input, uint* upper, int numPixels, uint* output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor8(input[x - 1], upper + x); + output[x] = SubPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub9(uint* input, uint* upper, int numPixels, uint* output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor9(input[x - 1], upper + x); + output[x] = SubPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub10(uint* input, uint* upper, int numPixels, uint* output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor10(input[x - 1], upper + x); + output[x] = SubPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub11(uint* input, uint* upper, int numPixels, uint* output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor11(input[x - 1], upper + x); + output[x] = SubPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub12(uint* input, uint* upper, int numPixels, uint* output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor12(input[x - 1], upper + x); + output[x] = SubPixels(input[x], pred); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void PredictorSub13(uint* input, uint* upper, int numPixels, uint* output) + { + for (int x = 0; x < numPixels; x++) + { + uint pred = Predictor13(input[x - 1], upper + x); + output[x] = SubPixels(input[x], pred); + } + } + + /// + /// Computes sampled size of 'size' when sampling using 'sampling bits'. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public static int SubSampleSize(int size, int samplingBits) => (size + (1 << samplingBits) - 1) >> samplingBits; + + /// + /// Sum of each component, mod 256. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public static uint AddPixels(uint a, uint b) + { + uint alphaAndGreen = (a & 0xff00ff00u) + (b & 0xff00ff00u); + uint redAndBlue = (a & 0x00ff00ffu) + (b & 0x00ff00ffu); + return (alphaAndGreen & 0xff00ff00u) | (redAndBlue & 0x00ff00ffu); + } + + // For sign-extended multiplying constants, pre-shifted by 5: + [MethodImpl(InliningOptions.ShortMethod)] + public static short Cst5b(int x) => (short)(((short)(x << 8)) >> 5); + + 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; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int AddSubtractComponentHalf(int a, int b) => (int)Clip255((uint)(a + ((a - b) / 2))); + + [MethodImpl(InliningOptions.ShortMethod)] + private static int AddSubtractComponentFull(int a, int b, int c) => (int)Clip255((uint)(a + b - c)); + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint Clip255(uint a) => a < 256 ? a : ~a >> 24; + +#if SUPPORTS_RUNTIME_INTRINSICS + [MethodImpl(InliningOptions.ShortMethod)] + private static Vector128 MkCst16(int hi, int lo) => Vector128.Create((hi << 16) | (lo & 0xffff)); +#endif + + 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; + } + + [MethodImpl(InliningOptions.ShortMethod)] + 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); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint Average2(uint a0, uint a1) => (((a0 ^ a1) & 0xfefefefeu) >> 1) + (a0 & a1); + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint Average3(uint a0, uint a1, uint a2) => Average2(Average2(a0, a2), a1); + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint Average4(uint a0, uint a1, uint a2, uint a3) => Average2(Average2(a0, a1), Average2(a2, a3)); + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint GetArgbIndex(uint idx) => (idx >> 8) & 0xff; + + [MethodImpl(InliningOptions.ShortMethod)] + private static int ColorTransformDelta(sbyte colorPred, sbyte color) => (colorPred * color) >> 5; + + [MethodImpl(InliningOptions.ShortMethod)] + private static sbyte U32ToS8(uint v) => (sbyte)(v & 0xff); + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/NearLosslessEnc.cs b/src/ImageSharp/Formats/Webp/Lossless/NearLosslessEnc.cs new file mode 100644 index 000000000..7a26a1073 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/NearLosslessEnc.cs @@ -0,0 +1,125 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + /// + /// Near-lossless image preprocessing adjusts pixel values to help compressibility with a guarantee + /// of maximum deviation between original and resulting pixel values. + /// + internal static class NearLosslessEnc + { + private const int MinDimForNearLossless = 64; + + public static void ApplyNearLossless(int xSize, int ySize, int quality, Span argbSrc, Span argbDst, int stride) + { + uint[] copyBuffer = new uint[xSize * 3]; + int limitBits = LosslessUtils.NearLosslessBits(quality); + + // For small icon images, don't attempt to apply near-lossless compression. + if ((xSize < MinDimForNearLossless && ySize < MinDimForNearLossless) || ySize < 3) + { + for (int i = 0; i < ySize; i++) + { + argbSrc.Slice(i * stride, xSize).CopyTo(argbDst.Slice(i * xSize, xSize)); + } + + return; + } + + NearLossless(xSize, ySize, argbSrc, stride, limitBits, copyBuffer, argbDst); + for (int i = limitBits - 1; i != 0; i--) + { + NearLossless(xSize, ySize, argbDst, xSize, i, copyBuffer, argbDst); + } + } + + // Adjusts pixel values of image with given maximum error. + private static void NearLossless(int xSize, int ySize, Span argbSrc, int stride, int limitBits, Span copyBuffer, Span argbDst) + { + int y; + int limit = 1 << limitBits; + Span prevRow = copyBuffer; + Span currRow = copyBuffer.Slice(xSize, xSize); + Span nextRow = copyBuffer.Slice(xSize * 2, xSize); + argbSrc.Slice(0, xSize).CopyTo(currRow); + argbSrc.Slice(xSize, xSize).CopyTo(nextRow); + + int srcOffset = 0; + int dstOffset = 0; + for (y = 0; y < ySize; y++) + { + if (y == 0 || y == ySize - 1) + { + argbSrc.Slice(srcOffset, xSize).CopyTo(argbDst.Slice(dstOffset, xSize)); + } + else + { + argbSrc.Slice(srcOffset + stride, xSize).CopyTo(nextRow); + argbDst[dstOffset] = argbSrc[srcOffset]; + argbDst[dstOffset + xSize - 1] = argbSrc[srcOffset + xSize - 1]; + for (int x = 1; x < xSize - 1; x++) + { + if (IsSmooth(prevRow, currRow, nextRow, x, limit)) + { + argbDst[dstOffset + x] = currRow[x]; + } + else + { + argbDst[dstOffset + x] = ClosestDiscretizedArgb(currRow[x], limitBits); + } + } + } + + Span temp = prevRow; + prevRow = currRow; + currRow = nextRow; + nextRow = temp; + srcOffset += stride; + dstOffset += xSize; + } + } + + // Applies FindClosestDiscretized to all channels of pixel. + private static uint ClosestDiscretizedArgb(uint a, int bits) => + (FindClosestDiscretized(a >> 24, bits) << 24) | + (FindClosestDiscretized((a >> 16) & 0xff, bits) << 16) | + (FindClosestDiscretized((a >> 8) & 0xff, bits) << 8) | + FindClosestDiscretized(a & 0xff, bits); + + private static uint FindClosestDiscretized(uint a, int bits) + { + uint mask = (1u << bits) - 1; + uint biased = a + (mask >> 1) + ((a >> bits) & 1); + if (biased > 0xff) + { + return 0xff; + } + + return biased & ~mask; + } + + private static bool IsSmooth(Span prevRow, Span currRow, Span nextRow, int ix, int limit) => + IsNear(currRow[ix], currRow[ix - 1], limit) && // Check that all pixels in 4-connected neighborhood are smooth. + IsNear(currRow[ix], currRow[ix + 1], limit) && + IsNear(currRow[ix], prevRow[ix], limit) && + IsNear(currRow[ix], nextRow[ix], limit); + + // Checks if distance between corresponding channel values of pixels a and b is within the given limit. + private static bool IsNear(uint a, uint b, int limit) + { + for (int k = 0; k < 4; ++k) + { + int delta = (int)((a >> (k * 8)) & 0xff) - (int)((b >> (k * 8)) & 0xff); + if (delta >= limit || delta <= -limit) + { + return false; + } + } + + return true; + } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs b/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs new file mode 100644 index 000000000..2d71a7af6 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + [DebuggerDisplay("Mode: {Mode}, Len: {Len}, BgraOrDistance: {BgraOrDistance}")] + internal class PixOrCopy + { + public PixOrCopyMode Mode { get; set; } + + public ushort Len { get; set; } + + public uint BgraOrDistance { get; set; } + + public static PixOrCopy CreateCacheIdx(int idx) => + new PixOrCopy() + { + Mode = PixOrCopyMode.CacheIdx, + BgraOrDistance = (uint)idx, + Len = 1 + }; + + public static PixOrCopy CreateLiteral(uint bgra) => + new PixOrCopy() + { + Mode = PixOrCopyMode.Literal, + BgraOrDistance = bgra, + Len = 1 + }; + + public static PixOrCopy CreateCopy(uint distance, ushort len) => new PixOrCopy() + { + Mode = PixOrCopyMode.Copy, + BgraOrDistance = distance, + Len = len + }; + + public uint Literal(int component) => (this.BgraOrDistance >> (component * 8)) & 0xff; + + public uint CacheIdx() => this.BgraOrDistance; + + public ushort Length() => this.Len; + + public uint Distance() => this.BgraOrDistance; + + public bool IsLiteral() => this.Mode == PixOrCopyMode.Literal; + + public bool IsCacheIdx() => this.Mode == PixOrCopyMode.CacheIdx; + + public bool IsCopy() => this.Mode == PixOrCopyMode.Copy; + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/PixOrCopyMode.cs b/src/ImageSharp/Formats/Webp/Lossless/PixOrCopyMode.cs new file mode 100644 index 000000000..0d7023ffc --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/PixOrCopyMode.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + internal enum PixOrCopyMode + { + Literal, + + CacheIdx, + + Copy, + + None + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/PredictorEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/PredictorEncoder.cs new file mode 100644 index 000000000..671e9a043 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/PredictorEncoder.cs @@ -0,0 +1,1181 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +#if SUPPORTS_RUNTIME_INTRINSICS +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +#endif + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + /// + /// Image transform methods for the lossless webp encoder. + /// + internal static unsafe class PredictorEncoder + { + private const int GreenRedToBlueNumAxis = 8; + + private const int GreenRedToBlueMaxIters = 7; + + private const float MaxDiffCost = 1e30f; + + private const uint MaskAlpha = 0xff000000; + + private const float SpatialPredictorBias = 15.0f; + + private const int PredLowEffort = 11; + + /// + /// Finds the best predictor for each tile, and converts the image to residuals + /// with respect to predictions. If nearLosslessQuality < 100, applies + /// near lossless processing, shaving off more bits of residuals for lower qualities. + /// + public static void ResidualImage( + int width, + int height, + int bits, + Span bgra, + Span bgraScratch, + Span image, + bool nearLossless, + int nearLosslessQuality, + WebpTransparentColorMode transparentColorMode, + bool usedSubtractGreen, + bool lowEffort) + { + int tilesPerRow = LosslessUtils.SubSampleSize(width, bits); + int tilesPerCol = LosslessUtils.SubSampleSize(height, bits); + int maxQuantization = 1 << LosslessUtils.NearLosslessBits(nearLosslessQuality); + + // TODO: Can we optimize this? + int[][] histo = new int[4][]; + for (int i = 0; i < 4; i++) + { + histo[i] = new int[256]; + } + + if (lowEffort) + { + for (int i = 0; i < tilesPerRow * tilesPerCol; i++) + { + image[i] = WebpConstants.ArgbBlack | (PredLowEffort << 8); + } + } + else + { + for (int tileY = 0; tileY < tilesPerCol; tileY++) + { + for (int tileX = 0; tileX < tilesPerRow; tileX++) + { + int pred = GetBestPredictorForTile( + width, + height, + tileX, + tileY, + bits, + histo, + bgraScratch, + bgra, + maxQuantization, + transparentColorMode, + usedSubtractGreen, + nearLossless, + image); + + image[(tileY * tilesPerRow) + tileX] = (uint)(WebpConstants.ArgbBlack | (pred << 8)); + } + } + } + + CopyImageWithPrediction( + width, + height, + bits, + image, + bgraScratch, + bgra, + maxQuantization, + transparentColorMode, + usedSubtractGreen, + nearLossless, + lowEffort); + } + + public static void ColorSpaceTransform(int width, int height, int bits, int quality, Span bgra, Span image) + { + int maxTileSize = 1 << bits; + int tileXSize = LosslessUtils.SubSampleSize(width, bits); + int tileYSize = LosslessUtils.SubSampleSize(height, bits); + int[] accumulatedRedHisto = new int[256]; + int[] accumulatedBlueHisto = new int[256]; + var prevX = default(Vp8LMultipliers); + var prevY = default(Vp8LMultipliers); + for (int tileY = 0; tileY < tileYSize; tileY++) + { + for (int tileX = 0; tileX < tileXSize; tileX++) + { + int tileXOffset = tileX * maxTileSize; + int tileYOffset = tileY * maxTileSize; + int allXMax = GetMin(tileXOffset + maxTileSize, width); + int allYMax = GetMin(tileYOffset + maxTileSize, height); + int offset = (tileY * tileXSize) + tileX; + if (tileY != 0) + { + LosslessUtils.ColorCodeToMultipliers(image[offset - tileXSize], ref prevY); + } + + prevX = GetBestColorTransformForTile( + tileX, + tileY, + bits, + prevX, + prevY, + quality, + width, + height, + accumulatedRedHisto, + accumulatedBlueHisto, + bgra); + + image[offset] = MultipliersToColorCode(prevX); + CopyTileWithColorTransform(width, height, tileXOffset, tileYOffset, maxTileSize, prevX, bgra); + + // Gather accumulated histogram data. + for (int y = tileYOffset; y < allYMax; y++) + { + int ix = (y * width) + tileXOffset; + int ixEnd = ix + allXMax - tileXOffset; + + for (; ix < ixEnd; ix++) + { + uint pix = bgra[ix]; + if (ix >= 2 && pix == bgra[ix - 2] && pix == bgra[ix - 1]) + { + continue; // Repeated pixels are handled by backward references. + } + + if (ix >= width + 2 && bgra[ix - 2] == bgra[ix - width - 2] && bgra[ix - 1] == bgra[ix - width - 1] && pix == bgra[ix - width]) + { + continue; // Repeated pixels are handled by backward references. + } + + accumulatedRedHisto[(pix >> 16) & 0xff]++; + accumulatedBlueHisto[(pix >> 0) & 0xff]++; + } + } + } + } + } + + /// + /// Returns best predictor and updates the accumulated histogram. + /// If maxQuantization > 1, assumes that near lossless processing will be + /// applied, quantizing residuals to multiples of quantization levels up to + /// maxQuantization (the actual quantization level depends on smoothness near + /// the given pixel). + /// + /// Best predictor. + private static int GetBestPredictorForTile( + int width, + int height, + int tileX, + int tileY, + int bits, + int[][] accumulated, + Span argbScratch, + Span argb, + int maxQuantization, + WebpTransparentColorMode transparentColorMode, + bool usedSubtractGreen, + bool nearLossless, + Span modes) + { + const int numPredModes = 14; + int startX = tileX << bits; + int startY = tileY << bits; + int tileSize = 1 << bits; + int maxY = GetMin(tileSize, height - startY); + int maxX = GetMin(tileSize, width - startX); + + // Whether there exist columns just outside the tile. + int haveLeft = startX > 0 ? 1 : 0; + + // Position and size of the strip covering the tile and adjacent columns if they exist. + int contextStartX = startX - haveLeft; + int contextWidth = maxX + haveLeft + (maxX < width ? 1 : 0) - startX; + int tilesPerRow = LosslessUtils.SubSampleSize(width, bits); + + // Prediction modes of the left and above neighbor tiles. + int leftMode = (int)(tileX > 0 ? (modes[(tileY * tilesPerRow) + tileX - 1] >> 8) & 0xff : 0xff); + int aboveMode = (int)(tileY > 0 ? (modes[((tileY - 1) * tilesPerRow) + tileX] >> 8) & 0xff : 0xff); + + // The width of upper_row and current_row is one pixel larger than image width + // to allow the top right pixel to point to the leftmost pixel of the next row + // when at the right edge. + Span upperRow = argbScratch; + Span currentRow = upperRow.Slice(width + 1); + Span maxDiffs = MemoryMarshal.Cast(currentRow.Slice(width + 1)); + float bestDiff = MaxDiffCost; + int bestMode = 0; + uint[] residuals = new uint[1 << WebpConstants.MaxTransformBits]; + int[][] histoArgb = new int[4][]; + int[][] bestHisto = new int[4][]; + for (int i = 0; i < 4; i++) + { + histoArgb[i] = new int[256]; + bestHisto[i] = new int[256]; + } + + for (int mode = 0; mode < numPredModes; mode++) + { + for (int i = 0; i < 4; i++) + { + histoArgb[i].AsSpan().Fill(0); + } + + if (startY > 0) + { + // Read the row above the tile which will become the first upper_row. + // Include a pixel to the left if it exists; include a pixel to the right + // in all cases (wrapping to the leftmost pixel of the next row if it does + // not exist). + Span src = argb.Slice(((startY - 1) * width) + contextStartX, maxX + haveLeft + 1); + Span dst = currentRow.Slice(contextStartX); + src.CopyTo(dst); + } + + for (int relativeY = 0; relativeY < maxY; relativeY++) + { + int y = startY + relativeY; + Span tmp = upperRow; + upperRow = currentRow; + currentRow = tmp; + + // Read currentRow. Include a pixel to the left if it exists; include a + // pixel to the right in all cases except at the bottom right corner of + // the image (wrapping to the leftmost pixel of the next row if it does + // not exist in the currentRow). + int offset = (y * width) + contextStartX; + Span src = argb.Slice(offset, maxX + haveLeft + (y + 1 < height ? 1 : 0)); + Span dst = currentRow.Slice(contextStartX); + src.CopyTo(dst); + + if (nearLossless) + { + if (maxQuantization > 1 && y >= 1 && y + 1 < height) + { + MaxDiffsForRow(contextWidth, width, argb, offset, maxDiffs.Slice(contextStartX), usedSubtractGreen); + } + } + + GetResidual(width, height, upperRow, currentRow, maxDiffs, mode, startX, startX + maxX, y, maxQuantization, transparentColorMode, usedSubtractGreen, nearLossless, residuals); + for (int relativeX = 0; relativeX < maxX; ++relativeX) + { + UpdateHisto(histoArgb, residuals[relativeX]); + } + } + + float curDiff = PredictionCostSpatialHistogram(accumulated, histoArgb); + + // Favor keeping the areas locally similar. + if (mode == leftMode) + { + curDiff -= SpatialPredictorBias; + } + + if (mode == aboveMode) + { + curDiff -= SpatialPredictorBias; + } + + if (curDiff < bestDiff) + { + int[][] tmp = histoArgb; + histoArgb = bestHisto; + bestHisto = tmp; + bestDiff = curDiff; + bestMode = mode; + } + } + + for (int i = 0; i < 4; i++) + { + for (int j = 0; j < 256; j++) + { + accumulated[i][j] += bestHisto[i][j]; + } + } + + return bestMode; + } + + /// + /// Stores the difference between the pixel and its prediction in "output". + /// In case of a lossy encoding, updates the source image to avoid propagating + /// the deviation further to pixels which depend on the current pixel for their + /// predictions. + /// + private static void GetResidual( + int width, + int height, + Span upperRowSpan, + Span currentRowSpan, + Span maxDiffs, + int mode, + int xStart, + int xEnd, + int y, + int maxQuantization, + WebpTransparentColorMode transparentColorMode, + bool usedSubtractGreen, + bool nearLossless, + Span output) + { + if (transparentColorMode == WebpTransparentColorMode.Preserve) + { + PredictBatch(mode, xStart, y, xEnd - xStart, currentRowSpan, upperRowSpan, output); + } + else + { +#pragma warning disable SA1503 // Braces should not be omitted + fixed (uint* currentRow = currentRowSpan) + fixed (uint* upperRow = upperRowSpan) + { + for (int x = xStart; x < xEnd; x++) + { + uint predict = 0; + uint residual; + if (y == 0) + { + predict = x == 0 ? WebpConstants.ArgbBlack : currentRow[x - 1]; // Left. + } + else if (x == 0) + { + predict = upperRow[x]; // Top. + } + else + { + switch (mode) + { + case 0: + predict = WebpConstants.ArgbBlack; + break; + case 1: + predict = currentRow[x - 1]; + break; + case 2: + predict = LosslessUtils.Predictor2(currentRow[x - 1], upperRow + x); + break; + case 3: + predict = LosslessUtils.Predictor3(currentRow[x - 1], upperRow + x); + break; + case 4: + predict = LosslessUtils.Predictor4(currentRow[x - 1], upperRow + x); + break; + case 5: + predict = LosslessUtils.Predictor5(currentRow[x - 1], upperRow + x); + break; + case 6: + predict = LosslessUtils.Predictor6(currentRow[x - 1], upperRow + x); + break; + case 7: + predict = LosslessUtils.Predictor7(currentRow[x - 1], upperRow + x); + break; + case 8: + predict = LosslessUtils.Predictor8(currentRow[x - 1], upperRow + x); + break; + case 9: + predict = LosslessUtils.Predictor9(currentRow[x - 1], upperRow + x); + break; + case 10: + predict = LosslessUtils.Predictor10(currentRow[x - 1], upperRow + x); + break; + case 11: + predict = LosslessUtils.Predictor11(currentRow[x - 1], upperRow + x); + break; + case 12: + predict = LosslessUtils.Predictor12(currentRow[x - 1], upperRow + x); + break; + case 13: + predict = LosslessUtils.Predictor13(currentRow[x - 1], upperRow + x); + break; + } + } + + if (nearLossless) + { + if (maxQuantization == 1 || mode == 0 || y == 0 || y == height - 1 || x == 0 || x == width - 1) + { + residual = LosslessUtils.SubPixels(currentRow[x], predict); + } + else + { + residual = NearLossless(currentRow[x], predict, maxQuantization, maxDiffs[x], usedSubtractGreen); + + // Update the source image. + currentRow[x] = LosslessUtils.AddPixels(predict, residual); + + // x is never 0 here so we do not need to update upperRow like below. + } + } + else + { + residual = LosslessUtils.SubPixels(currentRow[x], predict); + } + + if ((currentRow[x] & MaskAlpha) == 0) + { + // If alpha is 0, cleanup RGB. We can choose the RGB values of the + // residual for best compression. The prediction of alpha itself can be + // non-zero and must be kept though. We choose RGB of the residual to be + // 0. + residual &= MaskAlpha; + + // Update the source image. + currentRow[x] = predict & ~MaskAlpha; + + // The prediction for the rightmost pixel in a row uses the leftmost + // pixel + // in that row as its top-right context pixel. Hence if we change the + // leftmost pixel of current_row, the corresponding change must be + // applied + // to upperRow as well where top-right context is being read from. + if (x == 0 && y != 0) + { + upperRow[width] = currentRow[0]; + } + } + + output[x - xStart] = residual; + } + } + } + } +#pragma warning restore SA1503 // Braces should not be omitted + + /// + /// Quantize every component of the difference between the actual pixel value and + /// its prediction to a multiple of a quantization (a power of 2, not larger than + /// maxQuantization which is a power of 2, smaller than maxDiff). Take care if + /// value and predict have undergone subtract green, which means that red and + /// blue are represented as offsets from green. + /// + private static uint NearLossless(uint value, uint predict, int maxQuantization, int maxDiff, bool usedSubtractGreen) + { + byte newGreen = 0; + byte greenDiff = 0; + byte a; + if (maxDiff <= 2) + { + return LosslessUtils.SubPixels(value, predict); + } + + int quantization = maxQuantization; + while (quantization >= maxDiff) + { + quantization >>= 1; + } + + if (value >> 24 is 0 or 0xff) + { + // Preserve transparency of fully transparent or fully opaque pixels. + a = NearLosslessDiff((byte)((value >> 24) & 0xff), (byte)((predict >> 24) & 0xff)); + } + else + { + a = NearLosslessComponent((byte)(value >> 24), (byte)(predict >> 24), 0xff, quantization); + } + + byte g = NearLosslessComponent((byte)((value >> 8) & 0xff), (byte)((predict >> 8) & 0xff), 0xff, quantization); + + if (usedSubtractGreen) + { + // The green offset will be added to red and blue components during decoding + // to obtain the actual red and blue values. + newGreen = (byte)(((predict >> 8) + g) & 0xff); + + // The amount by which green has been adjusted during quantization. It is + // subtracted from red and blue for compensation, to avoid accumulating two + // quantization errors in them. + greenDiff = NearLosslessDiff(newGreen, (byte)((value >> 8) & 0xff)); + } + + byte r = NearLosslessComponent(NearLosslessDiff((byte)((value >> 16) & 0xff), greenDiff), (byte)((predict >> 16) & 0xff), (byte)(0xff - newGreen), quantization); + byte b = NearLosslessComponent(NearLosslessDiff((byte)(value & 0xff), greenDiff), (byte)(predict & 0xff), (byte)(0xff - newGreen), quantization); + + return ((uint)a << 24) | ((uint)r << 16) | ((uint)g << 8) | b; + } + + /// + /// Quantize the difference between the actual component value and its prediction + /// to a multiple of quantization, working modulo 256, taking care not to cross + /// a boundary (inclusive upper limit). + /// + private static byte NearLosslessComponent(byte value, byte predict, byte boundary, int quantization) + { + int residual = (value - predict) & 0xff; + int boundaryResidual = (boundary - predict) & 0xff; + int lower = residual & ~(quantization - 1); + int upper = lower + quantization; + + // Resolve ties towards a value closer to the prediction (i.e. towards lower + // if value comes after prediction and towards upper otherwise). + int bias = ((boundary - value) & 0xff) < boundaryResidual ? 1 : 0; + + if (residual - lower < upper - residual + bias) + { + // lower is closer to residual than upper. + if (residual > boundaryResidual && lower <= boundaryResidual) + { + // Halve quantization step to avoid crossing boundary. This midpoint is + // on the same side of boundary as residual because midpoint >= residual + // (since lower is closer than upper) and residual is above the boundary. + return (byte)(lower + (quantization >> 1)); + } + + return (byte)lower; + } + else + { + // upper is closer to residual than lower. + if (residual <= boundaryResidual && upper > boundaryResidual) + { + // Halve quantization step to avoid crossing boundary. This midpoint is + // on the same side of boundary as residual because midpoint <= residual + // (since upper is closer than lower) and residual is below the boundary. + return (byte)(lower + (quantization >> 1)); + } + + return (byte)(upper & 0xff); + } + } + + /// + /// Converts pixels of the image to residuals with respect to predictions. + /// If max_quantization > 1, applies near lossless processing, quantizing + /// residuals to multiples of quantization levels up to max_quantization + /// (the actual quantization level depends on smoothness near the given pixel). + /// + private static void CopyImageWithPrediction( + int width, + int height, + int bits, + Span modes, + Span argbScratch, + Span argb, + int maxQuantization, + WebpTransparentColorMode transparentColorMode, + bool usedSubtractGreen, + bool nearLossless, + bool lowEffort) + { + int tilesPerRow = LosslessUtils.SubSampleSize(width, bits); + + // The width of upperRow and currentRow is one pixel larger than image width + // to allow the top right pixel to point to the leftmost pixel of the next row + // when at the right edge. + Span upperRow = argbScratch; + Span currentRow = upperRow.Slice(width + 1); + Span currentMaxDiffs = MemoryMarshal.Cast(currentRow.Slice(width + 1)); + + Span lowerMaxDiffs = currentMaxDiffs.Slice(width); + for (int y = 0; y < height; y++) + { + Span tmp32 = upperRow; + upperRow = currentRow; + currentRow = tmp32; + Span src = argb.Slice(y * width, width + (y + 1 < height ? 1 : 0)); + src.CopyTo(currentRow); + + if (lowEffort) + { + PredictBatch(PredLowEffort, 0, y, width, currentRow, upperRow, argb.Slice(y * width)); + } + else + { + if (nearLossless && maxQuantization > 1) + { + // Compute maxDiffs for the lower row now, because that needs the + // contents of bgra for the current row, which we will overwrite with + // residuals before proceeding with the next row. + Span tmp8 = currentMaxDiffs; + currentMaxDiffs = lowerMaxDiffs; + lowerMaxDiffs = tmp8; + if (y + 2 < height) + { + MaxDiffsForRow(width, width, argb, (y + 1) * width, lowerMaxDiffs, usedSubtractGreen); + } + } + + for (int x = 0; x < width;) + { + int mode = (int)((modes[((y >> bits) * tilesPerRow) + (x >> bits)] >> 8) & 0xff); + int xEnd = x + (1 << bits); + if (xEnd > width) + { + xEnd = width; + } + + GetResidual( + width, + height, + upperRow, + currentRow, + currentMaxDiffs, + mode, + x, + xEnd, + y, + maxQuantization, + transparentColorMode, + usedSubtractGreen, + nearLossless, + argb.Slice((y * width) + x)); + + x = xEnd; + } + } + } + } + + private static void PredictBatch( + int mode, + int xStart, + int y, + int numPixels, + Span currentSpan, + Span upperSpan, + Span outputSpan) + { +#pragma warning disable SA1503 // Braces should not be omitted + fixed (uint* current = currentSpan) + fixed (uint* upper = upperSpan) + fixed (uint* outputFixed = outputSpan) + { + uint* output = outputFixed; + if (xStart == 0) + { + if (y == 0) + { + // ARGB_BLACK. + LosslessUtils.PredictorSub0(current, 1, output); + } + else + { + // Top one. + LosslessUtils.PredictorSub2(current, upper, 1, output); + } + + ++xStart; + ++output; + --numPixels; + } + + if (y == 0) + { + // Left one. + LosslessUtils.PredictorSub1(current + xStart, numPixels, output); + } + else + { + switch (mode) + { + case 0: + LosslessUtils.PredictorSub0(current + xStart, numPixels, output); + break; + case 1: + LosslessUtils.PredictorSub1(current + xStart, numPixels, output); + break; + case 2: + LosslessUtils.PredictorSub2(current + xStart, upper + xStart, numPixels, output); + break; + case 3: + LosslessUtils.PredictorSub3(current + xStart, upper + xStart, numPixels, output); + break; + case 4: + LosslessUtils.PredictorSub4(current + xStart, upper + xStart, numPixels, output); + break; + case 5: + LosslessUtils.PredictorSub5(current + xStart, upper + xStart, numPixels, output); + break; + case 6: + LosslessUtils.PredictorSub6(current + xStart, upper + xStart, numPixels, output); + break; + case 7: + LosslessUtils.PredictorSub7(current + xStart, upper + xStart, numPixels, output); + break; + case 8: + LosslessUtils.PredictorSub8(current + xStart, upper + xStart, numPixels, output); + break; + case 9: + LosslessUtils.PredictorSub9(current + xStart, upper + xStart, numPixels, output); + break; + case 10: + LosslessUtils.PredictorSub10(current + xStart, upper + xStart, numPixels, output); + break; + case 11: + LosslessUtils.PredictorSub11(current + xStart, upper + xStart, numPixels, output); + break; + case 12: + LosslessUtils.PredictorSub12(current + xStart, upper + xStart, numPixels, output); + break; + case 13: + LosslessUtils.PredictorSub13(current + xStart, upper + xStart, numPixels, output); + break; + } + } + } + } +#pragma warning restore SA1503 // Braces should not be omitted + + private static void MaxDiffsForRow(int width, int stride, Span argb, int offset, Span maxDiffs, bool usedSubtractGreen) + { + if (width <= 2) + { + return; + } + + uint current = argb[offset]; + uint right = argb[offset + 1]; + if (usedSubtractGreen) + { + current = AddGreenToBlueAndRed(current); + right = AddGreenToBlueAndRed(right); + } + + for (int x = 1; x < width - 1; x++) + { + uint up = argb[offset - stride + x]; + uint down = argb[offset + stride + x]; + uint left = current; + current = right; + right = argb[offset + x + 1]; + if (usedSubtractGreen) + { + up = AddGreenToBlueAndRed(up); + down = AddGreenToBlueAndRed(down); + right = AddGreenToBlueAndRed(right); + } + + maxDiffs[x] = (byte)MaxDiffAroundPixel(current, up, down, left, right); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int MaxDiffBetweenPixels(uint p1, uint p2) + { + int diffA = Math.Abs((int)(p1 >> 24) - (int)(p2 >> 24)); + int diffR = Math.Abs((int)((p1 >> 16) & 0xff) - (int)((p2 >> 16) & 0xff)); + int diffG = Math.Abs((int)((p1 >> 8) & 0xff) - (int)((p2 >> 8) & 0xff)); + int diffB = Math.Abs((int)(p1 & 0xff) - (int)(p2 & 0xff)); + return GetMax(GetMax(diffA, diffR), GetMax(diffG, diffB)); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int MaxDiffAroundPixel(uint current, uint up, uint down, uint left, uint right) + { + int diffUp = MaxDiffBetweenPixels(current, up); + int diffDown = MaxDiffBetweenPixels(current, down); + int diffLeft = MaxDiffBetweenPixels(current, left); + int diffRight = MaxDiffBetweenPixels(current, right); + return GetMax(GetMax(diffUp, diffDown), GetMax(diffLeft, diffRight)); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void UpdateHisto(int[][] histoArgb, uint argb) + { + ++histoArgb[0][argb >> 24]; + ++histoArgb[1][(argb >> 16) & 0xff]; + ++histoArgb[2][(argb >> 8) & 0xff]; + ++histoArgb[3][argb & 0xff]; + } + + private static uint AddGreenToBlueAndRed(uint argb) + { + uint green = (argb >> 8) & 0xff; + uint redBlue = argb & 0x00ff00ffu; + redBlue += (green << 16) | green; + redBlue &= 0x00ff00ffu; + return (argb & 0xff00ff00u) | redBlue; + } + + private static void CopyTileWithColorTransform(int xSize, int ySize, int tileX, int tileY, int maxTileSize, Vp8LMultipliers colorTransform, Span argb) + { + int xScan = GetMin(maxTileSize, xSize - tileX); + int yScan = GetMin(maxTileSize, ySize - tileY); + argb = argb.Slice((tileY * xSize) + tileX); + while (yScan-- > 0) + { + LosslessUtils.TransformColor(colorTransform, argb, xScan); + + if (argb.Length > xSize) + { + argb = argb.Slice(xSize); + } + } + } + + private static Vp8LMultipliers GetBestColorTransformForTile(int tileX, int tileY, int bits, Vp8LMultipliers prevX, Vp8LMultipliers prevY, int quality, int xSize, int ySize, int[] accumulatedRedHisto, int[] accumulatedBlueHisto, Span argb) + { + int maxTileSize = 1 << bits; + int tileYOffset = tileY * maxTileSize; + int tileXOffset = tileX * maxTileSize; + int allXMax = GetMin(tileXOffset + maxTileSize, xSize); + int allYMax = GetMin(tileYOffset + maxTileSize, ySize); + int tileWidth = allXMax - tileXOffset; + int tileHeight = allYMax - tileYOffset; + Span tileArgb = argb.Slice((tileYOffset * xSize) + tileXOffset); + + var bestTx = default(Vp8LMultipliers); + + GetBestGreenToRed(tileArgb, xSize, tileWidth, tileHeight, prevX, prevY, quality, accumulatedRedHisto, ref bestTx); + + GetBestGreenRedToBlue(tileArgb, xSize, tileWidth, tileHeight, prevX, prevY, quality, accumulatedBlueHisto, ref bestTx); + + return bestTx; + } + + private static void GetBestGreenToRed(Span argb, int stride, int tileWidth, int tileHeight, Vp8LMultipliers prevX, Vp8LMultipliers prevY, int quality, int[] accumulatedRedHisto, ref Vp8LMultipliers bestTx) + { + int maxIters = 4 + ((7 * quality) >> 8); // in range [4..6] + int greenToRedBest = 0; + double bestDiff = GetPredictionCostCrossColorRed(argb, stride, tileWidth, tileHeight, prevX, prevY, greenToRedBest, accumulatedRedHisto); + for (int iter = 0; iter < maxIters; iter++) + { + // ColorTransformDelta is a 3.5 bit fixed point, so 32 is equal to + // one in color computation. Having initial delta here as 1 is sufficient + // to explore the range of (-2, 2). + int delta = 32 >> iter; + + // Try a negative and a positive delta from the best known value. + for (int offset = -delta; offset <= delta; offset += 2 * delta) + { + int greenToRedCur = offset + greenToRedBest; + double curDiff = GetPredictionCostCrossColorRed(argb, stride, tileWidth, tileHeight, prevX, prevY, greenToRedCur, accumulatedRedHisto); + if (curDiff < bestDiff) + { + bestDiff = curDiff; + greenToRedBest = greenToRedCur; + } + } + } + + bestTx.GreenToRed = (byte)(greenToRedBest & 0xff); + } + + private static void GetBestGreenRedToBlue(Span argb, int stride, int tileWidth, int tileHeight, Vp8LMultipliers prevX, Vp8LMultipliers prevY, int quality, int[] accumulatedBlueHisto, ref Vp8LMultipliers bestTx) + { + int iters = (quality < 25) ? 1 : (quality > 50) ? GreenRedToBlueMaxIters : 4; + int greenToBlueBest = 0; + int redToBlueBest = 0; + sbyte[][] offset = { new sbyte[] { 0, -1 }, new sbyte[] { 0, 1 }, new sbyte[] { -1, 0 }, new sbyte[] { 1, 0 }, new sbyte[] { -1, -1 }, new sbyte[] { -1, 1 }, new sbyte[] { 1, -1 }, new sbyte[] { 1, 1 } }; + sbyte[] deltaLut = { 16, 16, 8, 4, 2, 2, 2 }; + + // Initial value at origin: + double bestDiff = GetPredictionCostCrossColorBlue(argb, stride, tileWidth, tileHeight, prevX, prevY, greenToBlueBest, redToBlueBest, accumulatedBlueHisto); + for (int iter = 0; iter < iters; iter++) + { + int delta = deltaLut[iter]; + for (int axis = 0; axis < GreenRedToBlueNumAxis; axis++) + { + int greenToBlueCur = (offset[axis][0] * delta) + greenToBlueBest; + int redToBlueCur = (offset[axis][1] * delta) + redToBlueBest; + double curDiff = GetPredictionCostCrossColorBlue(argb, stride, tileWidth, tileHeight, prevX, prevY, greenToBlueCur, redToBlueCur, accumulatedBlueHisto); + if (curDiff < bestDiff) + { + bestDiff = curDiff; + greenToBlueBest = greenToBlueCur; + redToBlueBest = redToBlueCur; + } + + if (quality < 25 && iter == 4) + { + // Only axis aligned diffs for lower quality. + break; // next iter. + } + } + + if (delta == 2 && greenToBlueBest == 0 && redToBlueBest == 0) + { + // Further iterations would not help. + break; // out of iter-loop. + } + } + + bestTx.GreenToBlue = (byte)(greenToBlueBest & 0xff); + bestTx.RedToBlue = (byte)(redToBlueBest & 0xff); + } + + private static double GetPredictionCostCrossColorRed(Span argb, int stride, int tileWidth, int tileHeight, Vp8LMultipliers prevX, Vp8LMultipliers prevY, int greenToRed, int[] accumulatedRedHisto) + { + int[] histo = new int[256]; + + CollectColorRedTransforms(argb, stride, tileWidth, tileHeight, greenToRed, histo); + double curDiff = PredictionCostCrossColor(accumulatedRedHisto, histo); + + if ((byte)greenToRed == prevX.GreenToRed) + { + // Favor keeping the areas locally similar. + curDiff -= 3; + } + + if ((byte)greenToRed == prevY.GreenToRed) + { + // Favor keeping the areas locally similar. + curDiff -= 3; + } + + if (greenToRed == 0) + { + curDiff -= 3; + } + + return curDiff; + } + + private static double GetPredictionCostCrossColorBlue(Span argb, int stride, int tileWidth, int tileHeight, Vp8LMultipliers prevX, Vp8LMultipliers prevY, int greenToBlue, int redToBlue, int[] accumulatedBlueHisto) + { + int[] histo = new int[256]; + + CollectColorBlueTransforms(argb, stride, tileWidth, tileHeight, greenToBlue, redToBlue, histo); + double curDiff = PredictionCostCrossColor(accumulatedBlueHisto, histo); + if ((byte)greenToBlue == prevX.GreenToBlue) + { + // Favor keeping the areas locally similar. + curDiff -= 3; + } + + if ((byte)greenToBlue == prevY.GreenToBlue) + { + // Favor keeping the areas locally similar. + curDiff -= 3; + } + + if ((byte)redToBlue == prevX.RedToBlue) + { + // Favor keeping the areas locally similar. + curDiff -= 3; + } + + if ((byte)redToBlue == prevY.RedToBlue) + { + // Favor keeping the areas locally similar. + curDiff -= 3; + } + + if (greenToBlue == 0) + { + curDiff -= 3; + } + + if (redToBlue == 0) + { + curDiff -= 3; + } + + return curDiff; + } + + private static void CollectColorRedTransforms(Span bgra, int stride, int tileWidth, int tileHeight, int greenToRed, int[] histo) + { +#if SUPPORTS_RUNTIME_INTRINSICS + if (Sse41.IsSupported) + { + var multsg = Vector128.Create(LosslessUtils.Cst5b(greenToRed)); + var maskgreen = Vector128.Create(0x00ff00); + var mask = Vector128.Create((short)0xff); + + const int span = 8; + Span values = stackalloc ushort[span]; + for (int y = 0; y < tileHeight; y++) + { + Span srcSpan = bgra.Slice(y * stride); +#pragma warning disable SA1503 // Braces should not be omitted + fixed (uint* src = srcSpan) + fixed (ushort* dst = values) + { + for (int x = 0; x + span <= tileWidth; x += span) + { + uint* input0Idx = src + x; + uint* input1Idx = src + x + (span / 2); + Vector128 input0 = Sse2.LoadVector128((ushort*)input0Idx).AsByte(); + Vector128 input1 = Sse2.LoadVector128((ushort*)input1Idx).AsByte(); + Vector128 g0 = Sse2.And(input0, maskgreen.AsByte()); // 0 0 | g 0 + Vector128 g1 = Sse2.And(input1, maskgreen.AsByte()); + Vector128 g = Sse41.PackUnsignedSaturate(g0.AsInt32(), g1.AsInt32()); // g 0 + Vector128 a0 = Sse2.ShiftRightLogical(input0.AsInt32(), 16); // 0 0 | x r + Vector128 a1 = Sse2.ShiftRightLogical(input1.AsInt32(), 16); + Vector128 a = Sse41.PackUnsignedSaturate(a0, a1); // x r + Vector128 b = Sse2.MultiplyHigh(g.AsInt16(), multsg); // x dr + Vector128 c = Sse2.Subtract(a.AsByte(), b.AsByte()); // x r' + Vector128 d = Sse2.And(c, mask.AsByte()); // 0 r' + Sse2.Store(dst, d.AsUInt16()); + for (int i = 0; i < span; i++) + { + ++histo[values[i]]; + } + } + } + } +#pragma warning restore SA1503 // Braces should not be omitted + + int leftOver = tileWidth & (span - 1); + if (leftOver > 0) + { + CollectColorRedTransformsNoneVectorized(bgra.Slice(tileWidth - leftOver), stride, leftOver, tileHeight, greenToRed, histo); + } + } + else +#endif + { + CollectColorRedTransformsNoneVectorized(bgra, stride, tileWidth, tileHeight, greenToRed, histo); + } + } + + private static void CollectColorRedTransformsNoneVectorized(Span bgra, int stride, int tileWidth, int tileHeight, int greenToRed, int[] histo) + { + int pos = 0; + while (tileHeight-- > 0) + { + for (int x = 0; x < tileWidth; x++) + { + int idx = LosslessUtils.TransformColorRed((sbyte)greenToRed, bgra[pos + x]); + ++histo[idx]; + } + + pos += stride; + } + } + + private static void CollectColorBlueTransforms(Span bgra, int stride, int tileWidth, int tileHeight, int greenToBlue, int redToBlue, int[] histo) + { +#if SUPPORTS_RUNTIME_INTRINSICS + if (Sse41.IsSupported) + { + const int span = 8; + Span values = stackalloc ushort[span]; + var multsr = Vector128.Create(LosslessUtils.Cst5b(redToBlue)); + var multsg = Vector128.Create(LosslessUtils.Cst5b(greenToBlue)); + var maskgreen = Vector128.Create(0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255); + var maskgreenblue = Vector128.Create(255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0); + var maskblue = Vector128.Create(255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0); + var shufflerLow = Vector128.Create(255, 2, 255, 6, 255, 10, 255, 14, 255, 255, 255, 255, 255, 255, 255, 255); + var shufflerHigh = Vector128.Create(255, 255, 255, 255, 255, 255, 255, 255, 255, 2, 255, 6, 255, 10, 255, 14); + + for (int y = 0; y < tileHeight; y++) + { + Span srcSpan = bgra.Slice(y * stride); +#pragma warning disable SA1503 // Braces should not be omitted + fixed (uint* src = srcSpan) + fixed (ushort* dst = values) + { + for (int x = 0; x + span <= tileWidth; x += span) + { + uint* input0Idx = src + x; + uint* input1Idx = src + x + (span / 2); + Vector128 input0 = Sse2.LoadVector128((ushort*)input0Idx).AsByte(); + Vector128 input1 = Sse2.LoadVector128((ushort*)input1Idx).AsByte(); + Vector128 r0 = Ssse3.Shuffle(input0, shufflerLow); + Vector128 r1 = Ssse3.Shuffle(input1, shufflerHigh); + Vector128 r = Sse2.Or(r0, r1); + Vector128 gb0 = Sse2.And(input0, maskgreenblue); + Vector128 gb1 = Sse2.And(input1, maskgreenblue); + Vector128 gb = Sse41.PackUnsignedSaturate(gb0.AsInt32(), gb1.AsInt32()); + Vector128 g = Sse2.And(gb.AsByte(), maskgreen); + Vector128 a = Sse2.MultiplyHigh(r.AsInt16(), multsr); + Vector128 b = Sse2.MultiplyHigh(g.AsInt16(), multsg); + Vector128 c = Sse2.Subtract(gb.AsByte(), b.AsByte()); + Vector128 d = Sse2.Subtract(c, a.AsByte()); + Vector128 e = Sse2.And(d, maskblue); + Sse2.Store(dst, e.AsUInt16()); + for (int i = 0; i < span; i++) + { + ++histo[values[i]]; + } + } + } + } +#pragma warning restore SA1503 // Braces should not be omitted + + int leftOver = tileWidth & (span - 1); + if (leftOver > 0) + { + CollectColorBlueTransformsNoneVectorized(bgra.Slice(tileWidth - leftOver), stride, leftOver, tileHeight, greenToBlue, redToBlue, histo); + } + } + else +#endif + { + CollectColorBlueTransformsNoneVectorized(bgra, stride, tileWidth, tileHeight, greenToBlue, redToBlue, histo); + } + } + + private static void CollectColorBlueTransformsNoneVectorized(Span bgra, int stride, int tileWidth, int tileHeight, int greenToBlue, int redToBlue, int[] histo) + { + int pos = 0; + while (tileHeight-- > 0) + { + for (int x = 0; x < tileWidth; x++) + { + int idx = LosslessUtils.TransformColorBlue((sbyte)greenToBlue, (sbyte)redToBlue, bgra[pos + x]); + ++histo[idx]; + } + + pos += stride; + } + } + + private static float PredictionCostSpatialHistogram(int[][] accumulated, int[][] tile) + { + double retVal = 0.0d; + for (int i = 0; i < 4; i++) + { + double kExpValue = 0.94; + retVal += PredictionCostSpatial(tile[i], 1, kExpValue); + retVal += LosslessUtils.CombinedShannonEntropy(tile[i], accumulated[i]); + } + + return (float)retVal; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static double PredictionCostCrossColor(int[] accumulated, int[] counts) + { + // Favor low entropy, locally and globally. + // Favor small absolute values for PredictionCostSpatial. + const double expValue = 2.4d; + return LosslessUtils.CombinedShannonEntropy(counts, accumulated) + PredictionCostSpatial(counts, 3, expValue); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static float PredictionCostSpatial(int[] counts, int weight0, double expVal) + { + int significantSymbols = 256 >> 4; + double expDecayFactor = 0.6; + double bits = weight0 * counts[0]; + for (int i = 1; i < significantSymbols; i++) + { + bits += expVal * (counts[i] + counts[256 - i]); + expVal *= expDecayFactor; + } + + return (float)(-0.1 * bits); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static byte NearLosslessDiff(byte a, byte b) => (byte)((a - b) & 0xff); + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint MultipliersToColorCode(Vp8LMultipliers m) => 0xff000000u | ((uint)m.RedToBlue << 16) | ((uint)m.GreenToBlue << 8) | m.GreenToRed; + + [MethodImpl(InliningOptions.ShortMethod)] + private static int GetMin(int a, int b) => a > b ? b : a; + + [MethodImpl(InliningOptions.ShortMethod)] + private static int GetMax(int a, int b) => (a < b) ? b : a; + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LBackwardRefs.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LBackwardRefs.cs new file mode 100644 index 000000000..502728b15 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LBackwardRefs.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + internal class Vp8LBackwardRefs + { + public Vp8LBackwardRefs() => this.Refs = new List(); + + /// + /// Gets or sets the common block-size. + /// + public int BlockSize { get; set; } + + /// + /// Gets the backward references. + /// + public List Refs { get; } + + public void Add(PixOrCopy pixOrCopy) => this.Refs.Add(pixOrCopy); + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LBitEntropy.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LBitEntropy.cs new file mode 100644 index 000000000..bfe4e384e --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LBitEntropy.cs @@ -0,0 +1,221 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + /// + /// Holds bit entropy results and entropy-related functions. + /// + internal class Vp8LBitEntropy + { + /// + /// Not a trivial literal symbol. + /// + private const uint NonTrivialSym = 0xffffffff; + + /// + /// Initializes a new instance of the class. + /// + public Vp8LBitEntropy() + { + this.Entropy = 0.0d; + this.Sum = 0; + this.NoneZeros = 0; + this.MaxVal = 0; + this.NoneZeroCode = NonTrivialSym; + } + + /// + /// Gets or sets the entropy. + /// + public double Entropy { get; set; } + + /// + /// Gets or sets the sum of the population. + /// + public uint Sum { get; set; } + + /// + /// Gets or sets the number of non-zero elements in the population. + /// + public int NoneZeros { get; set; } + + /// + /// Gets or sets the maximum value in the population. + /// + public uint MaxVal { get; set; } + + /// + /// Gets or sets the index of the last non-zero in the population. + /// + public uint NoneZeroCode { get; set; } + + public void Init() + { + this.Entropy = 0.0d; + this.Sum = 0; + this.NoneZeros = 0; + this.MaxVal = 0; + this.NoneZeroCode = NonTrivialSym; + } + + public double BitsEntropyRefine() + { + double mix; + if (this.NoneZeros < 5) + { + if (this.NoneZeros <= 1) + { + return 0; + } + + // Two symbols, they will be 0 and 1 in a Huffman code. + // Let's mix in a bit of entropy to favor good clustering when + // distributions of these are combined. + if (this.NoneZeros == 2) + { + return (0.99 * this.Sum) + (0.01 * this.Entropy); + } + + // No matter what the entropy says, we cannot be better than minLimit + // with Huffman coding. I am mixing a bit of entropy into the + // minLimit since it produces much better (~0.5 %) compression results + // perhaps because of better entropy clustering. + if (this.NoneZeros == 3) + { + mix = 0.95; + } + else + { + mix = 0.7; // nonzeros == 4. + } + } + else + { + mix = 0.627; + } + + double minLimit = (2 * this.Sum) - this.MaxVal; + minLimit = (mix * minLimit) + ((1.0 - mix) * this.Entropy); + return this.Entropy < minLimit ? minLimit : this.Entropy; + } + + public void BitsEntropyUnrefined(Span array, int n) + { + this.Init(); + + for (int i = 0; i < n; i++) + { + if (array[i] != 0) + { + this.Sum += array[i]; + this.NoneZeroCode = (uint)i; + this.NoneZeros++; + this.Entropy -= LosslessUtils.FastSLog2(array[i]); + if (this.MaxVal < array[i]) + { + this.MaxVal = array[i]; + } + } + } + + this.Entropy += LosslessUtils.FastSLog2(this.Sum); + } + + /// + /// Get the entropy for the distribution 'X'. + /// + public void BitsEntropyUnrefined(uint[] x, int length, Vp8LStreaks stats) + { + int i; + int iPrev = 0; + uint xPrev = x[0]; + + this.Init(); + + for (i = 1; i < length; i++) + { + uint xi = x[i]; + if (xi != xPrev) + { + this.GetEntropyUnrefined(xi, i, ref xPrev, ref iPrev, stats); + } + } + + this.GetEntropyUnrefined(0, i, ref xPrev, ref iPrev, stats); + + this.Entropy += LosslessUtils.FastSLog2(this.Sum); + } + + public void GetCombinedEntropyUnrefined(uint[] x, uint[] y, int length, Vp8LStreaks stats) + { + int i; + int iPrev = 0; + uint xyPrev = x[0] + y[0]; + + this.Init(); + + for (i = 1; i < length; i++) + { + uint xy = x[i] + y[i]; + if (xy != xyPrev) + { + this.GetEntropyUnrefined(xy, i, ref xyPrev, ref iPrev, stats); + } + } + + this.GetEntropyUnrefined(0, i, ref xyPrev, ref iPrev, stats); + + this.Entropy += LosslessUtils.FastSLog2(this.Sum); + } + + public void GetEntropyUnrefined(uint[] x, int length, Vp8LStreaks stats) + { + int i; + int iPrev = 0; + uint xPrev = x[0]; + + this.Init(); + + for (i = 1; i < length; i++) + { + uint xi = x[i]; + if (xi != xPrev) + { + this.GetEntropyUnrefined(xi, i, ref xPrev, ref iPrev, stats); + } + } + + this.GetEntropyUnrefined(0, i, ref xPrev, ref iPrev, stats); + + this.Entropy += LosslessUtils.FastSLog2(this.Sum); + } + + private void GetEntropyUnrefined(uint val, int i, ref uint valPrev, ref int iPrev, Vp8LStreaks stats) + { + int streak = i - iPrev; + + // Gather info for the bit entropy. + if (valPrev != 0) + { + this.Sum += (uint)(valPrev * streak); + this.NoneZeros += streak; + this.NoneZeroCode = (uint)iPrev; + this.Entropy -= LosslessUtils.FastSLog2(valPrev) * streak; + if (this.MaxVal < valPrev) + { + this.MaxVal = valPrev; + } + } + + // Gather info for the Huffman cost. + stats.Counts[valPrev != 0 ? 1 : 0] += streak > 3 ? 1 : 0; + stats.Streaks[valPrev != 0 ? 1 : 0][streak > 3 ? 1 : 0] += streak; + + valPrev = val; + iPrev = i; + } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LDecoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LDecoder.cs new file mode 100644 index 000000000..a95ec0a49 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LDecoder.cs @@ -0,0 +1,70 @@ +// Copyright (c) Six Labors. +// 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.Lossless +{ + /// + /// 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/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs new file mode 100644 index 000000000..693585637 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -0,0 +1,1786 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Webp.BitWriter; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + /// + /// Encoder for lossless webp images. + /// + internal class Vp8LEncoder : IDisposable + { + /// + /// The to use for buffer allocations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The global configuration. + /// + private readonly Configuration configuration; + + /// + /// Maximum number of reference blocks the image will be segmented into. + /// + private const int MaxRefsBlockPerImage = 16; + + /// + /// Minimum block size for backward references. + /// + private const int MinBlockSize = 256; + + /// + /// A bit writer for writing lossless webp streams. + /// + private Vp8LBitWriter bitWriter; + + /// + /// The quality, that will be used to encode the image. + /// + private readonly int quality; + + /// + /// Quality/speed trade-off (0=fast, 6=slower-better). + /// + private readonly WebpEncodingMethod method; + + /// + /// Flag indicating whether to preserve the exact RGB values under transparent area. Otherwise, discard this invisible + /// RGB information for better compression. + /// + private readonly WebpTransparentColorMode transparentColorMode; + + /// + /// Indicating whether near lossless mode should be used. + /// + private readonly bool nearLossless; + + /// + /// The near lossless quality. The range is 0 (maximum preprocessing) to 100 (no preprocessing, the default). + /// + private readonly int nearLosslessQuality; + + private const int ApplyPaletteGreedyMax = 4; + + private const int PaletteInvSizeBits = 11; + + private const int PaletteInvSize = 1 << PaletteInvSizeBits; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The global configuration. + /// The width of the input image. + /// The height of the input image. + /// The encoding quality. + /// Quality/speed trade-off (0=fast, 6=slower-better). + /// Flag indicating whether to preserve the exact RGB values under transparent area. + /// Otherwise, discard this invisible RGB information for better compression. + /// Indicating whether near lossless mode should be used. + /// The near lossless quality. The range is 0 (maximum preprocessing) to 100 (no preprocessing, the default). + public Vp8LEncoder( + MemoryAllocator memoryAllocator, + Configuration configuration, + int width, + int height, + int quality, + WebpEncodingMethod method, + WebpTransparentColorMode transparentColorMode, + bool nearLossless, + int nearLosslessQuality) + { + int pixelCount = width * height; + int initialSize = pixelCount * 2; + + this.memoryAllocator = memoryAllocator; + this.configuration = configuration; + this.quality = Numerics.Clamp(quality, 0, 100); + this.method = method; + this.transparentColorMode = transparentColorMode; + this.nearLossless = nearLossless; + this.nearLosslessQuality = Numerics.Clamp(nearLosslessQuality, 0, 100); + this.bitWriter = new Vp8LBitWriter(initialSize); + this.Bgra = memoryAllocator.Allocate(pixelCount); + this.EncodedData = memoryAllocator.Allocate(pixelCount); + this.Palette = memoryAllocator.Allocate(WebpConstants.MaxPaletteSize); + this.Refs = new Vp8LBackwardRefs[3]; + this.HashChain = new Vp8LHashChain(pixelCount); + + // We round the block size up, so we're guaranteed to have at most MaxRefsBlockPerImage blocks used: + int refsBlockSize = ((pixelCount - 1) / MaxRefsBlockPerImage) + 1; + for (int i = 0; i < this.Refs.Length; i++) + { + this.Refs[i] = new Vp8LBackwardRefs + { + BlockSize = refsBlockSize < MinBlockSize ? MinBlockSize : refsBlockSize + }; + } + } + + /// + /// Gets the memory for the image data as packed bgra values. + /// + public IMemoryOwner Bgra { get; } + + /// + /// Gets the memory for the encoded output image data. + /// + public IMemoryOwner EncodedData { get; } + + /// + /// Gets or sets the scratch memory for bgra rows used for predictions. + /// + public IMemoryOwner BgraScratch { get; set; } + + /// + /// Gets or sets the packed image width. + /// + public int CurrentWidth { get; set; } + + /// + /// Gets or sets the huffman image bits. + /// + public int HistoBits { get; set; } + + /// + /// Gets or sets the bits used for the transformation. + /// + public int TransformBits { get; set; } + + /// + /// Gets or sets the transform data. + /// + public IMemoryOwner TransformData { get; set; } + + /// + /// Gets or sets the cache bits. If equal to 0, don't use color cache. + /// + public int CacheBits { get; set; } + + /// + /// Gets or sets a value indicating whether to use the cross color transform. + /// + public bool UseCrossColorTransform { get; set; } + + /// + /// Gets or sets a value indicating whether to use the substract green transform. + /// + public bool UseSubtractGreenTransform { get; set; } + + /// + /// Gets or sets a value indicating whether to use the predictor transform. + /// + public bool UsePredictorTransform { get; set; } + + /// + /// Gets or sets a value indicating whether to use color indexing transform. + /// + public bool UsePalette { get; set; } + + /// + /// Gets or sets the palette size. + /// + public int PaletteSize { get; set; } + + /// + /// Gets the palette. + /// + public IMemoryOwner Palette { get; } + + /// + /// Gets the backward references. + /// + public Vp8LBackwardRefs[] Refs { get; } + + /// + /// Gets the hash chain. + /// + public Vp8LHashChain HashChain { get; } + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + public void Encode(Image image, Stream stream) + where TPixel : unmanaged, IPixel + { + image.Metadata.SyncProfiles(); + int width = image.Width; + int height = image.Height; + + // Convert image pixels to bgra array. + bool hasAlpha = this.ConvertPixelsToBgra(image, width, height); + + // Write the image size. + this.WriteImageSize(width, height); + + // Write the non-trivial Alpha flag and lossless version. + this.WriteAlphaAndVersion(hasAlpha); + + // Encode the main image stream. + this.EncodeStream(image); + + // Write bytes from the bitwriter buffer to the stream. + this.bitWriter.WriteEncodedImageToStream(stream, image.Metadata.ExifProfile, (uint)width, (uint)height); + } + + /// + /// Writes the image size to the bitwriter buffer. + /// + /// The input image width. + /// The input image height. + private void WriteImageSize(int inputImgWidth, int inputImgHeight) + { + Guard.MustBeLessThan(inputImgWidth, WebpConstants.MaxDimension, nameof(inputImgWidth)); + Guard.MustBeLessThan(inputImgHeight, WebpConstants.MaxDimension, nameof(inputImgHeight)); + + uint width = (uint)inputImgWidth - 1; + uint height = (uint)inputImgHeight - 1; + + this.bitWriter.PutBits(width, WebpConstants.Vp8LImageSizeBits); + this.bitWriter.PutBits(height, WebpConstants.Vp8LImageSizeBits); + } + + /// + /// Writes a flag indicating if alpha channel is used and the VP8L version to the bitwriter buffer. + /// + /// Indicates if a alpha channel is present. + private void WriteAlphaAndVersion(bool hasAlpha) + { + this.bitWriter.PutBits(hasAlpha ? 1U : 0, 1); + this.bitWriter.PutBits(WebpConstants.Vp8LVersion, WebpConstants.Vp8LVersionBits); + } + + /// + /// Encodes the image stream using lossless webp format. + /// + /// The pixel type. + /// The image to encode. + private void EncodeStream(Image image) + where TPixel : unmanaged, IPixel + { + int width = image.Width; + int height = image.Height; + + Span bgra = this.Bgra.GetSpan(); + Span encodedData = this.EncodedData.GetSpan(); + bool lowEffort = this.method == 0; + + // Analyze image (entropy, numPalettes etc). + CrunchConfig[] crunchConfigs = this.EncoderAnalyze(bgra, width, height, out bool redAndBlueAlwaysZero); + + int bestSize = 0; + Vp8LBitWriter bitWriterInit = this.bitWriter; + Vp8LBitWriter bitWriterBest = this.bitWriter.Clone(); + bool isFirstConfig = true; + foreach (CrunchConfig crunchConfig in crunchConfigs) + { + bgra.CopyTo(encodedData); + bool useCache = true; + this.UsePalette = crunchConfig.EntropyIdx is EntropyIx.Palette or EntropyIx.PaletteAndSpatial; + this.UseSubtractGreenTransform = crunchConfig.EntropyIdx is EntropyIx.SubGreen or EntropyIx.SpatialSubGreen; + this.UsePredictorTransform = crunchConfig.EntropyIdx is EntropyIx.Spatial or EntropyIx.SpatialSubGreen; + if (lowEffort) + { + this.UseCrossColorTransform = false; + } + else + { + this.UseCrossColorTransform = !redAndBlueAlwaysZero && this.UsePredictorTransform; + } + + this.AllocateTransformBuffer(width, height); + + // Reset any parameter in the encoder that is set in the previous iteration. + this.CacheBits = 0; + this.ClearRefs(); + + if (this.nearLossless) + { + // Apply near-lossless preprocessing. + bool useNearLossless = this.nearLosslessQuality < 100 && !this.UsePalette && !this.UsePredictorTransform; + if (useNearLossless) + { + this.AllocateTransformBuffer(width, height); + NearLosslessEnc.ApplyNearLossless(width, height, this.nearLosslessQuality, bgra, bgra, width); + } + } + + // Encode palette. + if (this.UsePalette) + { + this.EncodePalette(lowEffort); + this.MapImageFromPalette(width, height); + + // If using a color cache, do not have it bigger than the number of colors. + if (useCache && this.PaletteSize < 1 << WebpConstants.MaxColorCacheBits) + { + this.CacheBits = Numerics.Log2((uint)this.PaletteSize) + 1; + } + } + + // Apply transforms and write transform data. + if (this.UseSubtractGreenTransform) + { + this.ApplySubtractGreen(); + } + + if (this.UsePredictorTransform) + { + this.ApplyPredictFilter(this.CurrentWidth, height, lowEffort); + } + + if (this.UseCrossColorTransform) + { + this.ApplyCrossColorFilter(this.CurrentWidth, height, lowEffort); + } + + this.bitWriter.PutBits(0, 1); // No more transforms. + + // Encode and write the transformed image. + this.EncodeImage( + this.CurrentWidth, + height, + useCache, + crunchConfig, + this.CacheBits, + lowEffort); + + // If we are better than what we already have. + if (isFirstConfig || this.bitWriter.NumBytes() < bestSize) + { + bestSize = this.bitWriter.NumBytes(); + this.BitWriterSwap(ref this.bitWriter, ref bitWriterBest); + } + + // Reset the bit writer for the following iteration if any. + if (crunchConfigs.Length > 1) + { + this.bitWriter.Reset(bitWriterInit); + } + + isFirstConfig = false; + } + + this.BitWriterSwap(ref bitWriterBest, ref this.bitWriter); + } + + /// + /// Converts the pixels of the image to bgra. + /// + /// The type of the pixels. + /// The image to convert. + /// The width of the image. + /// The height of the image. + /// true, if the image is non opaque. + private bool ConvertPixelsToBgra(Image image, int width, int height) + where TPixel : unmanaged, IPixel + { + bool nonOpaque = false; + Span bgra = this.Bgra.GetSpan(); + Span bgraBytes = MemoryMarshal.Cast(bgra); + int widthBytes = width * 4; + for (int y = 0; y < height; y++) + { + Span rowSpan = image.GetPixelRowSpan(y); + Span rowBytes = bgraBytes.Slice(y * widthBytes, widthBytes); + PixelOperations.Instance.ToBgra32Bytes(this.configuration, rowSpan, rowBytes, width); + if (!nonOpaque) + { + Span rowBgra = MemoryMarshal.Cast(rowBytes); + nonOpaque = WebpCommonUtils.CheckNonOpaque(rowBgra); + } + } + + return nonOpaque; + } + + /// + /// Analyzes the image and decides which transforms should be used. + /// + /// The image as packed bgra values. + /// The image width. + /// The image height. + /// Indicates if red and blue are always zero. + private CrunchConfig[] EncoderAnalyze(ReadOnlySpan bgra, int width, int height, out bool redAndBlueAlwaysZero) + { + // Check if we only deal with a small number of colors and should use a palette. + bool usePalette = this.AnalyzeAndCreatePalette(bgra, width, height); + + // Empirical bit sizes. + this.HistoBits = GetHistoBits(this.method, usePalette, width, height); + this.TransformBits = GetTransformBits(this.method, this.HistoBits); + + // Try out multiple LZ77 on images with few colors. + int nlz77s = this.PaletteSize is > 0 and <= 16 ? 2 : 1; + EntropyIx entropyIdx = this.AnalyzeEntropy(bgra, width, height, usePalette, this.PaletteSize, this.TransformBits, out redAndBlueAlwaysZero); + + bool doNotCache = false; + var crunchConfigs = new List(); + + if (this.method == WebpEncodingMethod.BestQuality && this.quality == 100) + { + doNotCache = true; + + // Go brute force on all transforms. + foreach (EntropyIx entropyIx in Enum.GetValues(typeof(EntropyIx)).Cast()) + { + // We can only apply kPalette or kPaletteAndSpatial if we can indeed use a palette. + if ((entropyIx != EntropyIx.Palette && entropyIx != EntropyIx.PaletteAndSpatial) || usePalette) + { + crunchConfigs.Add(new CrunchConfig { EntropyIdx = entropyIx }); + } + } + } + else + { + // Only choose the guessed best transform. + crunchConfigs.Add(new CrunchConfig { EntropyIdx = entropyIdx }); + if (this.quality >= 75 && this.method == WebpEncodingMethod.Level5) + { + // Test with and without color cache. + doNotCache = true; + + // If we have a palette, also check in combination with spatial. + if (entropyIdx == EntropyIx.Palette) + { + crunchConfigs.Add(new CrunchConfig { EntropyIdx = EntropyIx.PaletteAndSpatial }); + } + } + } + + // Fill in the different LZ77s. + foreach (CrunchConfig crunchConfig in crunchConfigs) + { + for (int j = 0; j < nlz77s; j++) + { + crunchConfig.SubConfigs.Add(new CrunchSubConfig + { + Lz77 = j == 0 ? (int)Vp8LLz77Type.Lz77Standard | (int)Vp8LLz77Type.Lz77Rle : (int)Vp8LLz77Type.Lz77Box, + DoNotCache = doNotCache + }); + } + } + + return crunchConfigs.ToArray(); + } + + private void EncodeImage(int width, int height, bool useCache, CrunchConfig config, int cacheBits, bool lowEffort) + { + // bgra data with transformations applied. + Span bgra = this.EncodedData.GetSpan(); + int histogramImageXySize = LosslessUtils.SubSampleSize(width, this.HistoBits) * LosslessUtils.SubSampleSize(height, this.HistoBits); + ushort[] histogramSymbols = new ushort[histogramImageXySize]; + var huffTree = new HuffmanTree[3 * WebpConstants.CodeLengthCodes]; + for (int i = 0; i < huffTree.Length; i++) + { + huffTree[i] = default; + } + + if (useCache) + { + if (cacheBits == 0) + { + cacheBits = WebpConstants.MaxColorCacheBits; + } + } + else + { + cacheBits = 0; + } + + // Calculate backward references from BGRA image. + this.HashChain.Fill(this.memoryAllocator, bgra, this.quality, width, height, lowEffort); + + Vp8LBitWriter bitWriterBest = config.SubConfigs.Count > 1 ? this.bitWriter.Clone() : this.bitWriter; + Vp8LBitWriter bwInit = this.bitWriter; + bool isFirstIteration = true; + foreach (CrunchSubConfig subConfig in config.SubConfigs) + { + Vp8LBackwardRefs refsBest = BackwardReferenceEncoder.GetBackwardReferences( + width, + height, + bgra, + this.quality, + subConfig.Lz77, + ref cacheBits, + this.HashChain, + this.Refs[0], + this.Refs[1]); + + // Keep the best references aside and use the other element from the first + // two as a temporary for later usage. + Vp8LBackwardRefs refsTmp = this.Refs[refsBest.Equals(this.Refs[0]) ? 1 : 0]; + + this.bitWriter.Reset(bwInit); + var tmpHisto = new Vp8LHistogram(cacheBits); + var histogramImage = new List(histogramImageXySize); + for (int i = 0; i < histogramImageXySize; i++) + { + histogramImage.Add(new Vp8LHistogram(cacheBits)); + } + + // Build histogram image and symbols from backward references. + HistogramEncoder.GetHistoImageSymbols(width, height, refsBest, this.quality, this.HistoBits, cacheBits, histogramImage, tmpHisto, histogramSymbols); + + // Create Huffman bit lengths and codes for each histogram image. + int histogramImageSize = histogramImage.Count; + int bitArraySize = 5 * histogramImageSize; + var huffmanCodes = new HuffmanTreeCode[bitArraySize]; + for (int i = 0; i < huffmanCodes.Length; i++) + { + huffmanCodes[i] = default; + } + + GetHuffBitLengthsAndCodes(histogramImage, huffmanCodes); + + // Color Cache parameters. + if (cacheBits > 0) + { + this.bitWriter.PutBits(1, 1); + this.bitWriter.PutBits((uint)cacheBits, 4); + } + else + { + this.bitWriter.PutBits(0, 1); + } + + // Huffman image + meta huffman. + bool writeHistogramImage = histogramImageSize > 1; + this.bitWriter.PutBits((uint)(writeHistogramImage ? 1 : 0), 1); + if (writeHistogramImage) + { + using IMemoryOwner histogramBgraBuffer = this.memoryAllocator.Allocate(histogramImageXySize); + Span histogramBgra = histogramBgraBuffer.GetSpan(); + int maxIndex = 0; + for (int i = 0; i < histogramImageXySize; i++) + { + int symbolIndex = histogramSymbols[i] & 0xffff; + histogramBgra[i] = (uint)(symbolIndex << 8); + if (symbolIndex >= maxIndex) + { + maxIndex = symbolIndex + 1; + } + } + + this.bitWriter.PutBits((uint)(this.HistoBits - 2), 3); + this.EncodeImageNoHuffman( + histogramBgra, + this.HashChain, + refsTmp, + this.Refs[2], + LosslessUtils.SubSampleSize(width, this.HistoBits), + LosslessUtils.SubSampleSize(height, this.HistoBits), + this.quality, + lowEffort); + } + + // Store Huffman codes. + // Find maximum number of symbols for the huffman tree-set. + int maxTokens = 0; + for (int i = 0; i < 5 * histogramImage.Count; i++) + { + HuffmanTreeCode codes = huffmanCodes[i]; + if (maxTokens < codes.NumSymbols) + { + maxTokens = codes.NumSymbols; + } + } + + var tokens = new HuffmanTreeToken[maxTokens]; + for (int i = 0; i < tokens.Length; i++) + { + tokens[i] = new HuffmanTreeToken(); + } + + for (int i = 0; i < 5 * histogramImage.Count; i++) + { + HuffmanTreeCode codes = huffmanCodes[i]; + this.StoreHuffmanCode(huffTree, tokens, codes); + ClearHuffmanTreeIfOnlyOneSymbol(codes); + } + + // Store actual literals. + this.StoreImageToBitMask(width, this.HistoBits, refsBest, histogramSymbols, huffmanCodes); + + // Keep track of the smallest image so far. + if (isFirstIteration || (bitWriterBest != null && this.bitWriter.NumBytes() < bitWriterBest.NumBytes())) + { + Vp8LBitWriter tmp = this.bitWriter; + this.bitWriter = bitWriterBest; + bitWriterBest = tmp; + } + + isFirstIteration = false; + } + + this.bitWriter = bitWriterBest; + } + + /// + /// Save the palette to the bitstream. + /// + private void EncodePalette(bool lowEffort) + { + Span tmpPalette = new uint[WebpConstants.MaxPaletteSize]; + int paletteSize = this.PaletteSize; + Span palette = this.Palette.Memory.Span; + this.bitWriter.PutBits(WebpConstants.TransformPresent, 1); + this.bitWriter.PutBits((uint)Vp8LTransformType.ColorIndexingTransform, 2); + this.bitWriter.PutBits((uint)paletteSize - 1, 8); + for (int i = paletteSize - 1; i >= 1; i--) + { + tmpPalette[i] = LosslessUtils.SubPixels(palette[i], palette[i - 1]); + } + + tmpPalette[0] = palette[0]; + this.EncodeImageNoHuffman(tmpPalette, this.HashChain, this.Refs[0], this.Refs[1], width: paletteSize, height: 1, quality: 20, lowEffort); + } + + /// + /// Applies the subtract green transformation to the pixel data of the image. + /// + private void ApplySubtractGreen() + { + this.bitWriter.PutBits(WebpConstants.TransformPresent, 1); + this.bitWriter.PutBits((uint)Vp8LTransformType.SubtractGreen, 2); + LosslessUtils.SubtractGreenFromBlueAndRed(this.EncodedData.GetSpan()); + } + + private void ApplyPredictFilter(int width, int height, bool lowEffort) + { + // We disable near-lossless quantization if palette is used. + int nearLosslessStrength = this.UsePalette ? 100 : this.nearLosslessQuality; + int predBits = this.TransformBits; + int transformWidth = LosslessUtils.SubSampleSize(width, predBits); + int transformHeight = LosslessUtils.SubSampleSize(height, predBits); + + PredictorEncoder.ResidualImage( + width, + height, + predBits, + this.EncodedData.GetSpan(), + this.BgraScratch.GetSpan(), + this.TransformData.GetSpan(), + this.nearLossless, + nearLosslessStrength, + this.transparentColorMode, + this.UseSubtractGreenTransform, + lowEffort); + + this.bitWriter.PutBits(WebpConstants.TransformPresent, 1); + this.bitWriter.PutBits((uint)Vp8LTransformType.PredictorTransform, 2); + this.bitWriter.PutBits((uint)(predBits - 2), 3); + + this.EncodeImageNoHuffman(this.TransformData.GetSpan(), this.HashChain, this.Refs[0], this.Refs[1], transformWidth, transformHeight, this.quality, lowEffort); + } + + private void ApplyCrossColorFilter(int width, int height, bool lowEffort) + { + int colorTransformBits = this.TransformBits; + int transformWidth = LosslessUtils.SubSampleSize(width, colorTransformBits); + int transformHeight = LosslessUtils.SubSampleSize(height, colorTransformBits); + + PredictorEncoder.ColorSpaceTransform(width, height, colorTransformBits, this.quality, this.EncodedData.GetSpan(), this.TransformData.GetSpan()); + + this.bitWriter.PutBits(WebpConstants.TransformPresent, 1); + this.bitWriter.PutBits((uint)Vp8LTransformType.CrossColorTransform, 2); + this.bitWriter.PutBits((uint)(colorTransformBits - 2), 3); + + this.EncodeImageNoHuffman(this.TransformData.GetSpan(), this.HashChain, this.Refs[0], this.Refs[1], transformWidth, transformHeight, this.quality, lowEffort); + } + + private void EncodeImageNoHuffman(Span bgra, Vp8LHashChain hashChain, Vp8LBackwardRefs refsTmp1, Vp8LBackwardRefs refsTmp2, int width, int height, int quality, bool lowEffort) + { + int cacheBits = 0; + ushort[] histogramSymbols = new ushort[1]; // Only one tree, one symbol. + + var huffmanCodes = new HuffmanTreeCode[5]; + for (int i = 0; i < huffmanCodes.Length; i++) + { + huffmanCodes[i] = default; + } + + var huffTree = new HuffmanTree[3UL * WebpConstants.CodeLengthCodes]; + for (int i = 0; i < huffTree.Length; i++) + { + huffTree[i] = default; + } + + // Calculate backward references from the image pixels. + hashChain.Fill(this.memoryAllocator, bgra, quality, width, height, lowEffort); + + Vp8LBackwardRefs refs = BackwardReferenceEncoder.GetBackwardReferences( + width, + height, + bgra, + quality, + (int)Vp8LLz77Type.Lz77Standard | (int)Vp8LLz77Type.Lz77Rle, + ref cacheBits, + hashChain, + refsTmp1, + refsTmp2); + + var histogramImage = new List() + { + new Vp8LHistogram(cacheBits) + }; + + // Build histogram image and symbols from backward references. + histogramImage[0].StoreRefs(refs); + + // Create Huffman bit lengths and codes for each histogram image. + GetHuffBitLengthsAndCodes(histogramImage, huffmanCodes); + + // No color cache, no Huffman image. + this.bitWriter.PutBits(0, 1); + + // Find maximum number of symbols for the huffman tree-set. + int maxTokens = 0; + for (int i = 0; i < 5; i++) + { + HuffmanTreeCode codes = huffmanCodes[i]; + if (maxTokens < codes.NumSymbols) + { + maxTokens = codes.NumSymbols; + } + } + + var tokens = new HuffmanTreeToken[maxTokens]; + for (int i = 0; i < tokens.Length; i++) + { + tokens[i] = new HuffmanTreeToken(); + } + + // Store Huffman codes. + for (int i = 0; i < 5; i++) + { + HuffmanTreeCode codes = huffmanCodes[i]; + this.StoreHuffmanCode(huffTree, tokens, codes); + ClearHuffmanTreeIfOnlyOneSymbol(codes); + } + + // Store actual literals. + this.StoreImageToBitMask(width, 0, refs, histogramSymbols, huffmanCodes); + } + + private void StoreHuffmanCode(HuffmanTree[] huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode huffmanCode) + { + int count = 0; + int[] symbols = { 0, 0 }; + int maxBits = 8; + int maxSymbol = 1 << maxBits; + + // Check whether it's a small tree. + for (int i = 0; i < huffmanCode.NumSymbols && count < 3; i++) + { + if (huffmanCode.CodeLengths[i] != 0) + { + if (count < 2) + { + symbols[count] = i; + } + + count++; + } + } + + if (count == 0) + { + // Emit minimal tree for empty cases. + // bits: small tree marker: 1, count-1: 0, large 8-bit code: 0, code: 0 + this.bitWriter.PutBits(0x01, 4); + } + else if (count <= 2 && symbols[0] < maxSymbol && symbols[1] < maxSymbol) + { + this.bitWriter.PutBits(1, 1); // Small tree marker to encode 1 or 2 symbols. + this.bitWriter.PutBits((uint)(count - 1), 1); + if (symbols[0] <= 1) + { + this.bitWriter.PutBits(0, 1); // Code bit for small (1 bit) symbol value. + this.bitWriter.PutBits((uint)symbols[0], 1); + } + else + { + this.bitWriter.PutBits(1, 1); + this.bitWriter.PutBits((uint)symbols[0], 8); + } + + if (count == 2) + { + this.bitWriter.PutBits((uint)symbols[1], 8); + } + } + else + { + this.StoreFullHuffmanCode(huffTree, tokens, huffmanCode); + } + } + + private void StoreFullHuffmanCode(HuffmanTree[] huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode tree) + { + int i; + byte[] codeLengthBitDepth = new byte[WebpConstants.CodeLengthCodes]; + short[] codeLengthBitDepthSymbols = new short[WebpConstants.CodeLengthCodes]; + var huffmanCode = new HuffmanTreeCode + { + NumSymbols = WebpConstants.CodeLengthCodes, + CodeLengths = codeLengthBitDepth, + Codes = codeLengthBitDepthSymbols + }; + + this.bitWriter.PutBits(0, 1); + int numTokens = HuffmanUtils.CreateCompressedHuffmanTree(tree, tokens); + uint[] histogram = new uint[WebpConstants.CodeLengthCodes + 1]; + bool[] bufRle = new bool[WebpConstants.CodeLengthCodes + 1]; + for (i = 0; i < numTokens; i++) + { + histogram[tokens[i].Code]++; + } + + HuffmanUtils.CreateHuffmanTree(histogram, 7, bufRle, huffTree, huffmanCode); + this.StoreHuffmanTreeOfHuffmanTreeToBitMask(codeLengthBitDepth); + ClearHuffmanTreeIfOnlyOneSymbol(huffmanCode); + + int trailingZeroBits = 0; + int trimmedLength = numTokens; + i = numTokens; + while (i-- > 0) + { + int ix = tokens[i].Code; + if (ix is 0 or 17 or 18) + { + trimmedLength--; // Discount trailing zeros. + trailingZeroBits += codeLengthBitDepth[ix]; + if (ix == 17) + { + trailingZeroBits += 3; + } + else if (ix == 18) + { + trailingZeroBits += 7; + } + } + else + { + break; + } + } + + bool writeTrimmedLength = trimmedLength > 1 && trailingZeroBits > 12; + int length = writeTrimmedLength ? trimmedLength : numTokens; + this.bitWriter.PutBits((uint)(writeTrimmedLength ? 1 : 0), 1); + if (writeTrimmedLength) + { + if (trimmedLength == 2) + { + this.bitWriter.PutBits(0, 3 + 2); // nbitpairs=1, trimmedLength=2 + } + else + { + int nBits = Numerics.Log2((uint)trimmedLength - 2); + int nBitPairs = (nBits / 2) + 1; + this.bitWriter.PutBits((uint)nBitPairs - 1, 3); + this.bitWriter.PutBits((uint)trimmedLength - 2, nBitPairs * 2); + } + } + + this.StoreHuffmanTreeToBitMask(tokens, length, huffmanCode); + } + + private void StoreHuffmanTreeToBitMask(HuffmanTreeToken[] tokens, int numTokens, HuffmanTreeCode huffmanCode) + { + for (int i = 0; i < numTokens; i++) + { + int ix = tokens[i].Code; + int extraBits = tokens[i].ExtraBits; + this.bitWriter.PutBits((uint)huffmanCode.Codes[ix], huffmanCode.CodeLengths[ix]); + switch (ix) + { + case 16: + this.bitWriter.PutBits((uint)extraBits, 2); + break; + case 17: + this.bitWriter.PutBits((uint)extraBits, 3); + break; + case 18: + this.bitWriter.PutBits((uint)extraBits, 7); + break; + } + } + } + + private void StoreHuffmanTreeOfHuffmanTreeToBitMask(byte[] codeLengthBitDepth) + { + // RFC 1951 will calm you down if you are worried about this funny sequence. + // This sequence is tuned from that, but more weighted for lower symbol count, + // and more spiking histograms. + byte[] storageOrder = { 17, 18, 0, 1, 2, 3, 4, 5, 16, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; + + // Throw away trailing zeros: + int codesToStore = WebpConstants.CodeLengthCodes; + for (; codesToStore > 4; codesToStore--) + { + if (codeLengthBitDepth[storageOrder[codesToStore - 1]] != 0) + { + break; + } + } + + this.bitWriter.PutBits((uint)codesToStore - 4, 4); + for (int i = 0; i < codesToStore; i++) + { + this.bitWriter.PutBits(codeLengthBitDepth[storageOrder[i]], 3); + } + } + + private void StoreImageToBitMask(int width, int histoBits, Vp8LBackwardRefs backwardRefs, ushort[] histogramSymbols, HuffmanTreeCode[] huffmanCodes) + { + int histoXSize = histoBits > 0 ? LosslessUtils.SubSampleSize(width, histoBits) : 1; + int tileMask = histoBits == 0 ? 0 : -(1 << histoBits); + + // x and y trace the position in the image. + int x = 0; + int y = 0; + int tileX = x & tileMask; + int tileY = y & tileMask; + int histogramIx = histogramSymbols[0]; + Span codes = huffmanCodes.AsSpan(5 * histogramIx); + using List.Enumerator c = backwardRefs.Refs.GetEnumerator(); + while (c.MoveNext()) + { + PixOrCopy v = c.Current; + if (tileX != (x & tileMask) || tileY != (y & tileMask)) + { + tileX = x & tileMask; + tileY = y & tileMask; + histogramIx = histogramSymbols[((y >> histoBits) * histoXSize) + (x >> histoBits)]; + codes = huffmanCodes.AsSpan(5 * histogramIx); + } + + if (v.IsLiteral()) + { + byte[] order = { 1, 2, 0, 3 }; + for (int k = 0; k < 4; k++) + { + int code = (int)v.Literal(order[k]); + this.bitWriter.WriteHuffmanCode(codes[k], code); + } + } + else if (v.IsCacheIdx()) + { + int code = (int)v.CacheIdx(); + int literalIx = 256 + WebpConstants.NumLengthCodes + code; + this.bitWriter.WriteHuffmanCode(codes[0], literalIx); + } + else + { + int bits = 0; + int nBits = 0; + int distance = (int)v.Distance(); + int code = LosslessUtils.PrefixEncode(v.Len, ref nBits, ref bits); + this.bitWriter.WriteHuffmanCodeWithExtraBits(codes[0], 256 + code, bits, nBits); + + // Don't write the distance with the extra bits code since + // the distance can be up to 18 bits of extra bits, and the prefix + // 15 bits, totaling to 33, and our PutBits only supports up to 32 bits. + code = LosslessUtils.PrefixEncode(distance, ref nBits, ref bits); + this.bitWriter.WriteHuffmanCode(codes[4], code); + this.bitWriter.PutBits((uint)bits, nBits); + } + + x += v.Length(); + while (x >= width) + { + x -= width; + y++; + } + } + } + + /// + /// Analyzes the entropy of the input image to determine which transforms to use during encoding the image. + /// + /// The image to analyze as a bgra span. + /// The image width. + /// The image height. + /// Indicates whether a palette should be used. + /// The palette size. + /// The transformation bits. + /// Indicates if red and blue are always zero. + /// The entropy mode to use. + private EntropyIx AnalyzeEntropy(ReadOnlySpan bgra, int width, int height, bool usePalette, int paletteSize, int transformBits, out bool redAndBlueAlwaysZero) + { + if (usePalette && paletteSize <= 16) + { + // In the case of small palettes, we pack 2, 4 or 8 pixels together. In + // practice, small palettes are better than any other transform. + redAndBlueAlwaysZero = true; + return EntropyIx.Palette; + } + + using IMemoryOwner histoBuffer = this.memoryAllocator.Allocate((int)HistoIx.HistoTotal * 256); + Span histo = histoBuffer.Memory.Span; + uint pixPrev = bgra[0]; // Skip the first pixel. + ReadOnlySpan prevRow = null; + for (int y = 0; y < height; y++) + { + ReadOnlySpan currentRow = bgra.Slice(y * width, width); + for (int x = 0; x < width; x++) + { + uint pix = currentRow[x]; + uint pixDiff = LosslessUtils.SubPixels(pix, pixPrev); + pixPrev = pix; + if (pixDiff == 0 || (prevRow != null && pix == prevRow[x])) + { + continue; + } + + AddSingle( + pix, + histo.Slice((int)HistoIx.HistoAlpha * 256), + histo.Slice((int)HistoIx.HistoRed * 256), + histo.Slice((int)HistoIx.HistoGreen * 256), + histo.Slice((int)HistoIx.HistoBlue * 256)); + AddSingle( + pixDiff, + histo.Slice((int)HistoIx.HistoAlphaPred * 256), + histo.Slice((int)HistoIx.HistoRedPred * 256), + histo.Slice((int)HistoIx.HistoGreenPred * 256), + histo.Slice((int)HistoIx.HistoBluePred * 256)); + AddSingleSubGreen( + pix, + histo.Slice((int)HistoIx.HistoRedSubGreen * 256), + histo.Slice((int)HistoIx.HistoBlueSubGreen * 256)); + AddSingleSubGreen( + pixDiff, + histo.Slice((int)HistoIx.HistoRedPredSubGreen * 256), + histo.Slice((int)HistoIx.HistoBluePredSubGreen * 256)); + + // Approximate the palette by the entropy of the multiplicative hash. + uint hash = HashPix(pix); + histo[((int)HistoIx.HistoPalette * 256) + (int)hash]++; + } + + prevRow = currentRow; + } + + double[] entropyComp = new double[(int)HistoIx.HistoTotal]; + double[] entropy = new double[(int)EntropyIx.NumEntropyIx]; + int lastModeToAnalyze = usePalette ? (int)EntropyIx.Palette : (int)EntropyIx.SpatialSubGreen; + + // Let's add one zero to the predicted histograms. The zeros are removed + // too efficiently by the pixDiff == 0 comparison, at least one of the + // zeros is likely to exist. + histo[(int)HistoIx.HistoRedPredSubGreen * 256]++; + histo[(int)HistoIx.HistoBluePredSubGreen * 256]++; + histo[(int)HistoIx.HistoRedPred * 256]++; + histo[(int)HistoIx.HistoGreenPred * 256]++; + histo[(int)HistoIx.HistoBluePred * 256]++; + histo[(int)HistoIx.HistoAlphaPred * 256]++; + + for (int j = 0; j < (int)HistoIx.HistoTotal; j++) + { + var bitEntropy = new Vp8LBitEntropy(); + Span curHisto = histo.Slice(j * 256, 256); + bitEntropy.BitsEntropyUnrefined(curHisto, 256); + entropyComp[j] = bitEntropy.BitsEntropyRefine(); + } + + entropy[(int)EntropyIx.Direct] = entropyComp[(int)HistoIx.HistoAlpha] + + entropyComp[(int)HistoIx.HistoRed] + + entropyComp[(int)HistoIx.HistoGreen] + + entropyComp[(int)HistoIx.HistoBlue]; + entropy[(int)EntropyIx.Spatial] = entropyComp[(int)HistoIx.HistoAlphaPred] + + entropyComp[(int)HistoIx.HistoRedPred] + + entropyComp[(int)HistoIx.HistoGreenPred] + + entropyComp[(int)HistoIx.HistoBluePred]; + entropy[(int)EntropyIx.SubGreen] = entropyComp[(int)HistoIx.HistoAlpha] + + entropyComp[(int)HistoIx.HistoRedSubGreen] + + entropyComp[(int)HistoIx.HistoGreen] + + entropyComp[(int)HistoIx.HistoBlueSubGreen]; + entropy[(int)EntropyIx.SpatialSubGreen] = entropyComp[(int)HistoIx.HistoAlphaPred] + + entropyComp[(int)HistoIx.HistoRedPredSubGreen] + + entropyComp[(int)HistoIx.HistoGreenPred] + + entropyComp[(int)HistoIx.HistoBluePredSubGreen]; + entropy[(int)EntropyIx.Palette] = entropyComp[(int)HistoIx.HistoPalette]; + + // When including transforms, there is an overhead in bits from + // storing them. This overhead is small but matters for small images. + // For spatial, there are 14 transformations. + entropy[(int)EntropyIx.Spatial] += LosslessUtils.SubSampleSize(width, transformBits) * + LosslessUtils.SubSampleSize(height, transformBits) * + LosslessUtils.FastLog2(14); + + // For color transforms: 24 as only 3 channels are considered in a ColorTransformElement. + entropy[(int)EntropyIx.SpatialSubGreen] += LosslessUtils.SubSampleSize(width, transformBits) * + LosslessUtils.SubSampleSize(height, transformBits) * + LosslessUtils.FastLog2(24); + + // For palettes, add the cost of storing the palette. + // We empirically estimate the cost of a compressed entry as 8 bits. + // The palette is differential-coded when compressed hence a much + // lower cost than sizeof(uint32_t)*8. + entropy[(int)EntropyIx.Palette] += paletteSize * 8; + + EntropyIx minEntropyIx = EntropyIx.Direct; + for (int k = (int)EntropyIx.Direct + 1; k <= lastModeToAnalyze; k++) + { + if (entropy[(int)minEntropyIx] > entropy[k]) + { + minEntropyIx = (EntropyIx)k; + } + } + + redAndBlueAlwaysZero = true; + + // Let's check if the histogram of the chosen entropy mode has + // non-zero red and blue values. If all are zero, we can later skip + // the cross color optimization. + byte[][] histoPairs = + { + new[] { (byte)HistoIx.HistoRed, (byte)HistoIx.HistoBlue }, + new[] { (byte)HistoIx.HistoRedPred, (byte)HistoIx.HistoBluePred }, + new[] { (byte)HistoIx.HistoRedSubGreen, (byte)HistoIx.HistoBlueSubGreen }, + new[] { (byte)HistoIx.HistoRedPredSubGreen, (byte)HistoIx.HistoBluePredSubGreen }, + new[] { (byte)HistoIx.HistoRed, (byte)HistoIx.HistoBlue } + }; + Span redHisto = histo.Slice(256 * histoPairs[(int)minEntropyIx][0]); + Span blueHisto = histo.Slice(256 * histoPairs[(int)minEntropyIx][1]); + for (int i = 1; i < 256; i++) + { + if ((redHisto[i] | blueHisto[i]) != 0) + { + redAndBlueAlwaysZero = false; + break; + } + } + + return minEntropyIx; + } + + /// + /// If number of colors in the image is less than or equal to MaxPaletteSize, + /// creates a palette and returns true, else returns false. + /// + /// The image as packed bgra values. + /// The image width. + /// The image height. + /// true, if a palette should be used. + private bool AnalyzeAndCreatePalette(ReadOnlySpan bgra, int width, int height) + { + Span palette = this.Palette.Memory.Span; + this.PaletteSize = this.GetColorPalette(bgra, width, height, palette); + if (this.PaletteSize > WebpConstants.MaxPaletteSize) + { + this.PaletteSize = 0; + return false; + } + + uint[] paletteArray = palette.Slice(0, this.PaletteSize).ToArray(); + Array.Sort(paletteArray); + paletteArray.CopyTo(palette); + + if (PaletteHasNonMonotonousDeltas(palette, this.PaletteSize)) + { + GreedyMinimizeDeltas(palette, this.PaletteSize); + } + + return true; + } + + /// + /// Gets the color palette. + /// + /// The image to get the palette from as packed bgra values. + /// The image width. + /// The image height. + /// The span to store the palette into. + /// The number of palette entries. + private int GetColorPalette(ReadOnlySpan bgra, int width, int height, Span palette) + { + var colors = new HashSet(); + for (int y = 0; y < height; y++) + { + ReadOnlySpan bgraRow = bgra.Slice(y * width, width); + for (int x = 0; x < width; x++) + { + colors.Add(bgraRow[x]); + if (colors.Count > WebpConstants.MaxPaletteSize) + { + // Exact count is not needed, because a palette will not be used then anyway. + return WebpConstants.MaxPaletteSize + 1; + } + } + } + + // Fill the colors into the palette. + using HashSet.Enumerator colorEnumerator = colors.GetEnumerator(); + int idx = 0; + while (colorEnumerator.MoveNext()) + { + palette[idx++] = colorEnumerator.Current; + } + + return colors.Count; + } + + private void MapImageFromPalette(int width, int height) + { + Span src = this.EncodedData.GetSpan(); + int srcStride = this.CurrentWidth; + Span dst = this.EncodedData.GetSpan(); // Applying the palette will be done in place. + Span palette = this.Palette.GetSpan(); + int paletteSize = this.PaletteSize; + int xBits; + + // Replace each input pixel by corresponding palette index. + // This is done line by line. + if (paletteSize <= 4) + { + xBits = paletteSize <= 2 ? 3 : 2; + } + else + { + xBits = paletteSize <= 16 ? 1 : 0; + } + + this.CurrentWidth = LosslessUtils.SubSampleSize(width, xBits); + this.ApplyPalette(src, srcStride, dst, this.CurrentWidth, palette, paletteSize, width, height, xBits); + } + + /// + /// Remap bgra values in src[] to packed palettes entries in dst[] + /// using 'row' as a temporary buffer of size 'width'. + /// We assume that all src[] values have a corresponding entry in the palette. + /// Note: src[] can be the same as dst[] + /// + private void ApplyPalette(Span src, int srcStride, Span dst, int dstStride, Span palette, int paletteSize, int width, int height, int xBits) + { + using IMemoryOwner tmpRowBuffer = this.memoryAllocator.Allocate(width); + Span tmpRow = tmpRowBuffer.GetSpan(); + + if (paletteSize < ApplyPaletteGreedyMax) + { + uint prevPix = palette[0]; + uint prevIdx = 0; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + uint pix = src[x]; + if (pix != prevPix) + { + prevIdx = SearchColorGreedy(palette, pix); + prevPix = pix; + } + + tmpRow[x] = (byte)prevIdx; + } + + BundleColorMap(tmpRow, width, xBits, dst); + src = src.Slice(srcStride); + dst = dst.Slice(dstStride); + } + } + else + { + uint[] buffer = new uint[PaletteInvSize]; + + // Try to find a perfect hash function able to go from a color to an index + // within 1 << PaletteInvSize in order to build a hash map to go from color to index in palette. + int i; + for (i = 0; i < 3; i++) + { + bool useLut = true; + + // Set each element in buffer to max value. + buffer.AsSpan().Fill(uint.MaxValue); + + for (int j = 0; j < paletteSize; j++) + { + uint ind = 0; + switch (i) + { + case 0: + ind = ApplyPaletteHash0(palette[j]); + break; + case 1: + ind = ApplyPaletteHash1(palette[j]); + break; + case 2: + ind = ApplyPaletteHash2(palette[j]); + break; + } + + if (buffer[ind] != uint.MaxValue) + { + useLut = false; + break; + } + else + { + buffer[ind] = (uint)j; + } + } + + if (useLut) + { + break; + } + } + + if (i is 0 or 1 or 2) + { + ApplyPaletteFor(width, height, palette, i, src, srcStride, dst, dstStride, tmpRow, buffer, xBits); + } + else + { + uint[] idxMap = new uint[paletteSize]; + uint[] paletteSorted = new uint[paletteSize]; + PrepareMapToPalette(palette, paletteSize, paletteSorted, idxMap); + ApplyPaletteForWithIdxMap(width, height, palette, src, srcStride, dst, dstStride, tmpRow, idxMap, xBits, paletteSorted, paletteSize); + } + } + } + + private static void ApplyPaletteFor(int width, int height, Span palette, int hashIdx, Span src, int srcStride, Span dst, int dstStride, Span tmpRow, uint[] buffer, int xBits) + { + uint prevPix = palette[0]; + uint prevIdx = 0; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + uint pix = src[x]; + if (pix != prevPix) + { + switch (hashIdx) + { + case 0: + prevIdx = buffer[ApplyPaletteHash0(pix)]; + break; + case 1: + prevIdx = buffer[ApplyPaletteHash1(pix)]; + break; + case 2: + prevIdx = buffer[ApplyPaletteHash2(pix)]; + break; + } + + prevPix = pix; + } + + tmpRow[x] = (byte)prevIdx; + } + + LosslessUtils.BundleColorMap(tmpRow, width, xBits, dst); + + src = src.Slice(srcStride); + dst = dst.Slice(dstStride); + } + } + + private static void ApplyPaletteForWithIdxMap(int width, int height, Span palette, Span src, int srcStride, Span dst, int dstStride, Span tmpRow, uint[] idxMap, int xBits, uint[] paletteSorted, int paletteSize) + { + uint prevPix = palette[0]; + uint prevIdx = 0; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + uint pix = src[x]; + if (pix != prevPix) + { + prevIdx = idxMap[SearchColorNoIdx(paletteSorted, pix, paletteSize)]; + prevPix = pix; + } + + tmpRow[x] = (byte)prevIdx; + } + + LosslessUtils.BundleColorMap(tmpRow, width, xBits, dst); + + src = src.Slice(srcStride); + dst = dst.Slice(dstStride); + } + } + + /// + /// Sort palette in increasing order and prepare an inverse mapping array. + /// + private static void PrepareMapToPalette(Span palette, int numColors, uint[] sorted, uint[] idxMap) + { + palette.Slice(0, numColors).CopyTo(sorted); + Array.Sort(sorted, PaletteCompareColorsForSort); + for (int i = 0; i < numColors; i++) + { + idxMap[SearchColorNoIdx(sorted, palette[i], numColors)] = (uint)i; + } + } + + private static int SearchColorNoIdx(uint[] sorted, uint color, int hi) + { + int low = 0; + if (sorted[low] == color) + { + return low; // loop invariant: sorted[low] != color + } + + while (true) + { + int mid = (low + hi) >> 1; + if (sorted[mid] == color) + { + return mid; + } + else if (sorted[mid] < color) + { + low = mid; + } + else + { + hi = mid; + } + } + } + + private static void ClearHuffmanTreeIfOnlyOneSymbol(HuffmanTreeCode huffmanCode) + { + int count = 0; + for (int k = 0; k < huffmanCode.NumSymbols; k++) + { + if (huffmanCode.CodeLengths[k] != 0) + { + count++; + if (count > 1) + { + return; + } + } + } + + for (int k = 0; k < huffmanCode.NumSymbols; k++) + { + huffmanCode.CodeLengths[k] = 0; + huffmanCode.Codes[k] = 0; + } + } + + /// + /// The palette has been sorted by alpha. This function checks if the other components of the palette + /// have a monotonic development with regards to position in the palette. + /// If all have monotonic development, there is no benefit to re-organize them greedily. A monotonic development + /// would be spotted in green-only situations (like lossy alpha) or gray-scale images. + /// + /// The palette. + /// Number of colors in the palette. + /// True, if the palette has no monotonous deltas. + private static bool PaletteHasNonMonotonousDeltas(Span palette, int numColors) + { + uint predict = 0x000000; + byte signFound = 0x00; + for (int i = 0; i < numColors; i++) + { + uint diff = LosslessUtils.SubPixels(palette[i], predict); + byte rd = (byte)((diff >> 16) & 0xff); + byte gd = (byte)((diff >> 8) & 0xff); + byte bd = (byte)((diff >> 0) & 0xff); + if (rd != 0x00) + { + signFound |= (byte)(rd < 0x80 ? 1 : 2); + } + + if (gd != 0x00) + { + signFound |= (byte)(gd < 0x80 ? 8 : 16); + } + + if (bd != 0x00) + { + signFound |= (byte)(bd < 0x80 ? 64 : 128); + } + } + + return (signFound & (signFound << 1)) != 0; // two consequent signs. + } + + /// + /// Find greedily always the closest color of the predicted color to minimize + /// deltas in the palette. This reduces storage needs since the palette is stored with delta encoding. + /// + /// The palette. + /// The number of colors in the palette. + private static void GreedyMinimizeDeltas(Span palette, int numColors) + { + uint predict = 0x00000000; + for (int i = 0; i < numColors; i++) + { + int bestIdx = i; + uint bestScore = ~0U; + for (int k = i; k < numColors; k++) + { + uint curScore = PaletteColorDistance(palette[k], predict); + if (bestScore > curScore) + { + bestScore = curScore; + bestIdx = k; + } + } + + // Swap color(palette[bestIdx], palette[i]); + uint best = palette[bestIdx]; + palette[bestIdx] = palette[i]; + palette[i] = best; + predict = palette[i]; + } + } + + private static void GetHuffBitLengthsAndCodes(List histogramImage, HuffmanTreeCode[] huffmanCodes) + { + int maxNumSymbols = 0; + + // Iterate over all histograms and get the aggregate number of codes used. + for (int i = 0; i < histogramImage.Count; i++) + { + Vp8LHistogram histo = histogramImage[i]; + int startIdx = 5 * i; + for (int k = 0; k < 5; k++) + { + int numSymbols = + k == 0 ? histo.NumCodes() : + k == 4 ? WebpConstants.NumDistanceCodes : 256; + huffmanCodes[startIdx + k].NumSymbols = numSymbols; + } + } + + int end = 5 * histogramImage.Count; + for (int i = 0; i < end; i++) + { + int bitLength = huffmanCodes[i].NumSymbols; + huffmanCodes[i].Codes = new short[bitLength]; + huffmanCodes[i].CodeLengths = new byte[bitLength]; + if (maxNumSymbols < bitLength) + { + maxNumSymbols = bitLength; + } + } + + // Create Huffman trees. + bool[] bufRle = new bool[maxNumSymbols]; + var huffTree = new HuffmanTree[3 * maxNumSymbols]; + for (int i = 0; i < huffTree.Length; i++) + { + huffTree[i] = default; + } + + for (int i = 0; i < histogramImage.Count; i++) + { + int codesStartIdx = 5 * i; + Vp8LHistogram histo = histogramImage[i]; + HuffmanUtils.CreateHuffmanTree(histo.Literal, 15, bufRle, huffTree, huffmanCodes[codesStartIdx]); + HuffmanUtils.CreateHuffmanTree(histo.Red, 15, bufRle, huffTree, huffmanCodes[codesStartIdx + 1]); + HuffmanUtils.CreateHuffmanTree(histo.Blue, 15, bufRle, huffTree, huffmanCodes[codesStartIdx + 2]); + HuffmanUtils.CreateHuffmanTree(histo.Alpha, 15, bufRle, huffTree, huffmanCodes[codesStartIdx + 3]); + HuffmanUtils.CreateHuffmanTree(histo.Distance, 15, bufRle, huffTree, huffmanCodes[codesStartIdx + 4]); + } + } + + /// + /// Computes a value that is related to the entropy created by the palette entry diff. + /// + /// First color. + /// Second color. + /// The color distance. + [MethodImpl(InliningOptions.ShortMethod)] + private static uint PaletteColorDistance(uint col1, uint col2) + { + uint diff = LosslessUtils.SubPixels(col1, col2); + uint moreWeightForRGBThanForAlpha = 9; + uint score = PaletteComponentDistance((diff >> 0) & 0xff); + score += PaletteComponentDistance((diff >> 8) & 0xff); + score += PaletteComponentDistance((diff >> 16) & 0xff); + score *= moreWeightForRGBThanForAlpha; + score += PaletteComponentDistance((diff >> 24) & 0xff); + + return score; + } + + /// + /// Calculates the huffman image bits. + /// + private static int GetHistoBits(WebpEncodingMethod method, bool usePalette, int width, int height) + { + // Make tile size a function of encoding method (Range: 0 to 6). + int histoBits = (usePalette ? 9 : 7) - (int)method; + while (true) + { + int huffImageSize = LosslessUtils.SubSampleSize(width, histoBits) * LosslessUtils.SubSampleSize(height, histoBits); + if (huffImageSize <= WebpConstants.MaxHuffImageSize) + { + break; + } + + histoBits++; + } + + return histoBits < WebpConstants.MinHuffmanBits ? WebpConstants.MinHuffmanBits : + histoBits > WebpConstants.MaxHuffmanBits ? WebpConstants.MaxHuffmanBits : histoBits; + } + + /// + /// Bundles multiple (1, 2, 4 or 8) pixels into a single pixel. + /// + private static void BundleColorMap(Span row, int width, int xBits, Span dst) + { + int x; + if (xBits > 0) + { + int bitDepth = 1 << (3 - xBits); + int mask = (1 << xBits) - 1; + uint code = 0xff000000; + for (x = 0; x < width; x++) + { + int xSub = x & mask; + if (xSub == 0) + { + code = 0xff000000; + } + + code |= (uint)(row[x] << (8 + (bitDepth * xSub))); + dst[x >> xBits] = code; + } + } + else + { + for (x = 0; x < width; x++) + { + dst[x] = (uint)(0xff000000 | (row[x] << 8)); + } + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private void BitWriterSwap(ref Vp8LBitWriter src, ref Vp8LBitWriter dst) + { + Vp8LBitWriter tmp = src; + src = dst; + dst = tmp; + } + + /// + /// Calculates the bits used for the transformation. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static int GetTransformBits(WebpEncodingMethod method, int histoBits) + { + int maxTransformBits = (int)method < 4 ? 6 : method > WebpEncodingMethod.Level4 ? 4 : 5; + int res = histoBits > maxTransformBits ? maxTransformBits : histoBits; + return res; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void AddSingle(uint p, Span a, Span r, Span g, Span b) + { + a[(int)(p >> 24) & 0xff]++; + r[(int)(p >> 16) & 0xff]++; + g[(int)(p >> 8) & 0xff]++; + b[(int)(p >> 0) & 0xff]++; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void AddSingleSubGreen(uint p, Span r, Span b) + { + int green = (int)p >> 8; // The upper bits are masked away later. + r[(int)((p >> 16) - green) & 0xff]++; + b[(int)((p >> 0) - green) & 0xff]++; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint SearchColorGreedy(Span palette, uint color) + { + if (color == palette[0]) + { + return 0; + } + + if (color == palette[1]) + { + return 1; + } + + if (color == palette[2]) + { + return 2; + } + + return 3; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint ApplyPaletteHash0(uint color) => (color >> 8) & 0xff; // Focus on the green color. + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint ApplyPaletteHash1(uint color) => (uint)((color & 0x00ffffffu) * 4222244071ul) >> (32 - PaletteInvSizeBits); // Forget about alpha. + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint ApplyPaletteHash2(uint color) => (uint)((color & 0x00ffffffu) * ((1ul << 31) - 1)) >> (32 - PaletteInvSizeBits); // Forget about alpha. + + // Note that masking with 0xffffffffu is for preventing an + // 'unsigned int overflow' warning. Doesn't impact the compiled code. + [MethodImpl(InliningOptions.ShortMethod)] + private static uint HashPix(uint pix) => (uint)((((long)pix + (pix >> 19)) * 0x39c5fba7L) & 0xffffffffu) >> 24; + + [MethodImpl(InliningOptions.ShortMethod)] + private static int PaletteCompareColorsForSort(uint p1, uint p2) => p1 < p2 ? -1 : 1; + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint PaletteComponentDistance(uint v) => (v <= 128) ? v : (256 - v); + + public void AllocateTransformBuffer(int width, int height) + { + // VP8LResidualImage needs room for 2 scanlines of uint32 pixels with an extra + // pixel in each, plus 2 regular scanlines of bytes. + int bgraScratchSize = this.UsePredictorTransform ? ((width + 1) * 2) + (((width * 2) + 4 - 1) / 4) : 0; + int transformDataSize = this.UsePredictorTransform || this.UseCrossColorTransform ? LosslessUtils.SubSampleSize(width, this.TransformBits) * LosslessUtils.SubSampleSize(height, this.TransformBits) : 0; + + this.BgraScratch = this.memoryAllocator.Allocate(bgraScratchSize); + this.TransformData = this.memoryAllocator.Allocate(transformDataSize); + this.CurrentWidth = width; + } + + /// + /// Clears the backward references. + /// + public void ClearRefs() + { + for (int i = 0; i < this.Refs.Length; i++) + { + this.Refs[i].Refs.Clear(); + } + } + + /// + public void Dispose() + { + this.Bgra.Dispose(); + this.EncodedData.Dispose(); + this.BgraScratch.Dispose(); + this.Palette.Dispose(); + this.TransformData.Dispose(); + } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHashChain.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHashChain.cs new file mode 100644 index 000000000..977a094bd --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHashChain.cs @@ -0,0 +1,284 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + internal class Vp8LHashChain + { + private const uint HashMultiplierHi = 0xc6a4a793u; + + private const uint HashMultiplierLo = 0x5bd1e996u; + + private const int HashBits = 18; + + private const int HashSize = 1 << HashBits; + + /// + /// The number of bits for the window size. + /// + private const int WindowSizeBits = 20; + + /// + /// 1M window (4M bytes) minus 120 special codes for short distances. + /// + private const int WindowSize = (1 << WindowSizeBits) - 120; + + /// + /// Initializes a new instance of the class. + /// + /// The size off the chain. + public Vp8LHashChain(int size) + { + this.OffsetLength = new uint[size]; + this.OffsetLength.AsSpan().Fill(0xcdcdcdcd); + this.Size = size; + } + + /// + /// Gets the offset length. + /// The 20 most significant bits contain the offset at which the best match is found. + /// These 20 bits are the limit defined by GetWindowSizeForHashChain (through WindowSize = 1 << 20). + /// The lower 12 bits contain the length of the match. + /// + public uint[] OffsetLength { get; } + + /// + /// Gets the size of the hash chain. + /// This is the maximum size of the hash_chain that can be constructed. + /// Typically this is the pixel count (width x height) for a given image. + /// + public int Size { get; } + + public void Fill(MemoryAllocator memoryAllocator, ReadOnlySpan bgra, int quality, int xSize, int ySize, bool lowEffort) + { + int size = xSize * ySize; + int iterMax = GetMaxItersForQuality(quality); + int windowSize = GetWindowSizeForHashChain(quality, xSize); + int pos; + + if (size <= 2) + { + this.OffsetLength[0] = 0; + return; + } + + using IMemoryOwner hashToFirstIndexBuffer = memoryAllocator.Allocate(HashSize); + Span hashToFirstIndex = hashToFirstIndexBuffer.GetSpan(); + + // Initialize hashToFirstIndex array to -1. + hashToFirstIndex.Fill(-1); + + int[] chain = new int[size]; + + // Fill the chain linking pixels with the same hash. + bool bgraComp = bgra.Length > 1 && bgra[0] == bgra[1]; + for (pos = 0; pos < size - 2;) + { + uint hashCode; + bool bgraCompNext = bgra[pos + 1] == bgra[pos + 2]; + if (bgraComp && bgraCompNext) + { + // Consecutive pixels with the same color will share the same hash. + // We therefore use a different hash: the color and its repetition length. + uint[] tmp = new uint[2]; + uint len = 1; + tmp[0] = bgra[pos]; + + // Figure out how far the pixels are the same. The last pixel has a different 64 bit hash, + // as its next pixel does not have the same color, so we just need to get to + // the last pixel equal to its follower. + while (pos + (int)len + 2 < size && bgra[(int)(pos + len + 2)] == bgra[pos]) + { + ++len; + } + + if (len > BackwardReferenceEncoder.MaxLength) + { + // Skip the pixels that match for distance=1 and length>MaxLength + // because they are linked to their predecessor and we automatically + // check that in the main for loop below. Skipping means setting no + // predecessor in the chain, hence -1. + pos += (int)(len - BackwardReferenceEncoder.MaxLength); + len = BackwardReferenceEncoder.MaxLength; + } + + // Process the rest of the hash chain. + while (len > 0) + { + tmp[1] = len--; + hashCode = GetPixPairHash64(tmp); + chain[pos] = hashToFirstIndex[(int)hashCode]; + hashToFirstIndex[(int)hashCode] = pos++; + } + + bgraComp = false; + } + else + { + // Just move one pixel forward. + hashCode = GetPixPairHash64(bgra.Slice(pos)); + chain[pos] = hashToFirstIndex[(int)hashCode]; + hashToFirstIndex[(int)hashCode] = pos++; + bgraComp = bgraCompNext; + } + } + + // Process the penultimate pixel. + chain[pos] = hashToFirstIndex[(int)GetPixPairHash64(bgra.Slice(pos))]; + + // Find the best match interval at each pixel, defined by an offset to the + // pixel and a length. The right-most pixel cannot match anything to the right + // (hence a best length of 0) and the left-most pixel nothing to the left (hence an offset of 0). + this.OffsetLength[0] = this.OffsetLength[size - 1] = 0; + for (int basePosition = size - 2; basePosition > 0;) + { + int maxLen = LosslessUtils.MaxFindCopyLength(size - 1 - basePosition); + int bgraStart = basePosition; + int iter = iterMax; + int bestLength = 0; + uint bestDistance = 0; + int minPos = basePosition > windowSize ? basePosition - windowSize : 0; + int lengthMax = maxLen < 256 ? maxLen : 256; + pos = chain[basePosition]; + int currLength; + + if (!lowEffort) + { + // Heuristic: use the comparison with the above line as an initialization. + if (basePosition >= (uint)xSize) + { + currLength = LosslessUtils.FindMatchLength(bgra.Slice(bgraStart - xSize), bgra.Slice(bgraStart), bestLength, maxLen); + if (currLength > bestLength) + { + bestLength = currLength; + bestDistance = (uint)xSize; + } + + iter--; + } + + // Heuristic: compare to the previous pixel. + currLength = LosslessUtils.FindMatchLength(bgra.Slice(bgraStart - 1), bgra.Slice(bgraStart), bestLength, maxLen); + if (currLength > bestLength) + { + bestLength = currLength; + bestDistance = 1; + } + + iter--; + + // Skip the for loop if we already have the maximum. + if (bestLength == BackwardReferenceEncoder.MaxLength) + { + pos = minPos - 1; + } + } + + uint bestBgra = bgra.Slice(bgraStart)[bestLength]; + + for (; pos >= minPos && (--iter > 0); pos = chain[pos]) + { + if (bgra[pos + bestLength] != bestBgra) + { + continue; + } + + currLength = LosslessUtils.VectorMismatch(bgra.Slice(pos), bgra.Slice(bgraStart), maxLen); + if (bestLength < currLength) + { + bestLength = currLength; + bestDistance = (uint)(basePosition - pos); + bestBgra = bgra.Slice(bgraStart)[bestLength]; + + // Stop if we have reached a good enough length. + if (bestLength >= lengthMax) + { + break; + } + } + } + + // We have the best match but in case the two intervals continue matching + // to the left, we have the best matches for the left-extended pixels. + uint maxBasePosition = (uint)basePosition; + while (true) + { + this.OffsetLength[basePosition] = (bestDistance << BackwardReferenceEncoder.MaxLengthBits) | (uint)bestLength; + --basePosition; + + // Stop if we don't have a match or if we are out of bounds. + if (bestDistance == 0 || basePosition == 0) + { + break; + } + + // Stop if we cannot extend the matching intervals to the left. + if (basePosition < bestDistance || bgra[(int)(basePosition - bestDistance)] != bgra[basePosition]) + { + break; + } + + // Stop if we are matching at its limit because there could be a closer + // matching interval with the same maximum length. Then again, if the + // matching interval is as close as possible (best_distance == 1), we will + // never find anything better so let's continue. + if (bestLength == BackwardReferenceEncoder.MaxLength && bestDistance != 1 && basePosition + BackwardReferenceEncoder.MaxLength < maxBasePosition) + { + break; + } + + if (bestLength < BackwardReferenceEncoder.MaxLength) + { + bestLength++; + maxBasePosition = (uint)basePosition; + } + } + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public int FindLength(int basePosition) => (int)(this.OffsetLength[basePosition] & ((1U << BackwardReferenceEncoder.MaxLengthBits) - 1)); + + [MethodImpl(InliningOptions.ShortMethod)] + public int FindOffset(int basePosition) => (int)(this.OffsetLength[basePosition] >> BackwardReferenceEncoder.MaxLengthBits); + + /// + /// Calculates the hash for a pixel pair. + /// + /// An Span with two pixels. + /// The hash. + [MethodImpl(InliningOptions.ShortMethod)] + private static uint GetPixPairHash64(ReadOnlySpan bgra) + { + uint key = bgra[1] * HashMultiplierHi; + key += bgra[0] * HashMultiplierLo; + key >>= 32 - HashBits; + return key; + } + + /// + /// Returns the maximum number of hash chain lookups to do for a + /// given compression quality. Return value in range [8, 86]. + /// + /// The quality. + /// Number of hash chain lookups. + [MethodImpl(InliningOptions.ShortMethod)] + private static int GetMaxItersForQuality(int quality) => 8 + (quality * quality / 128); + + [MethodImpl(InliningOptions.ShortMethod)] + private static int GetWindowSizeForHashChain(int quality, int xSize) + { + int maxWindowSize = quality > 75 ? WindowSize + : quality > 50 ? xSize << 8 + : quality > 25 ? xSize << 6 + : xSize << 4; + + return maxWindowSize > WindowSize ? WindowSize : maxWindowSize; + } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs new file mode 100644 index 000000000..42260e2b2 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs @@ -0,0 +1,515 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + internal class Vp8LHistogram : IDeepCloneable + { + private const uint NonTrivialSym = 0xffffffff; + + /// + /// Initializes a new instance of the class. + /// + /// The histogram to create an instance from. + private Vp8LHistogram(Vp8LHistogram other) + : this(other.PaletteCodeBits) + { + other.Red.AsSpan().CopyTo(this.Red); + other.Blue.AsSpan().CopyTo(this.Blue); + other.Alpha.AsSpan().CopyTo(this.Alpha); + other.Literal.AsSpan().CopyTo(this.Literal); + other.Distance.AsSpan().CopyTo(this.Distance); + other.IsUsed.AsSpan().CopyTo(this.IsUsed); + this.LiteralCost = other.LiteralCost; + this.RedCost = other.RedCost; + this.BlueCost = other.BlueCost; + this.BitCost = other.BitCost; + this.TrivialSymbol = other.TrivialSymbol; + this.PaletteCodeBits = other.PaletteCodeBits; + } + + /// + /// Initializes a new instance of the class. + /// + /// The backward references to initialize the histogram with. + /// The palette code bits. + public Vp8LHistogram(Vp8LBackwardRefs refs, int paletteCodeBits) + : this(paletteCodeBits) => this.StoreRefs(refs); + + /// + /// Initializes a new instance of the class. + /// + /// The palette code bits. + public Vp8LHistogram(int paletteCodeBits) + { + this.PaletteCodeBits = paletteCodeBits; + this.Red = new uint[WebpConstants.NumLiteralCodes + 1]; + this.Blue = new uint[WebpConstants.NumLiteralCodes + 1]; + this.Alpha = new uint[WebpConstants.NumLiteralCodes + 1]; + this.Distance = new uint[WebpConstants.NumDistanceCodes]; + + int literalSize = WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes + (1 << WebpConstants.MaxColorCacheBits); + this.Literal = new uint[literalSize + 1]; + + // 5 for literal, red, blue, alpha, distance. + this.IsUsed = new bool[5]; + } + + /// + /// Gets or sets the palette code bits. + /// + public int PaletteCodeBits { get; set; } + + /// + /// Gets or sets the cached value of bit cost. + /// + public double BitCost { get; set; } + + /// + /// Gets or sets the cached value of literal entropy costs. + /// + public double LiteralCost { get; set; } + + /// + /// Gets or sets the cached value of red entropy costs. + /// + public double RedCost { get; set; } + + /// + /// Gets or sets the cached value of blue entropy costs. + /// + public double BlueCost { get; set; } + + public uint[] Red { get; } + + public uint[] Blue { get; } + + public uint[] Alpha { get; } + + public uint[] Literal { get; } + + public uint[] Distance { get; } + + public uint TrivialSymbol { get; set; } + + public bool[] IsUsed { get; } + + /// + public IDeepCloneable DeepClone() => new Vp8LHistogram(this); + + /// + /// Collect all the references into a histogram (without reset). + /// + /// The backward references. + public void StoreRefs(Vp8LBackwardRefs refs) + { + using List.Enumerator c = refs.Refs.GetEnumerator(); + while (c.MoveNext()) + { + this.AddSinglePixOrCopy(c.Current, false); + } + } + + /// + /// Accumulate a token 'v' into a histogram. + /// + /// The token to add. + /// Indicates whether to use the distance modifier. + /// xSize is only used when useDistanceModifier is true. + public void AddSinglePixOrCopy(PixOrCopy v, bool useDistanceModifier, int xSize = 0) + { + if (v.IsLiteral()) + { + this.Alpha[v.Literal(3)]++; + this.Red[v.Literal(2)]++; + this.Literal[v.Literal(1)]++; + this.Blue[v.Literal(0)]++; + } + else if (v.IsCacheIdx()) + { + int literalIx = (int)(WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes + v.CacheIdx()); + this.Literal[literalIx]++; + } + else + { + int extraBits = 0; + int code = LosslessUtils.PrefixEncodeBits(v.Length(), ref extraBits); + this.Literal[WebpConstants.NumLiteralCodes + code]++; + if (!useDistanceModifier) + { + code = LosslessUtils.PrefixEncodeBits((int)v.Distance(), ref extraBits); + } + else + { + code = LosslessUtils.PrefixEncodeBits(BackwardReferenceEncoder.DistanceToPlaneCode(xSize, (int)v.Distance()), ref extraBits); + } + + this.Distance[code]++; + } + } + + public int NumCodes() => WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes + (this.PaletteCodeBits > 0 ? 1 << this.PaletteCodeBits : 0); + + /// + /// Estimate how many bits the combined entropy of literals and distance approximately maps to. + /// + /// Estimated bits. + public double EstimateBits() + { + uint notUsed = 0; + return + PopulationCost(this.Literal, this.NumCodes(), ref notUsed, ref this.IsUsed[0]) + + PopulationCost(this.Red, WebpConstants.NumLiteralCodes, ref notUsed, ref this.IsUsed[1]) + + PopulationCost(this.Blue, WebpConstants.NumLiteralCodes, ref notUsed, ref this.IsUsed[2]) + + PopulationCost(this.Alpha, WebpConstants.NumLiteralCodes, ref notUsed, ref this.IsUsed[3]) + + PopulationCost(this.Distance, WebpConstants.NumDistanceCodes, ref notUsed, ref this.IsUsed[4]) + + ExtraCost(this.Literal.AsSpan(WebpConstants.NumLiteralCodes), WebpConstants.NumLengthCodes) + + ExtraCost(this.Distance, WebpConstants.NumDistanceCodes); + } + + public void UpdateHistogramCost() + { + uint alphaSym = 0, redSym = 0, blueSym = 0; + uint notUsed = 0; + double alphaCost = PopulationCost(this.Alpha, WebpConstants.NumLiteralCodes, ref alphaSym, ref this.IsUsed[3]); + double distanceCost = PopulationCost(this.Distance, WebpConstants.NumDistanceCodes, ref notUsed, ref this.IsUsed[4]) + ExtraCost(this.Distance, WebpConstants.NumDistanceCodes); + int numCodes = this.NumCodes(); + this.LiteralCost = PopulationCost(this.Literal, numCodes, ref notUsed, ref this.IsUsed[0]) + ExtraCost(this.Literal.AsSpan(WebpConstants.NumLiteralCodes), WebpConstants.NumLengthCodes); + this.RedCost = PopulationCost(this.Red, WebpConstants.NumLiteralCodes, ref redSym, ref this.IsUsed[1]); + this.BlueCost = PopulationCost(this.Blue, WebpConstants.NumLiteralCodes, ref blueSym, ref this.IsUsed[2]); + this.BitCost = this.LiteralCost + this.RedCost + this.BlueCost + alphaCost + distanceCost; + if ((alphaSym | redSym | blueSym) == NonTrivialSym) + { + this.TrivialSymbol = NonTrivialSym; + } + else + { + this.TrivialSymbol = (alphaSym << 24) | (redSym << 16) | (blueSym << 0); + } + } + + /// + /// Performs output = a + b, computing the cost C(a+b) - C(a) - C(b) while comparing + /// to the threshold value 'costThreshold'. The score returned is + /// Score = C(a+b) - C(a) - C(b), where C(a) + C(b) is known and fixed. + /// Since the previous score passed is 'costThreshold', we only need to compare + /// the partial cost against 'costThreshold + C(a) + C(b)' to possibly bail-out early. + /// + public double AddEval(Vp8LHistogram b, double costThreshold, Vp8LHistogram output) + { + double sumCost = this.BitCost + b.BitCost; + costThreshold += sumCost; + if (this.GetCombinedHistogramEntropy(b, costThreshold, costInitial: 0, out double cost)) + { + this.Add(b, output); + output.BitCost = cost; + output.PaletteCodeBits = this.PaletteCodeBits; + } + + return cost - sumCost; + } + + public double AddThresh(Vp8LHistogram b, double costThreshold) + { + double costInitial = -this.BitCost; + this.GetCombinedHistogramEntropy(b, costThreshold, costInitial, out double cost); + return cost; + } + + public void Add(Vp8LHistogram b, Vp8LHistogram output) + { + int literalSize = this.NumCodes(); + + this.AddLiteral(b, output, literalSize); + this.AddRed(b, output, WebpConstants.NumLiteralCodes); + this.AddBlue(b, output, WebpConstants.NumLiteralCodes); + this.AddAlpha(b, output, WebpConstants.NumLiteralCodes); + this.AddDistance(b, output, WebpConstants.NumDistanceCodes); + + for (int i = 0; i < 5; i++) + { + output.IsUsed[i] = this.IsUsed[i] | b.IsUsed[i]; + } + + output.TrivialSymbol = this.TrivialSymbol == b.TrivialSymbol + ? this.TrivialSymbol + : NonTrivialSym; + } + + public bool GetCombinedHistogramEntropy(Vp8LHistogram b, double costThreshold, double costInitial, out double cost) + { + bool trivialAtEnd = false; + cost = costInitial; + + cost += GetCombinedEntropy(this.Literal, b.Literal, this.NumCodes(), this.IsUsed[0], b.IsUsed[0], false); + + cost += ExtraCostCombined(this.Literal.AsSpan(WebpConstants.NumLiteralCodes), b.Literal.AsSpan(WebpConstants.NumLiteralCodes), WebpConstants.NumLengthCodes); + + if (cost > costThreshold) + { + return false; + } + + if (this.TrivialSymbol != NonTrivialSym && this.TrivialSymbol == b.TrivialSymbol) + { + // A, R and B are all 0 or 0xff. + uint colorA = (this.TrivialSymbol >> 24) & 0xff; + uint colorR = (this.TrivialSymbol >> 16) & 0xff; + uint colorB = (this.TrivialSymbol >> 0) & 0xff; + if ((colorA == 0 || colorA == 0xff) && + (colorR == 0 || colorR == 0xff) && + (colorB == 0 || colorB == 0xff)) + { + trivialAtEnd = true; + } + } + + cost += GetCombinedEntropy(this.Red, b.Red, WebpConstants.NumLiteralCodes, this.IsUsed[1], b.IsUsed[1], trivialAtEnd); + if (cost > costThreshold) + { + return false; + } + + cost += GetCombinedEntropy(this.Blue, b.Blue, WebpConstants.NumLiteralCodes, this.IsUsed[2], b.IsUsed[2], trivialAtEnd); + if (cost > costThreshold) + { + return false; + } + + cost += GetCombinedEntropy(this.Alpha, b.Alpha, WebpConstants.NumLiteralCodes, this.IsUsed[3], b.IsUsed[3], trivialAtEnd); + if (cost > costThreshold) + { + return false; + } + + cost += GetCombinedEntropy(this.Distance, b.Distance, WebpConstants.NumDistanceCodes, this.IsUsed[4], b.IsUsed[4], false); + if (cost > costThreshold) + { + return false; + } + + cost += ExtraCostCombined(this.Distance, b.Distance, WebpConstants.NumDistanceCodes); + if (cost > costThreshold) + { + return false; + } + + return true; + } + + private void AddLiteral(Vp8LHistogram b, Vp8LHistogram output, int literalSize) + { + if (this.IsUsed[0]) + { + if (b.IsUsed[0]) + { + AddVector(this.Literal, b.Literal, output.Literal, literalSize); + } + else + { + this.Literal.AsSpan(0, literalSize).CopyTo(output.Literal); + } + } + else if (b.IsUsed[0]) + { + b.Literal.AsSpan(0, literalSize).CopyTo(output.Literal); + } + else + { + output.Literal.AsSpan(0, literalSize).Fill(0); + } + } + + private void AddRed(Vp8LHistogram b, Vp8LHistogram output, int size) + { + if (this.IsUsed[1]) + { + if (b.IsUsed[1]) + { + AddVector(this.Red, b.Red, output.Red, size); + } + else + { + this.Red.AsSpan(0, size).CopyTo(output.Red); + } + } + else if (b.IsUsed[1]) + { + b.Red.AsSpan(0, size).CopyTo(output.Red); + } + else + { + output.Red.AsSpan(0, size).Fill(0); + } + } + + private void AddBlue(Vp8LHistogram b, Vp8LHistogram output, int size) + { + if (this.IsUsed[2]) + { + if (b.IsUsed[2]) + { + AddVector(this.Blue, b.Blue, output.Blue, size); + } + else + { + this.Blue.AsSpan(0, size).CopyTo(output.Blue); + } + } + else if (b.IsUsed[2]) + { + b.Blue.AsSpan(0, size).CopyTo(output.Blue); + } + else + { + output.Blue.AsSpan(0, size).Fill(0); + } + } + + private void AddAlpha(Vp8LHistogram b, Vp8LHistogram output, int size) + { + if (this.IsUsed[3]) + { + if (b.IsUsed[3]) + { + AddVector(this.Alpha, b.Alpha, output.Alpha, size); + } + else + { + this.Alpha.AsSpan(0, size).CopyTo(output.Alpha); + } + } + else if (b.IsUsed[3]) + { + b.Alpha.AsSpan(0, size).CopyTo(output.Alpha); + } + else + { + output.Alpha.AsSpan(0, size).Fill(0); + } + } + + private void AddDistance(Vp8LHistogram b, Vp8LHistogram output, int size) + { + if (this.IsUsed[4]) + { + if (b.IsUsed[4]) + { + AddVector(this.Distance, b.Distance, output.Distance, size); + } + else + { + this.Distance.AsSpan(0, size).CopyTo(output.Distance); + } + } + else if (b.IsUsed[4]) + { + b.Distance.AsSpan(0, size).CopyTo(output.Distance); + } + else + { + output.Distance.AsSpan(0, size).Fill(0); + } + } + + private static double GetCombinedEntropy(uint[] x, uint[] y, int length, bool isXUsed, bool isYUsed, bool trivialAtEnd) + { + var stats = new Vp8LStreaks(); + if (trivialAtEnd) + { + // This configuration is due to palettization that transforms an indexed + // pixel into 0xff000000 | (pixel << 8) in BundleColorMap. + // BitsEntropyRefine is 0 for histograms with only one non-zero value. + // Only FinalHuffmanCost needs to be evaluated. + + // Deal with the non-zero value at index 0 or length-1. + stats.Streaks[1][0] = 1; + + // Deal with the following/previous zero streak. + stats.Counts[0] = 1; + stats.Streaks[0][1] = length - 1; + + return stats.FinalHuffmanCost(); + } + + var bitEntropy = new Vp8LBitEntropy(); + if (isXUsed) + { + if (isYUsed) + { + bitEntropy.GetCombinedEntropyUnrefined(x, y, length, stats); + } + else + { + bitEntropy.GetEntropyUnrefined(x, length, stats); + } + } + else + { + if (isYUsed) + { + bitEntropy.GetEntropyUnrefined(y, length, stats); + } + else + { + stats.Counts[0] = 1; + stats.Streaks[0][length > 3 ? 1 : 0] = length; + bitEntropy.Init(); + } + } + + return bitEntropy.BitsEntropyRefine() + stats.FinalHuffmanCost(); + } + + private static double ExtraCostCombined(Span x, Span y, int length) + { + double cost = 0.0d; + for (int i = 2; i < length - 2; i++) + { + int xy = (int)(x[i + 2] + y[i + 2]); + cost += (i >> 1) * xy; + } + + return cost; + } + + /// + /// Get the symbol entropy for the distribution 'population'. + /// + private static double PopulationCost(uint[] population, int length, ref uint trivialSym, ref bool isUsed) + { + var bitEntropy = new Vp8LBitEntropy(); + var stats = new Vp8LStreaks(); + bitEntropy.BitsEntropyUnrefined(population, length, stats); + + trivialSym = (bitEntropy.NoneZeros == 1) ? bitEntropy.NoneZeroCode : NonTrivialSym; + + // The histogram is used if there is at least one non-zero streak. + isUsed = stats.Streaks[1][0] != 0 || stats.Streaks[1][1] != 0; + + return bitEntropy.BitsEntropyRefine() + stats.FinalHuffmanCost(); + } + + private static double ExtraCost(Span population, int length) + { + double cost = 0.0d; + for (int i = 2; i < length - 2; i++) + { + cost += (i >> 1) * population[i + 2]; + } + + return cost; + } + + private static void AddVector(uint[] a, uint[] b, uint[] output, int size) + { + for (int i = 0; i < size; i++) + { + output[i] = a[i] + b[i]; + } + } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LLz77Type.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LLz77Type.cs new file mode 100644 index 000000000..bbc2c3479 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LLz77Type.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + internal enum Vp8LLz77Type + { + Lz77Standard = 1, + + Lz77Rle = 2, + + Lz77Box = 4 + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LMetadata.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LMetadata.cs new file mode 100644 index 000000000..773cf9331 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LMetadata.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Buffers; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + 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/Lossless/Vp8LMultipliers.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LMultipliers.cs new file mode 100644 index 000000000..86454bd71 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LMultipliers.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + internal struct Vp8LMultipliers + { + public byte GreenToRed; + + public byte GreenToBlue; + + public byte RedToBlue; + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LStreaks.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LStreaks.cs new file mode 100644 index 000000000..27ddcfd43 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LStreaks.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + internal class Vp8LStreaks + { + /// + /// Initializes a new instance of the class. + /// + public Vp8LStreaks() + { + this.Counts = new int[2]; + this.Streaks = new int[2][]; + this.Streaks[0] = new int[2]; + this.Streaks[1] = new int[2]; + } + + /// + /// Gets the streak count. + /// index: 0=zero streak, 1=non-zero streak. + /// + public int[] Counts { get; } + + /// + /// Gets the streaks. + /// [zero/non-zero][streak < 3 / streak >= 3]. + /// + public int[][] Streaks { get; } + + public double FinalHuffmanCost() + { + // The constants in this function are experimental and got rounded from + // their original values in 1/8 when switched to 1/1024. + double retval = InitialHuffmanCost(); + + // Second coefficient: Many zeros in the histogram are covered efficiently + // by a run-length encode. Originally 2/8. + retval += (this.Counts[0] * 1.5625) + (0.234375 * this.Streaks[0][1]); + + // Second coefficient: Constant values are encoded less efficiently, but still + // RLE'ed. Originally 6/8. + retval += (this.Counts[1] * 2.578125) + (0.703125 * this.Streaks[1][1]); + + // 0s are usually encoded more efficiently than non-0s. + // Originally 15/8. + retval += 1.796875 * this.Streaks[0][0]; + + // Originally 26/8. + retval += 3.28125 * this.Streaks[1][0]; + + return retval; + } + + private static double InitialHuffmanCost() + { + // Small bias because Huffman code length is typically not stored in full length. + int huffmanCodeOfHuffmanCodeSize = WebpConstants.CodeLengthCodes * 3; + double smallBias = 9.1; + return huffmanCodeOfHuffmanCodeSize - smallBias; + } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LTransform.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LTransform.cs new file mode 100644 index 000000000..247512118 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LTransform.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Buffers; +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + /// + /// Data associated with a VP8L transformation to reduce the entropy. + /// + [DebuggerDisplay("Transformtype: {" + nameof(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/Lossless/Vp8LTransformType.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LTransformType.cs new file mode 100644 index 000000000..bde2e52e9 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LTransformType.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + /// + /// 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. + /// + internal 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 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. + /// + 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/Lossless/WebpLosslessDecoder.cs b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs new file mode 100644 index 000000000..960416009 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs @@ -0,0 +1,1001 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Webp.BitReader; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossless +{ + /// + /// 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 const int BitsSpecialMarker = 0x100; + + private const uint PackedNonLiteralCode = 0; + + private static readonly int CodeToPlaneCodes = WebpLookupTables.CodeToPlane.Length; + + // Memory needed for lookup tables of one Huffman tree group. Red, blue, alpha and distance alphabets are constant (256 for red, blue and alpha, 40 for + // distance) and lookup table sizes for them in worst case are 630 and 410 respectively. Size of green alphabet depends on color cache size and is equal + // to 256 (green component values) + 24 (length prefix values) + color_cache_size (between 0 and 2048). + // All values computed for 8-bit first level lookup with Mark Adler's tool: + // http://www.hdfgroup.org/ftp/lib-external/zlib/zlib-1.2.5/examples/enough.c + private const 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, width, height); + } + } + + 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 isColorCachePresent = this.bitReader.ReadBit(); + int colorCacheBits = 0; + int colorCacheSize = 0; + if (isColorCachePresent) + { + colorCacheBits = (int)this.bitReader.ReadValue(4); + + // Note: According to webpinfo color cache bits of 11 are valid, even though 10 is defined in the source code as maximum. + // That is why 11 bits is also considered valid here. + bool colorCacheBitsIsValid = colorCacheBits is >= 1 and <= WebpConstants.MaxColorCacheBits + 1; + if (!colorCacheBitsIsValid) + { + 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 (isColorCachePresent) + { + decoder.Metadata.ColorCache = new ColorCache(); + colorCacheSize = 1 << colorCacheBits; + decoder.Metadata.ColorCacheSize = colorCacheSize; + 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, int width, int height) + where TPixel : unmanaged, IPixel + { + Span pixelData = decoder.Pixels.GetSpan(); + + // Apply reverse transformations, if any are present. + ApplyInverseTransforms(decoder, pixelData, this.memoryAllocator); + + Span pixelDataAsBytes = MemoryMarshal.Cast(pixelData); + int bytesPerRow = width * 4; + for (int y = 0; y < height; y++) + { + Span rowAsBytes = pixelDataAsBytes.Slice(y * bytesPerRow, bytesPerRow); + Span pixelRow = pixels.GetRowSpan(y); + PixelOperations.Instance.FromBgra32Bytes( + this.configuration, + rowAsBytes.Slice(0, bytesPerRow), + pixelRow.Slice(0, width), + width); + } + } + + public 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; + const 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 = GetHTreeGroupForPos(decoder.Metadata, col, row); + + int totalPixels = width * height; + int decodedPixels = 0; + int lastCached = decodedPixels; + while (decodedPixels < totalPixels) + { + int code; + if ((col & mask) == 0) + { + hTreeGroup = 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; + } + + pixelData[decodedPixels] = (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 = PlaneCodeToDistance(width, distCode); + if (this.bitReader.IsEndOfStream()) + { + break; + } + + CopyBlock(pixelData, decodedPixels, dist, length); + decodedPixels += length; + col += length; + while (col >= width) + { + col -= width; + row++; + } + + if ((col & mask) != 0) + { + hTreeGroup = 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. + int huffmanPrecision = (int)(this.bitReader.ReadValue(3) + 2); + int huffmanXSize = LosslessUtils.SubSampleSize(xSize, huffmanPrecision); + int huffmanYSize = LosslessUtils.SubSampleSize(ySize, huffmanPrecision); + int huffmanPixels = huffmanXSize * huffmanYSize; + + IMemoryOwner huffmanImage = this.DecodeImageStream(decoder, huffmanXSize, huffmanYSize, false); + Span huffmanImageSpan = huffmanImage.GetSpan(); + decoder.Metadata.HuffmanSubSampleBits = huffmanPrecision; + + // TODO: Isn't huffmanPixels the length of the span? + 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; + int[] 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 == 0) + { + WebpThrowHelper.ThrowImageFormatException("Huffman table size is zero"); + } + + // TODO: Avoid allocation. + hTreeGroup.HTrees.Add(huffmanTable.Slice(0, size).ToArray()); + + HuffmanCode huffTableZero = huffmanTable[0]; + if (isTrivialLiteral && LiteralMap[j] == 1) + { + isTrivialLiteral = huffTableZero.BitsUsed == 0; + } + + totalSize += huffTableZero.BitsUsed; + huffmanTable = huffmanTable.Slice(size); + + if (j <= HuffIndex.Alpha) + { + int localMaxBits = codeLengths[0]; + int k; + for (k = 1; k < alphabetSize; ++k) + { + int codeLengthK = codeLengths[k]; + if (codeLengthK > localMaxBits) + { + localMaxBits = codeLengthK; + } + } + + maxBits += localMaxBits; + } + } + + hTreeGroup.IsTrivialLiteral = isTrivialLiteral; + hTreeGroup.IsTrivialCode = false; + if (isTrivialLiteral) + { + uint red = hTreeGroup.HTrees[HuffIndex.Red][0].Value; + uint blue = hTreeGroup.HTrees[HuffIndex.Blue][0].Value; + uint green = hTreeGroup.HTrees[HuffIndex.Green][0].Value; + uint alpha = hTreeGroup.HTrees[HuffIndex.Alpha][0].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 == 0 ? 1 : 8); + codeLengths[symbol] = 1; + + // The second code (if present), is always 8 bit long. + if (numSymbols == 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. + int[] 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, codeLengthCodeLengths, alphabetSize, codeLengths); + } + + int size = HuffmanUtils.BuildHuffmanTable(table, HuffmanUtils.HuffmanTableBits, codeLengths, alphabetSize); + + return size; + } + + private void ReadHuffmanCodeLengths(Span 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 == 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-- == 0) + { + break; + } + + this.bitReader.FillBitWindow(); + ulong prefetchBits = this.bitReader.PrefetchBits(); + int idx = (int)(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) + { + 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. + foreach (Vp8LTransform decoderTransform in decoder.Transforms) + { + if (decoderTransform.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 reverse the transformations, if any are present. + /// + /// The decoder holding the transformation infos. + /// The pixel data to apply the transformation. + /// The memory allocator is needed to allocate memory during the predictor transform. + public static void ApplyInverseTransforms(Vp8LDecoder decoder, Span pixelData, MemoryAllocator memoryAllocator) + { + List transforms = decoder.Transforms; + for (int i = transforms.Count - 1; i >= 0; i--) + { + Vp8LTransform transform = transforms[i]; + Vp8LTransformType transformType = transform.TransformType; + switch (transformType) + { + case Vp8LTransformType.PredictorTransform: + using (IMemoryOwner output = memoryAllocator.Allocate(pixelData.Length, AllocationOptions.Clean)) + { + LosslessUtils.PredictorInverseTransform(transform, pixelData, output.GetSpan()); + } + + break; + case Vp8LTransformType.SubtractGreen: + LosslessUtils.AddGreenToBlueAndRed(pixelData); + break; + case Vp8LTransformType.CrossColorTransform: + LosslessUtils.ColorSpaceInverseTransform(transform, pixelData); + break; + case Vp8LTransformType.ColorIndexingTransform: + LosslessUtils.ColorIndexInverseTransform(transform, pixelData); + break; + } + } + } + + /// + /// The alpha channel of a lossy webp image can be compressed using the lossless webp compression. + /// This method will undo the compression. + /// + /// The alpha decoder. + 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; + const int lenCodeLimit = WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes; + int mask = hdr.HuffmanMask; + HTreeGroup[] htreeGroup = pos < last ? GetHTreeGroupForPos(hdr, col, row) : null; + while (!this.bitReader.Eos && pos < last) + { + // Only update when changing tile. + if ((col & mask) == 0) + { + htreeGroup = 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 % WebpConstants.NumArgbCacheRows == 0) + { + dec.ExtractPalettedAlphaRows(row); + } + } + } + else if (code < lenCodeLimit) + { + // Backward reference + int lengthSym = code - WebpConstants.NumLiteralCodes; + int length = this.GetCopyLength(lengthSym); + int distSymbol = (int)this.ReadSymbol(htreeGroup[0].HTrees[HuffIndex.Dist]); + this.bitReader.FillBitWindow(); + int distCode = this.GetCopyDistance(distSymbol); + int dist = PlaneCodeToDistance(width, distCode); + if (pos >= dist && end - pos >= length) + { + CopyBlock8B(data, pos, dist, length); + } + else + { + WebpThrowHelper.ThrowImageFormatException("error while decoding alpha data"); + } + + pos += length; + col += length; + while (col >= width) + { + col -= width; + ++row; + if (row <= lastRow && row % WebpConstants.NumArgbCacheRows == 0) + { + dec.ExtractPalettedAlphaRows(row); + } + } + + if (pos < last && (col & mask) > 0) + { + htreeGroup = GetHTreeGroupForPos(hdr, col, row); + } + } + else + { + WebpThrowHelper.ThrowImageFormatException("bitstream error while parsing alpha data"); + } + + this.bitReader.Eos = this.bitReader.IsEndOfStream(); + } + + // Process the remaining rows corresponding to last row-block. + dec.ExtractPalettedAlphaRows(row > lastRow ? lastRow : row); + } + + 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 == 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 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 >>= AccumulateHCode(hCode, 8, huff); + bits >>= AccumulateHCode(hTreeGroup.HTrees[HuffIndex.Red][bits], 16, huff); + bits >>= AccumulateHCode(hTreeGroup.HTrees[HuffIndex.Blue][bits], 0, huff); + bits >>= AccumulateHCode(hTreeGroup.HTrees[HuffIndex.Alpha][bits], 24, huff); + } + } + } + + /// + /// Decodes the next Huffman code from the bit-stream. + /// FillBitWindow() 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; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private int GetCopyLength(int lengthSymbol) => + this.GetCopyDistance(lengthSymbol); // Length and distance prefixes are encoded the same way. + + 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); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static HTreeGroup[] GetHTreeGroupForPos(Vp8LMetadata metadata, int x, int y) + { + uint metaIndex = GetMetaIndex(metadata.HuffmanImage, metadata.HuffmanXSize, metadata.HuffmanSubSampleBits, x, y); + return metadata.HTreeGroups.AsSpan((int)metaIndex).ToArray(); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static 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 static 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; + } + + /// + /// Copies pixels when a backward reference is used. + /// Copy 'length' number of pixels (in scan-line order) from the sequence of pixels prior to them by 'dist' pixels. + /// + /// The pixel data. + /// The number of so far decoded pixels. + /// The backward reference distance prior to the current decoded pixel. + /// The number of pixels to copy. + private static void CopyBlock(Span pixelData, int decodedPixels, int dist, int length) + { + int start = decodedPixels - dist; + if (start < 0) + { + WebpThrowHelper.ThrowImageFormatException("webp image data seems to be invalid"); + } + + if (dist >= length) + { + // no overlap. + Span src = pixelData.Slice(start, length); + Span dest = pixelData.Slice(decodedPixels); + src.CopyTo(dest); + } + else + { + // There is overlap between the backward reference distance and the pixels to copy. + Span src = pixelData.Slice(start); + Span dest = pixelData.Slice(decodedPixels); + for (int i = 0; i < length; i++) + { + dest[i] = src[i]; + } + } + } + + /// + /// Copies alpha values when a backward reference is used. + /// Copy 'length' number of alpha values from the sequence of alpha values prior to them by 'dist'. + /// + /// The alpha values. + /// The position of the so far decoded pixels. + /// The backward reference distance prior to the current decoded pixel. + /// The number of pixels to copy. + private static void CopyBlock8B(Span data, int pos, int dist, int length) + { + if (dist >= length) + { + // no overlap. + data.Slice(pos - dist, length).CopyTo(data.Slice(pos)); + } + else + { + Span dst = data.Slice(pos); + Span src = data.Slice(pos - dist); + for (int i = 0; i < length; i++) + { + dst[i] = src[i]; + } + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int AccumulateHCode(HuffmanCode hCode, int shift, HuffmanCode huff) + { + huff.BitsUsed += hCode.BitsUsed; + huff.Value |= hCode.Value << shift; + return hCode.BitsUsed; + } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/Webp_Lossless_Bitstream_Specification.pdf b/src/ImageSharp/Formats/Webp/Lossless/Webp_Lossless_Bitstream_Specification.pdf new file mode 100644 index 000000000..4b5ddd57f Binary files /dev/null and b/src/ImageSharp/Formats/Webp/Lossless/Webp_Lossless_Bitstream_Specification.pdf differ diff --git a/src/ImageSharp/Formats/Webp/Lossy/IntraPredictionMode.cs b/src/ImageSharp/Formats/Webp/Lossy/IntraPredictionMode.cs new file mode 100644 index 000000000..2f3ba3766 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/IntraPredictionMode.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + internal enum IntraPredictionMode + { + /// + /// Predict DC using row above and column to the left. + /// + DcPrediction = 0, + + /// + /// Propagate second differences a la "True Motion". + /// + TrueMotion = 1, + + /// + /// Predict rows using row above. + /// + VPrediction = 2, + + /// + /// Predict columns using column to the left. + /// + HPrediction = 3, + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/LoopFilter.cs b/src/ImageSharp/Formats/Webp/Lossy/LoopFilter.cs new file mode 100644 index 000000000..1a49b1427 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/LoopFilter.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + /// + /// 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/Lossy/LossyUtils.cs b/src/ImageSharp/Formats/Webp/Lossy/LossyUtils.cs new file mode 100644 index 000000000..1584237b0 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/LossyUtils.cs @@ -0,0 +1,1086 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; + +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + internal static class LossyUtils + { + [MethodImpl(InliningOptions.ShortMethod)] + public static int Vp8Sse16X16(Span a, Span b) => GetSse(a, b, 16, 16); + + [MethodImpl(InliningOptions.ShortMethod)] + public static int Vp8Sse16X8(Span a, Span b) => GetSse(a, b, 16, 8); + + [MethodImpl(InliningOptions.ShortMethod)] + public static int Vp8Sse4X4(Span a, Span b) => GetSse(a, b, 4, 4); + + [MethodImpl(InliningOptions.ShortMethod)] + public static int GetSse(Span a, Span b, int w, int h) + { + int count = 0; + int aOffset = 0; + int bOffset = 0; + for (int y = 0; y < h; y++) + { + for (int x = 0; x < w; x++) + { + int diff = a[aOffset + x] - b[bOffset + x]; + count += diff * diff; + } + + aOffset += WebpConstants.Bps; + bOffset += WebpConstants.Bps; + } + + return count; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void Vp8Copy4X4(Span src, Span dst) => Copy(src, dst, 4, 4); + + [MethodImpl(InliningOptions.ShortMethod)] + public static void Vp8Copy16X8(Span src, Span dst) => Copy(src, dst, 16, 8); + + [MethodImpl(InliningOptions.ShortMethod)] + public static void Copy(Span src, Span dst, int w, int h) + { + int offset = 0; + for (int y = 0; y < h; y++) + { + src.Slice(offset, w).CopyTo(dst.Slice(offset, w)); + offset += WebpConstants.Bps; + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static int Vp8Disto16X16(Span a, Span b, Span w) + { + int d = 0; + for (int y = 0; y < 16 * WebpConstants.Bps; y += 4 * WebpConstants.Bps) + { + for (int x = 0; x < 16; x += 4) + { + d += Vp8Disto4X4(a.Slice(x + y), b.Slice(x + y), w); + } + } + + return d; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static int Vp8Disto4X4(Span a, Span b, Span w) + { + int sum1 = TTransform(a, w); + int sum2 = TTransform(b, w); + return Math.Abs(sum2 - sum1) >> 5; + } + + public static void DC16(Span dst, Span yuv, int offset) + { + int offsetMinus1 = offset - 1; + int offsetMinusBps = offset - WebpConstants.Bps; + int dc = 16; + for (int j = 0; j < 16; j++) + { + // DC += dst[-1 + j * BPS] + dst[j - BPS]; + dc += yuv[offsetMinus1 + (j * WebpConstants.Bps)] + yuv[offsetMinusBps + j]; + } + + Put16(dc >> 5, dst); + } + + [MethodImpl(InliningOptions.ShortMethod)] + 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 + offset--; + for (int j = 16; j > 0; j--) + { + // memset(dst, dst[-1], 16); + byte v = yuv[offset]; + 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); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void DC16NoTopLeft(Span dst) => + Put16(0x80, dst); // DC with no top and left samples. + + public static void DC8uv(Span dst, Span yuv, int offset) + { + int dc0 = 8; + int offsetMinus1 = offset - 1; + int offsetMinusBps = offset - WebpConstants.Bps; + for (int i = 0; i < 8; i++) + { + // dc0 += dst[i - BPS] + dst[-1 + i * BPS]; + dc0 += yuv[offsetMinusBps + i] + yuv[offsetMinus1 + (i * WebpConstants.Bps)]; + } + + Put8x8uv((byte)(dc0 >> 4), dst); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void TM8uv(Span dst, Span yuv, int offset) => + TrueMotion(dst, yuv, offset, 8); // TrueMotion + + public static void VE8uv(Span dst, Span yuv, int offset) + { + // vertical + Span src = yuv.Slice(offset - WebpConstants.Bps, 8); + + int endIdx = 8 * WebpConstants.Bps; + for (int j = 0; j < endIdx; j += WebpConstants.Bps) + { + // memcpy(dst + j * BPS, dst - BPS, 8); + src.CopyTo(dst.Slice(j)); + } + } + + public static void HE8uv(Span dst, Span yuv, int offset) + { + // horizontal + offset--; + for (int j = 0; j < 8; j++) + { + // memset(dst, dst[-1], 8); + // dst += BPS; + byte v = yuv[offset]; + 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; + int offsetMinusOne = offset - 1; + int endIdx = 8 * WebpConstants.Bps; + for (int i = 0; i < endIdx; i += WebpConstants.Bps) + { + // dc0 += dst[-1 + i * BPS]; + dc0 += yuv[offsetMinusOne + i]; + } + + Put8x8uv((byte)(dc0 >> 3), dst); + } + + public static void DC8uvNoLeft(Span dst, Span yuv, int offset) + { + // DC with no left samples. + int offsetMinusBps = offset - WebpConstants.Bps; + int dc0 = 4; + for (int i = 0; i < 8; i++) + { + // dc0 += dst[i - BPS]; + dc0 += yuv[offsetMinusBps + i]; + } + + Put8x8uv((byte)(dc0 >> 3), dst); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void DC8uvNoTopLeft(Span dst) => + Put8x8uv(0x80, dst); // DC with nothing. + + public static void DC4(Span dst, Span yuv, int offset) + { + int dc = 4; + int offsetMinusBps = offset - WebpConstants.Bps; + int offsetMinusOne = offset - 1; + for (int i = 0; i < 4; i++) + { + dc += yuv[offsetMinusBps + i] + yuv[offsetMinusOne + (i * WebpConstants.Bps)]; + } + + dc >>= 3; + int endIndx = 4 * WebpConstants.Bps; + for (int i = 0; i < endIndx; i += WebpConstants.Bps) + { + Memset(dst, (byte)dc, i, 4); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + 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]) + }; + + int endIdx = 4 * WebpConstants.Bps; + for (int i = 0; i < endIdx; i += WebpConstants.Bps) + { + vals.CopyTo(dst.Slice(i)); + } + } + + public static void HE4(Span dst, Span yuv, int offset) + { + // horizontal + int offsetMinusOne = offset - 1; + byte a = yuv[offsetMinusOne - WebpConstants.Bps]; + byte b = yuv[offsetMinusOne]; + byte c = yuv[offsetMinusOne + WebpConstants.Bps]; + byte d = yuv[offsetMinusOne + (2 * WebpConstants.Bps)]; + byte e = yuv[offsetMinusOne + (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 + int offsetMinusOne = offset - 1; + byte i = yuv[offsetMinusOne]; + byte j = yuv[offsetMinusOne + (1 * WebpConstants.Bps)]; + byte k = yuv[offsetMinusOne + (2 * WebpConstants.Bps)]; + byte l = yuv[offsetMinusOne + (3 * WebpConstants.Bps)]; + byte x = yuv[offsetMinusOne - 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 + int offsetMinusOne = offset - 1; + byte i = yuv[offsetMinusOne]; + byte j = yuv[offsetMinusOne + (1 * WebpConstants.Bps)]; + byte k = yuv[offsetMinusOne + (2 * WebpConstants.Bps)]; + byte x = yuv[offsetMinusOne - 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); + } + + /// + /// Paragraph 14.3: Implementation of the Walsh-Hadamard transform inversion. + /// + public static void TransformWht(Span input, Span output) + { + int[] tmp = new int[16]; + for (int i = 0; i < 4; i++) + { + int iPlus4 = 4 + i; + int iPlus8 = 8 + i; + int iPlus12 = 12 + i; + int a0 = input[i] + input[iPlus12]; + int a1 = input[iPlus4] + input[iPlus8]; + int a2 = input[iPlus4] - input[iPlus8]; + int a3 = input[i] - input[iPlus12]; + tmp[i] = a0 + a1; + tmp[iPlus8] = a0 - a1; + tmp[iPlus4] = a3 + a2; + tmp[iPlus12] = a3 - a2; + } + + int outputOffset = 0; + for (int i = 0; i < 4; i++) + { + int imul4 = i * 4; + int dc = tmp[0 + imul4] + 3; + int a0 = dc + tmp[3 + imul4]; + int a1 = tmp[1 + imul4] + tmp[2 + imul4]; + int a2 = tmp[1 + imul4] - tmp[2 + imul4]; + int a3 = dc - tmp[3 + imul4]; + 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; + } + } + + /// + /// Hadamard transform + /// Returns the weighted sum of the absolute value of transformed coefficients. + /// w[] contains a row-major 4 by 4 symmetric matrix. + /// + public static int TTransform(Span input, Span w) + { + int sum = 0; + int[] tmp = new int[16]; + + // horizontal pass. + int inputOffset = 0; + for (int i = 0; i < 4; i++) + { + int inputOffsetPlusOne = inputOffset + 1; + int inputOffsetPlusTwo = inputOffset + 2; + int inputOffsetPlusThree = inputOffset + 3; + int a0 = input[inputOffset] + input[inputOffsetPlusTwo]; + int a1 = input[inputOffsetPlusOne] + input[inputOffsetPlusThree]; + int a2 = input[inputOffsetPlusOne] - input[inputOffsetPlusThree]; + int a3 = input[inputOffset] - input[inputOffsetPlusTwo]; + tmp[0 + (i * 4)] = a0 + a1; + tmp[1 + (i * 4)] = a3 + a2; + tmp[2 + (i * 4)] = a3 - a2; + tmp[3 + (i * 4)] = a0 - a1; + + inputOffset += WebpConstants.Bps; + } + + // vertical pass + for (int i = 0; i < 4; i++) + { + int a0 = tmp[0 + i] + tmp[8 + i]; + int a1 = tmp[4 + i] + tmp[12 + i]; + int a2 = tmp[4 + i] - tmp[12 + i]; + int a3 = tmp[0 + i] - tmp[8 + i]; + int b0 = a0 + a1; + int b1 = a3 + a2; + int b2 = a3 - a2; + int b3 = a0 - a1; + + sum += w[0] * Math.Abs(b0); + sum += w[4] * Math.Abs(b1); + sum += w[8] * Math.Abs(b2); + sum += w[12] * Math.Abs(b3); + + w = w.Slice(1); + } + + return sum; + } + + public static void TransformTwo(Span src, Span dst) + { + TransformOne(src, dst); + TransformOne(src.Slice(16), dst.Slice(4)); + } + + public static void TransformOne(Span src, Span dst) + { + Span tmp = stackalloc int[4 * 4]; + int tmpOffset = 0; + for (int srcOffset = 0; srcOffset < 4; srcOffset++) + { + // vertical pass + int srcOffsetPlus4 = srcOffset + 4; + int srcOffsetPlus8 = srcOffset + 8; + int srcOffsetPlus12 = srcOffset + 12; + int a = src[srcOffset] + src[srcOffsetPlus8]; + int b = src[srcOffset] - src[srcOffsetPlus8]; + int c = Mul2(src[srcOffsetPlus4]) - Mul1(src[srcOffsetPlus12]); + int d = Mul1(src[srcOffsetPlus4]) + Mul2(src[srcOffsetPlus12]); + tmp[tmpOffset++] = a + d; + tmp[tmpOffset++] = b + c; + tmp[tmpOffset++] = b - c; + tmp[tmpOffset++] = a - d; + } + + // 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; + int dstOffset = 0; + for (int i = 0; i < 4; i++) + { + // horizontal pass + int tmpOffsetPlus4 = tmpOffset + 4; + int tmpOffsetPlus8 = tmpOffset + 8; + int tmpOffsetPlus12 = tmpOffset + 12; + int dc = tmp[tmpOffset] + 4; + int a = dc + tmp[tmpOffsetPlus8]; + int b = dc - tmp[tmpOffsetPlus8]; + int c = Mul2(tmp[tmpOffsetPlus4]) - Mul1(tmp[tmpOffsetPlus12]); + int d = Mul1(tmp[tmpOffsetPlus4]) + Mul2(tmp[tmpOffsetPlus12]); + Store(dst.Slice(dstOffset), 0, 0, a + d); + Store(dst.Slice(dstOffset), 1, 0, b + c); + Store(dst.Slice(dstOffset), 2, 0, b - c); + Store(dst.Slice(dstOffset), 3, 0, a - d); + tmpOffset++; + + dstOffset += 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) + { + TransformTwo(src.Slice(0 * 16), dst); + TransformTwo(src.Slice(2 * 16), dst.Slice(4 * WebpConstants.Bps)); + } + + 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)); + } + } + + // Simple In-loop filtering (Paragraph 15.2) + public static void SimpleVFilter16(Span p, int offset, int stride, int thresh) + { + int thresh2 = (2 * thresh) + 1; + int end = 16 + offset; + for (int i = offset; i < end; i++) + { + if (NeedsFilter(p, i, stride, thresh2)) + { + DoFilter2(p, i, stride); + } + } + } + + public static void SimpleHFilter16(Span p, int offset, int stride, int thresh) + { + int thresh2 = (2 * thresh) + 1; + int end = offset + (16 * stride); + for (int i = offset; i < end; i += stride) + { + if (NeedsFilter(p, i, 1, thresh2)) + { + DoFilter2(p, i, 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); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + 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); + + [MethodImpl(InliningOptions.ShortMethod)] + 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. + [MethodImpl(InliningOptions.ShortMethod)] + 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); + } + + [MethodImpl(InliningOptions.ShortMethod)] + 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); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void VFilter8i(Span u, Span v, int offset, int stride, int thresh, int ithresh, int hevThresh) + { + int offset4mulstride = offset + (4 * stride); + FilterLoop24(u, offset4mulstride, stride, 1, 8, thresh, ithresh, hevThresh); + FilterLoop24(v, offset4mulstride, stride, 1, 8, thresh, ithresh, hevThresh); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void HFilter8i(Span u, Span v, int offset, int stride, int thresh, int ithresh, int hevThresh) + { + int offsetPlus4 = offset + 4; + FilterLoop24(u, offsetPlus4, 1, stride, 8, thresh, ithresh, hevThresh); + FilterLoop24(v, offsetPlus4, 1, stride, 8, thresh, ithresh, hevThresh); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static uint LoadUv(byte u, byte v) => + (uint)(u | (v << 16)); // We process u and v together stashed into 32bit(16bit each). + + [MethodImpl(InliningOptions.ShortMethod)] + 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); + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static int YuvToB(int y, int u) => Clip8(MultHi(y, 19077) + MultHi(u, 33050) - 17685); + + [MethodImpl(InliningOptions.ShortMethod)] + public static int YuvToG(int y, int u, int v) => Clip8(MultHi(y, 19077) - MultHi(u, 6419) - MultHi(v, 13320) + 8708); + + [MethodImpl(InliningOptions.ShortMethod)] + public static int YuvToR(int y, int v) => Clip8(MultHi(y, 19077) + MultHi(v, 26149) - 14234); + + [MethodImpl(InliningOptions.ShortMethod)] + public static byte Avg2(byte a, byte b) => (byte)((a + b + 1) >> 1); + + [MethodImpl(InliningOptions.ShortMethod)] + public static byte Avg3(byte a, byte b, byte c) => (byte)((a + (2 * b) + c + 2) >> 2); + + [MethodImpl(InliningOptions.ShortMethod)] + public static void Dst(Span dst, int x, int y, byte v) => dst[x + (y * WebpConstants.Bps)] = v; + + [MethodImpl(InliningOptions.ShortMethod)] + public static byte Clip8B(int v) => (byte)((v & ~0xff) == 0 ? v : v < 0 ? 0 : 255); + + // Cost of coding one event with probability 'proba'. + public static int Vp8BitCost(int bit, byte proba) => bit == 0 ? WebpLookupTables.Vp8EntropyCost[proba] : WebpLookupTables.Vp8EntropyCost[255 - proba]; + + [MethodImpl(InliningOptions.ShortMethod)] + 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); + } + } + + 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); + } + } + + // 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 offsetMinus2Step = offset - (2 * step); + int p1 = p[offsetMinus2Step]; + 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[offsetMinus2Step] = 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 step2 = 2 * step; + int step3 = 3 * step; + int offsetMinusStep = offset - step; + int p2 = p[offset - step3]; + int p1 = p[offset - step2]; + int p0 = p[offsetMinusStep]; + int q0 = p[offset]; + int q1 = p[offset + step]; + int q2 = p[offset + step2]; + 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 - step3] = WebpLookupTables.Clip1[p2 + a3]; + p[offset - step2] = WebpLookupTables.Clip1[p1 + a2]; + p[offsetMinusStep] = WebpLookupTables.Clip1[p0 + a1]; + p[offset] = WebpLookupTables.Clip1[q0 - a1]; + p[offset + step] = WebpLookupTables.Clip1[q1 - a2]; + p[offset + step2] = WebpLookupTables.Clip1[q2 - a3]; + } + + [MethodImpl(InliningOptions.ShortMethod)] + 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 step2 = 2 * step; + int step3 = 3 * step; + int p3 = p[offset - (4 * step)]; + int p2 = p[offset - step3]; + int p1 = p[offset - step2]; + int p0 = p[offset - step]; + int q0 = p[offset]; + int q1 = p[offset + step]; + int q2 = p[offset + step2]; + int q3 = p[offset + step3]; + 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; + } + + [MethodImpl(InliningOptions.ShortMethod)] + 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; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int MultHi(int v, int coeff) => (v * coeff) >> 8; + + [MethodImpl(InliningOptions.ShortMethod)] + private static void Store(Span dst, int x, int y, int v) + { + int index = x + (y * WebpConstants.Bps); + dst[index] = Clip8B(dst[index] + (v >> 3)); + } + + [MethodImpl(InliningOptions.ShortMethod)] + 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); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int Mul1(int a) => ((a * 20091) >> 16) + a; + + [MethodImpl(InliningOptions.ShortMethod)] + private static int Mul2(int a) => (a * 35468) >> 16; + + [MethodImpl(InliningOptions.ShortMethod)] + private static byte Clip8(int v) + { + int yuvMask = (256 << 6) - 1; + return (byte)((v & ~yuvMask) == 0 ? v >> 6 : v < 0 ? 0 : 255); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void Put8x8uv(byte value, Span dst) + { + int end = 8 * WebpConstants.Bps; + for (int j = 0; j < end; j += WebpConstants.Bps) + { + // memset(dst + j * BPS, value, 8); + Memset(dst, value, j, 8); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void Memset(Span dst, byte value, int startIdx, int count) + { + int end = startIdx + count; + for (int i = startIdx; i < end; i++) + { + dst[i] = value; + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int Clamp255(int x) => x < 0 ? 0 : x > 255 ? 255 : x; + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/PassStats.cs b/src/ImageSharp/Formats/Webp/Lossy/PassStats.cs new file mode 100644 index 000000000..64a122afb --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/PassStats.cs @@ -0,0 +1,76 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + /// + /// Class for organizing convergence in either size or PSNR. + /// + internal class PassStats + { + public PassStats(long targetSize, float targetPsnr, int qMin, int qMax, int quality) + { + bool doSizeSearch = targetSize != 0; + + this.IsFirst = true; + this.Dq = 10.0f; + this.Qmin = qMin; + this.Qmax = qMax; + this.Q = Numerics.Clamp(quality, qMin, qMax); + this.LastQ = this.Q; + this.Target = doSizeSearch ? targetSize + : targetPsnr > 0.0f ? targetPsnr + : 40.0f; // default, just in case + this.Value = 0.0f; + this.LastValue = 0.0f; + this.DoSizeSearch = doSizeSearch; + } + + public bool IsFirst { get; set; } + + public float Dq { get; set; } + + public float Q { get; set; } + + public float LastQ { get; set; } + + public float Qmin { get; } + + public float Qmax { get; } + + public double Value { get; set; } // PSNR or size + + public double LastValue { get; set; } + + public double Target { get; } + + public bool DoSizeSearch { get; } + + public float ComputeNextQ() + { + float dq; + if (this.IsFirst) + { + dq = this.Value > this.Target ? -this.Dq : this.Dq; + this.IsFirst = false; + } + else if (this.Value != this.LastValue) + { + double slope = (this.Target - this.Value) / (this.LastValue - this.Value); + dq = (float)(slope * (this.LastQ - this.Q)); + } + else + { + dq = 0.0f; // we're done?! + } + + // Limit variable to avoid large swings. + this.Dq = Numerics.Clamp(dq, -30.0f, 30.0f); + this.LastQ = this.Q; + this.LastValue = this.Value; + this.Q = Numerics.Clamp(this.Q + this.Dq, this.Qmin, this.Qmax); + + return this.Q; + } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs b/src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs new file mode 100644 index 000000000..2ed438166 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs @@ -0,0 +1,637 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + /// + /// Quantization methods. + /// + internal static class QuantEnc + { + private static readonly byte[] Zigzag = { 0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, 10, 7, 11, 14, 15 }; + + private static readonly ushort[] WeightY = { 38, 32, 20, 9, 32, 28, 17, 7, 20, 17, 10, 4, 9, 7, 4, 2 }; + + private const int MaxLevel = 2047; + + // Diffusion weights. We under-correct a bit (15/16th of the error is actually + // diffused) to avoid 'rainbow' chessboard pattern of blocks at q~=0. + private const int C1 = 7; // fraction of error sent to the 4x4 block below + private const int C2 = 8; // fraction of error sent to the 4x4 block on the right + private const int DSHIFT = 4; + private const int DSCALE = 1; // storage descaling, needed to make the error fit byte + + public static void PickBestIntra16(Vp8EncIterator it, ref Vp8ModeScore rd, Vp8SegmentInfo[] segmentInfos, Vp8EncProba proba) + { + const int numBlocks = 16; + Vp8SegmentInfo dqm = segmentInfos[it.CurrentMacroBlockInfo.Segment]; + int lambda = dqm.LambdaI16; + int tlambda = dqm.TLambda; + Span src = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc); + var rdTmp = new Vp8ModeScore(); + Vp8ModeScore rdCur = rdTmp; + Vp8ModeScore rdBest = rd; + int mode; + bool isFlat = IsFlatSource16(src); + rd.ModeI16 = -1; + for (mode = 0; mode < WebpConstants.NumPredModes; ++mode) + { + // scratch buffer. + Span tmpDst = it.YuvOut2.AsSpan(Vp8EncIterator.YOffEnc); + rdCur.ModeI16 = mode; + + // Reconstruct. + rdCur.Nz = (uint)ReconstructIntra16(it, dqm, rdCur, tmpDst, mode); + + // Measure RD-score. + rdCur.D = LossyUtils.Vp8Sse16X16(src, tmpDst); + rdCur.SD = tlambda != 0 ? Mult8B(tlambda, LossyUtils.Vp8Disto16X16(src, tmpDst, WeightY)) : 0; + rdCur.H = WebpConstants.Vp8FixedCostsI16[mode]; + rdCur.R = it.GetCostLuma16(rdCur, proba); + + if (isFlat) + { + // Refine the first impression (which was in pixel space). + isFlat = IsFlat(rdCur.YAcLevels, numBlocks, WebpConstants.FlatnessLimitI16); + if (isFlat) + { + // Block is very flat. We put emphasis on the distortion being very low! + rdCur.D *= 2; + rdCur.SD *= 2; + } + } + + // Since we always examine Intra16 first, we can overwrite *rd directly. + rdCur.SetRdScore(lambda); + + if (mode == 0 || rdCur.Score < rdBest.Score) + { + Vp8ModeScore tmp = rdCur; + rdCur = rdBest; + rdBest = tmp; + it.SwapOut(); + } + } + + if (rdBest != rd) + { + rd = rdBest; + } + + // Finalize score for mode decision. + rd.SetRdScore(dqm.LambdaMode); + it.SetIntra16Mode(rd.ModeI16); + + // We have a blocky macroblock (only DCs are non-zero) with fairly high + // distortion, record max delta so we can later adjust the minimal filtering + // strength needed to smooth these blocks out. + if ((rd.Nz & 0x100ffff) == 0x1000000 && rd.D > dqm.MinDisto) + { + dqm.StoreMaxDelta(rd.YDcLevels); + } + } + + public static bool PickBestIntra4(Vp8EncIterator it, ref Vp8ModeScore rd, Vp8SegmentInfo[] segmentInfos, Vp8EncProba proba, int maxI4HeaderBits) + { + Vp8SegmentInfo dqm = segmentInfos[it.CurrentMacroBlockInfo.Segment]; + int lambda = dqm.LambdaI4; + int tlambda = dqm.TLambda; + Span src0 = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc); + Span bestBlocks = it.YuvOut2.AsSpan(Vp8EncIterator.YOffEnc); + int totalHeaderBits = 0; + var rdBest = new Vp8ModeScore(); + + if (maxI4HeaderBits == 0) + { + return false; + } + + rdBest.InitScore(); + rdBest.H = 211; // '211' is the value of VP8BitCost(0, 145) + rdBest.SetRdScore(dqm.LambdaMode); + it.StartI4(); + do + { + int numBlocks = 1; + var rdi4 = new Vp8ModeScore(); + int mode; + int bestMode = -1; + Span src = src0.Slice(WebpLookupTables.Vp8Scan[it.I4]); + short[] modeCosts = it.GetCostModeI4(rd.ModesI4); + Span bestBlock = bestBlocks.Slice(WebpLookupTables.Vp8Scan[it.I4]); + Span tmpDst = it.Scratch.AsSpan(); + tmpDst.Fill(0); + + rdi4.InitScore(); + it.MakeIntra4Preds(); + for (mode = 0; mode < WebpConstants.NumBModes; ++mode) + { + var rdTmp = new Vp8ModeScore(); + short[] tmpLevels = new short[16]; + + // Reconstruct. + rdTmp.Nz = (uint)ReconstructIntra4(it, dqm, tmpLevels, src, tmpDst, mode); + + // Compute RD-score. + rdTmp.D = LossyUtils.Vp8Sse4X4(src, tmpDst); + rdTmp.SD = tlambda != 0 ? Mult8B(tlambda, LossyUtils.Vp8Disto4X4(src, tmpDst, WeightY)) : 0; + rdTmp.H = modeCosts[mode]; + + // Add flatness penalty, to avoid flat area to be mispredicted by a complex mode. + if (mode > 0 && IsFlat(tmpLevels, numBlocks, WebpConstants.FlatnessLimitI4)) + { + rdTmp.R = WebpConstants.FlatnessPenality * numBlocks; + } + else + { + rdTmp.R = 0; + } + + // early-out check. + rdTmp.SetRdScore(lambda); + if (bestMode >= 0 && rdTmp.Score >= rdi4.Score) + { + continue; + } + + // finish computing score. + rdTmp.R += it.GetCostLuma4(tmpLevels, proba); + rdTmp.SetRdScore(lambda); + + if (bestMode < 0 || rdTmp.Score < rdi4.Score) + { + rdi4.CopyScore(rdTmp); + bestMode = mode; + Span tmp = tmpDst; + tmpDst = bestBlock; + bestBlock = tmp; + tmpLevels.CopyTo(rdBest.YAcLevels.AsSpan(it.I4 * 16, 16)); + } + } + + rdi4.SetRdScore(dqm.LambdaMode); + rdBest.AddScore(rdi4); + if (rdBest.Score >= rd.Score) + { + return false; + } + + totalHeaderBits += (int)rdi4.H; // <- equal to modeCosts[bestMode]; + if (totalHeaderBits > maxI4HeaderBits) + { + return false; + } + + // Copy selected samples to the right place. + LossyUtils.Vp8Copy4X4(bestBlock, bestBlocks.Slice(WebpLookupTables.Vp8Scan[it.I4])); + + rd.ModesI4[it.I4] = (byte)bestMode; + it.TopNz[it.I4 & 3] = it.LeftNz[it.I4 >> 2] = rdi4.Nz != 0 ? 1 : 0; + } + while (it.RotateI4(bestBlocks)); + + // Finalize state. + rd.CopyScore(rdBest); + it.SetIntra4Mode(rd.ModesI4); + it.SwapOut(); + rdBest.YAcLevels.AsSpan().CopyTo(rd.YAcLevels); + + // Select intra4x4 over intra16x16. + return true; + } + + public static void PickBestUv(Vp8EncIterator it, ref Vp8ModeScore rd, Vp8SegmentInfo[] segmentInfos, Vp8EncProba proba) + { + const int numBlocks = 8; + Vp8SegmentInfo dqm = segmentInfos[it.CurrentMacroBlockInfo.Segment]; + int lambda = dqm.LambdaUv; + Span src = it.YuvIn.AsSpan(Vp8EncIterator.UOffEnc); + Span tmpDst = it.YuvOut2.AsSpan(Vp8EncIterator.UOffEnc); + Span dst0 = it.YuvOut.AsSpan(Vp8EncIterator.UOffEnc); + Span dst = dst0; + var rdBest = new Vp8ModeScore(); + int mode; + + rd.ModeUv = -1; + rdBest.InitScore(); + for (mode = 0; mode < WebpConstants.NumPredModes; ++mode) + { + var rdUv = new Vp8ModeScore(); + + // Reconstruct + rdUv.Nz = (uint)ReconstructUv(it, dqm, rdUv, tmpDst, mode); + + // Compute RD-score + rdUv.D = LossyUtils.Vp8Sse16X8(src, tmpDst); + rdUv.SD = 0; // not calling TDisto here: it tends to flatten areas. + rdUv.H = WebpConstants.Vp8FixedCostsUv[mode]; + rdUv.R = it.GetCostUv(rdUv, proba); + if (mode > 0 && IsFlat(rdUv.UvLevels, numBlocks, WebpConstants.FlatnessLimitIUv)) + { + rdUv.R += WebpConstants.FlatnessPenality * numBlocks; + } + + rdUv.SetRdScore(lambda); + if (mode == 0 || rdUv.Score < rdBest.Score) + { + rdBest.CopyScore(rdUv); + rd.ModeUv = mode; + rdUv.UvLevels.CopyTo(rd.UvLevels.AsSpan()); + for (int i = 0; i < 2; i++) + { + rd.Derr[i, 0] = rdUv.Derr[i, 0]; + rd.Derr[i, 1] = rdUv.Derr[i, 1]; + rd.Derr[i, 2] = rdUv.Derr[i, 2]; + } + + Span tmp = dst; + dst = tmpDst; + tmpDst = tmp; + } + } + + it.SetIntraUvMode(rd.ModeUv); + rd.AddScore(rdBest); + if (dst != dst0) + { + // copy 16x8 block if needed. + LossyUtils.Vp8Copy16X8(dst, dst0); + } + + // Store diffusion errors for next block. + it.StoreDiffusionErrors(rd); + } + + public static int ReconstructIntra16(Vp8EncIterator it, Vp8SegmentInfo dqm, Vp8ModeScore rd, Span yuvOut, int mode) + { + Span reference = it.YuvP.AsSpan(Vp8Encoding.Vp8I16ModeOffsets[mode]); + Span src = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc); + int nz = 0; + int n; + short[] dcTmp = new short[16]; + short[] tmp = new short[16 * 16]; + Span tmpSpan = tmp.AsSpan(); + + for (n = 0; n < 16; n += 2) + { + Vp8Encoding.FTransform2(src.Slice(WebpLookupTables.Vp8Scan[n]), reference.Slice(WebpLookupTables.Vp8Scan[n]), tmpSpan.Slice(n * 16, 16), tmpSpan.Slice((n + 1) * 16, 16)); + } + + Vp8Encoding.FTransformWht(tmp, dcTmp); + nz |= QuantizeBlock(dcTmp, rd.YDcLevels, dqm.Y2) << 24; + + for (n = 0; n < 16; n += 2) + { + // Zero-out the first coeff, so that: a) nz is correct below, and + // b) finding 'last' non-zero coeffs in SetResidualCoeffs() is simplified. + tmp[n * 16] = tmp[(n + 1) * 16] = 0; + nz |= Quantize2Blocks(tmpSpan.Slice(n * 16, 32), rd.YAcLevels.AsSpan(n * 16, 32), dqm.Y1) << n; + } + + // Transform back. + LossyUtils.TransformWht(dcTmp, tmpSpan); + for (n = 0; n < 16; n += 2) + { + Vp8Encoding.ITransform(reference.Slice(WebpLookupTables.Vp8Scan[n]), tmpSpan.Slice(n * 16, 32), yuvOut.Slice(WebpLookupTables.Vp8Scan[n]), true); + } + + return nz; + } + + public static int ReconstructIntra4(Vp8EncIterator it, Vp8SegmentInfo dqm, Span levels, Span src, Span yuvOut, int mode) + { + Span reference = it.YuvP.AsSpan(Vp8Encoding.Vp8I4ModeOffsets[mode]); + short[] tmp = new short[16]; + Vp8Encoding.FTransform(src, reference, tmp); + int nz = QuantizeBlock(tmp, levels, dqm.Y1); + Vp8Encoding.ITransform(reference, tmp, yuvOut, false); + + return nz; + } + + public static int ReconstructUv(Vp8EncIterator it, Vp8SegmentInfo dqm, Vp8ModeScore rd, Span yuvOut, int mode) + { + Span reference = it.YuvP.AsSpan(Vp8Encoding.Vp8UvModeOffsets[mode]); + Span src = it.YuvIn.AsSpan(Vp8EncIterator.UOffEnc); + int nz = 0; + int n; + short[] tmp = new short[8 * 16]; + + for (n = 0; n < 8; n += 2) + { + Vp8Encoding.FTransform2( + src.Slice(WebpLookupTables.Vp8ScanUv[n]), + reference.Slice(WebpLookupTables.Vp8ScanUv[n]), + tmp.AsSpan(n * 16, 16), + tmp.AsSpan((n + 1) * 16, 16)); + } + + CorrectDcValues(it, dqm.Uv, tmp, rd); + + for (n = 0; n < 8; n += 2) + { + nz |= Quantize2Blocks(tmp.AsSpan(n * 16, 32), rd.UvLevels.AsSpan(n * 16, 32), dqm.Uv) << n; + } + + for (n = 0; n < 8; n += 2) + { + Vp8Encoding.ITransform(reference.Slice(WebpLookupTables.Vp8ScanUv[n]), tmp.AsSpan(n * 16, 32), yuvOut.Slice(WebpLookupTables.Vp8ScanUv[n]), true); + } + + return nz << 16; + } + + // Refine intra16/intra4 sub-modes based on distortion only (not rate). + public static void RefineUsingDistortion(Vp8EncIterator it, Vp8SegmentInfo[] segmentInfos, Vp8ModeScore rd, bool tryBothModes, bool refineUvMode, int mbHeaderLimit) + { + long bestScore = Vp8ModeScore.MaxCost; + int nz = 0; + int mode; + bool isI16 = tryBothModes || it.CurrentMacroBlockInfo.MacroBlockType == Vp8MacroBlockType.I16X16; + Vp8SegmentInfo dqm = segmentInfos[it.CurrentMacroBlockInfo.Segment]; + + // Some empiric constants, of approximate order of magnitude. + const int lambdaDi16 = 106; + const int lambdaDi4 = 11; + const int lambdaDuv = 120; + long scoreI4 = dqm.I4Penalty; + long i4BitSum = 0; + long bitLimit = tryBothModes + ? mbHeaderLimit + : Vp8ModeScore.MaxCost; // no early-out allowed. + + if (isI16) + { + int bestMode = -1; + Span src = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc); + for (mode = 0; mode < WebpConstants.NumPredModes; ++mode) + { + Span reference = it.YuvP.AsSpan(Vp8Encoding.Vp8I16ModeOffsets[mode]); + long score = (LossyUtils.Vp8Sse16X16(src, reference) * WebpConstants.RdDistoMult) + (WebpConstants.Vp8FixedCostsI16[mode] * lambdaDi16); + + if (mode > 0 && WebpConstants.Vp8FixedCostsI16[mode] > bitLimit) + { + continue; + } + + if (score < bestScore) + { + bestMode = mode; + bestScore = score; + } + } + + if (it.X == 0 || it.Y == 0) + { + // Avoid starting a checkerboard resonance from the border. See bug #432 of libwebp. + if (IsFlatSource16(src)) + { + bestMode = it.X == 0 ? 0 : 2; + tryBothModes = false; // Stick to i16. + } + } + + it.SetIntra16Mode(bestMode); + + // We'll reconstruct later, if i16 mode actually gets selected. + } + + // Next, evaluate Intra4. + if (tryBothModes || !isI16) + { + // We don't evaluate the rate here, but just account for it through a + // constant penalty (i4 mode usually needs more bits compared to i16). + isI16 = false; + it.StartI4(); + do + { + int bestI4Mode = -1; + long bestI4Score = Vp8ModeScore.MaxCost; + Span src = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc + WebpLookupTables.Vp8Scan[it.I4]); + short[] modeCosts = it.GetCostModeI4(rd.ModesI4); + + it.MakeIntra4Preds(); + for (mode = 0; mode < WebpConstants.NumBModes; ++mode) + { + Span reference = it.YuvP.AsSpan(Vp8Encoding.Vp8I4ModeOffsets[mode]); + long score = (LossyUtils.Vp8Sse4X4(src, reference) * WebpConstants.RdDistoMult) + (modeCosts[mode] * lambdaDi4); + if (score < bestI4Score) + { + bestI4Mode = mode; + bestI4Score = score; + } + } + + i4BitSum += modeCosts[bestI4Mode]; + rd.ModesI4[it.I4] = (byte)bestI4Mode; + scoreI4 += bestI4Score; + if (scoreI4 >= bestScore || i4BitSum > bitLimit) + { + // Intra4 won't be better than Intra16. Bail out and pick Intra16. + isI16 = true; + break; + } + else + { + // Reconstruct partial block inside YuvOut2 buffer + Span tmpDst = it.YuvOut2.AsSpan(Vp8EncIterator.YOffEnc + WebpLookupTables.Vp8Scan[it.I4]); + nz |= ReconstructIntra4(it, dqm, rd.YAcLevels.AsSpan(it.I4 * 16, 16), src, tmpDst, bestI4Mode) << it.I4; + } + } + while (it.RotateI4(it.YuvOut2.AsSpan(Vp8EncIterator.YOffEnc))); + } + + // Final reconstruction, depending on which mode is selected. + if (!isI16) + { + it.SetIntra4Mode(rd.ModesI4); + it.SwapOut(); + bestScore = scoreI4; + } + else + { + int intra16Mode = it.Preds[it.PredIdx]; + nz = ReconstructIntra16(it, dqm, rd, it.YuvOut.AsSpan(Vp8EncIterator.YOffEnc), intra16Mode); + } + + // ... and UV! + if (refineUvMode) + { + int bestMode = -1; + long bestUvScore = Vp8ModeScore.MaxCost; + Span src = it.YuvIn.AsSpan(Vp8EncIterator.UOffEnc); + for (mode = 0; mode < WebpConstants.NumPredModes; ++mode) + { + Span reference = it.YuvP.AsSpan(Vp8Encoding.Vp8UvModeOffsets[mode]); + long score = (LossyUtils.Vp8Sse16X8(src, reference) * WebpConstants.RdDistoMult) + (WebpConstants.Vp8FixedCostsUv[mode] * lambdaDuv); + if (score < bestUvScore) + { + bestMode = mode; + bestUvScore = score; + } + } + + it.SetIntraUvMode(bestMode); + } + + nz |= ReconstructUv(it, dqm, rd, it.YuvOut.AsSpan(Vp8EncIterator.UOffEnc), it.CurrentMacroBlockInfo.UvMode); + + rd.Nz = (uint)nz; + rd.Score = bestScore; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static int Quantize2Blocks(Span input, Span output, Vp8Matrix mtx) + { + int nz = QuantizeBlock(input, output, mtx) << 0; + nz |= QuantizeBlock(input.Slice(1 * 16), output.Slice(1 * 16), mtx) << 1; + return nz; + } + + public static int QuantizeBlock(Span input, Span output, Vp8Matrix mtx) + { + int last = -1; + int n; + for (n = 0; n < 16; ++n) + { + int j = Zigzag[n]; + bool sign = input[j] < 0; + uint coeff = (uint)((sign ? -input[j] : input[j]) + mtx.Sharpen[j]); + if (coeff > mtx.ZThresh[j]) + { + uint q = mtx.Q[j]; + uint iQ = mtx.IQ[j]; + uint b = mtx.Bias[j]; + int level = QuantDiv(coeff, iQ, b); + if (level > MaxLevel) + { + level = MaxLevel; + } + + if (sign) + { + level = -level; + } + + input[j] = (short)(level * (int)q); + output[n] = (short)level; + if (level != 0) + { + last = n; + } + } + else + { + output[n] = 0; + input[j] = 0; + } + } + + return last >= 0 ? 1 : 0; + } + + // Quantize as usual, but also compute and return the quantization error. + // Error is already divided by DSHIFT. + public static int QuantizeSingle(Span v, Vp8Matrix mtx) + { + int v0 = v[0]; + bool sign = v0 < 0; + if (sign) + { + v0 = -v0; + } + + if (v0 > (int)mtx.ZThresh[0]) + { + int qV = QuantDiv((uint)v0, mtx.IQ[0], mtx.Bias[0]) * mtx.Q[0]; + int err = v0 - qV; + v[0] = (short)(sign ? -qV : qV); + return (sign ? -err : err) >> DSCALE; + } + + v[0] = 0; + return (sign ? -v0 : v0) >> DSCALE; + } + + public static void CorrectDcValues(Vp8EncIterator it, Vp8Matrix mtx, short[] tmp, Vp8ModeScore rd) + { +#pragma warning disable SA1005 // Single line comments should begin with single space + // | top[0] | top[1] + // --------+--------+--------- + // left[0] | tmp[0] tmp[1] <-> err0 err1 + // left[1] | tmp[2] tmp[3] err2 err3 + // + // Final errors {err1,err2,err3} are preserved and later restored + // as top[]/left[] on the next block. +#pragma warning restore SA1005 // Single line comments should begin with single space + for (int ch = 0; ch <= 1; ++ch) + { + Span top = it.TopDerr.AsSpan((it.X * 4) + ch, 2); + Span left = it.LeftDerr.AsSpan(ch, 2); + Span c = tmp.AsSpan(ch * 4 * 16, 4 * 16); + c[0] += (short)(((C1 * top[0]) + (C2 * left[0])) >> (DSHIFT - DSCALE)); + int err0 = QuantizeSingle(c, mtx); + c[1 * 16] += (short)(((C1 * top[1]) + (C2 * err0)) >> (DSHIFT - DSCALE)); + int err1 = QuantizeSingle(c.Slice(1 * 16), mtx); + c[2 * 16] += (short)(((C1 * err0) + (C2 * left[1])) >> (DSHIFT - DSCALE)); + int err2 = QuantizeSingle(c.Slice(2 * 16), mtx); + c[3 * 16] += (short)(((C1 * err1) + (C2 * err2)) >> (DSHIFT - DSCALE)); + int err3 = QuantizeSingle(c.Slice(3 * 16), mtx); + + rd.Derr[ch, 0] = err1; + rd.Derr[ch, 1] = err2; + rd.Derr[ch, 2] = err3; + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static bool IsFlatSource16(Span src) + { + uint v = src[0] * 0x01010101u; + Span vSpan = BitConverter.GetBytes(v).AsSpan(); + for (int i = 0; i < 16; i++) + { + if (!src.Slice(0, 4).SequenceEqual(vSpan) || !src.Slice(4, 4).SequenceEqual(vSpan) || + !src.Slice(8, 4).SequenceEqual(vSpan) || !src.Slice(12, 4).SequenceEqual(vSpan)) + { + return false; + } + + src = src.Slice(WebpConstants.Bps); + } + + return true; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static bool IsFlat(Span levels, int numBlocks, int thresh) + { + int score = 0; + while (numBlocks-- > 0) + { + for (int i = 1; i < 16; i++) + { + // omit DC, we're only interested in AC + score += levels[i] != 0 ? 1 : 0; + if (score > thresh) + { + return false; + } + } + + levels = levels.Slice(16); + } + + return true; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int Mult8B(int a, int b) => ((a * b) + 128) >> 8; + + [MethodImpl(InliningOptions.ShortMethod)] + private static int QuantDiv(uint n, uint iQ, uint b) => (int)(((n * iQ) + b) >> WebpConstants.QFix); + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8BandProbas.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8BandProbas.cs new file mode 100644 index 000000000..92479a400 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8BandProbas.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + /// + /// All the probabilities associated to one band. + /// + internal class Vp8BandProbas + { + /// + /// Initializes a new instance of the class. + /// + public Vp8BandProbas() + { + this.Probabilities = new Vp8ProbaArray[WebpConstants.NumCtx]; + for (int i = 0; i < WebpConstants.NumCtx; i++) + { + this.Probabilities[i] = new Vp8ProbaArray(); + } + } + + /// + /// Gets the Probabilities. + /// + public Vp8ProbaArray[] Probabilities { get; } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8CostArray.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8CostArray.cs new file mode 100644 index 000000000..f448de6dc --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8CostArray.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + internal class Vp8CostArray + { + /// + /// Initializes a new instance of the class. + /// + public Vp8CostArray() => this.Costs = new ushort[67 + 1]; + + public ushort[] Costs { get; } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Costs.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Costs.cs new file mode 100644 index 000000000..0058684f5 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Costs.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + internal class Vp8Costs + { + /// + /// Initializes a new instance of the class. + /// + public Vp8Costs() + { + this.Costs = new Vp8CostArray[WebpConstants.NumCtx]; + for (int i = 0; i < WebpConstants.NumCtx; i++) + { + this.Costs[i] = new Vp8CostArray(); + } + } + + /// + /// Gets the Costs. + /// + public Vp8CostArray[] Costs { get; } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Decoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Decoder.cs new file mode 100644 index 000000000..d62d23e17 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Decoder.cs @@ -0,0 +1,341 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Formats.Webp.BitReader; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + /// + /// 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); + int cacheUvSize = (16 * this.CacheUvStride) + extraUv; + this.CacheU = memoryAllocator.Allocate(cacheUvSize); + this.CacheV = memoryAllocator.Allocate(cacheUvSize); + 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 pre-calculated per-segment 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 info. + /// + public Vp8FilterInfo[] FilterInfo { get; set; } + + public Vp8MacroBlock CurrentMacroBlock => this.MacroBlockInfo[this.MbX]; + + public Vp8MacroBlock LeftMacroBlock => this.leftMacroBlock ??= new Vp8MacroBlock(); + + public Vp8MacroBlockData CurrentBlockData => this.MacroBlockData[this.MbX]; + + public void PrecomputeFilterStrengths() + { + if (this.Filter == 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.FilterLevel; + } + } + else + { + baseLevel = hdr.FilterLevel; + } + + 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; + } + + int iLevelCap = 9 - hdr.Sharpness; + if (iLevel > iLevelCap) + { + iLevel = iLevelCap; + } + } + + 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 = i4x4 == 1; + } + } + } + + /// + 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/Lossy/Vp8EncIterator.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs new file mode 100644 index 000000000..ca3f8481e --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs @@ -0,0 +1,948 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + /// + /// Iterator structure to iterate through macroblocks, pointing to the + /// right neighbouring data (samples, predictions, contexts, ...) + /// + internal class Vp8EncIterator + { + public const int YOffEnc = 0; + + public const int UOffEnc = 16; + + public const int VOffEnc = 16 + 8; + + private const int MaxIntra16Mode = 2; + + private const int MaxIntra4Mode = 2; + + private const int MaxUvMode = 2; + + private const int DefaultAlpha = -1; + + private readonly int mbw; + + private readonly int mbh; + + /// + /// Stride of the prediction plane(=4*mbw + 1). + /// + private readonly int predsWidth; + + // Array to record the position of the top sample to pass to the prediction functions. + private readonly byte[] vp8TopLeftI4 = + { + 17, 21, 25, 29, + 13, 17, 21, 25, + 9, 13, 17, 21, + 5, 9, 13, 17 + }; + + private int currentMbIdx; + + private int nzIdx; + + private int predIdx; + + private int yTopIdx; + + private int uvTopIdx; + + public Vp8EncIterator(byte[] yTop, byte[] uvTop, uint[] nz, Vp8MacroBlockInfo[] mb, byte[] preds, sbyte[] topDerr, int mbw, int mbh) + { + this.YTop = yTop; + this.UvTop = uvTop; + this.Nz = nz; + this.Mb = mb; + this.Preds = preds; + this.TopDerr = topDerr; + this.LeftDerr = new sbyte[2 * 2]; + this.mbw = mbw; + this.mbh = mbh; + this.currentMbIdx = 0; + this.nzIdx = 1; + this.yTopIdx = 0; + this.uvTopIdx = 0; + this.predsWidth = (4 * mbw) + 1; + this.predIdx = this.predsWidth; + this.YuvIn = new byte[WebpConstants.Bps * 16]; + this.YuvOut = new byte[WebpConstants.Bps * 16]; + this.YuvOut2 = new byte[WebpConstants.Bps * 16]; + this.YuvP = new byte[(32 * WebpConstants.Bps) + (16 * WebpConstants.Bps) + (8 * WebpConstants.Bps)]; // I16+Chroma+I4 preds + this.YLeft = new byte[32]; + this.UvLeft = new byte[32]; + this.TopNz = new int[9]; + this.LeftNz = new int[9]; + this.I4Boundary = new byte[37]; + this.BitCount = new long[4, 3]; + this.Scratch = new byte[WebpConstants.Bps * 16]; + + // To match the C initial values of the reference implementation, initialize all with 204. + byte defaultInitVal = 204; + this.YuvIn.AsSpan().Fill(defaultInitVal); + this.YuvOut.AsSpan().Fill(defaultInitVal); + this.YuvOut2.AsSpan().Fill(defaultInitVal); + this.YuvP.AsSpan().Fill(defaultInitVal); + this.YLeft.AsSpan().Fill(defaultInitVal); + this.UvLeft.AsSpan().Fill(defaultInitVal); + this.Scratch.AsSpan().Fill(defaultInitVal); + + this.Reset(); + } + + /// + /// Gets or sets the current macroblock X value. + /// + public int X { get; set; } + + /// + /// Gets or sets the current macroblock Y. + /// + public int Y { get; set; } + + /// + /// Gets the input samples. + /// + public byte[] YuvIn { get; } + + /// + /// Gets or sets the output samples. + /// + public byte[] YuvOut { get; set; } + + /// + /// Gets or sets the secondary buffer swapped with YuvOut. + /// + public byte[] YuvOut2 { get; set; } + + /// + /// Gets the scratch buffer for prediction. + /// + public byte[] YuvP { get; } + + /// + /// Gets the left luma samples. + /// + public byte[] YLeft { get; } + + /// + /// Gets the left uv samples. + /// + public byte[] UvLeft { get; } + + /// + /// Gets the left error diffusion (u/v). + /// + public sbyte[] LeftDerr { get; } + + /// + /// Gets the top luma samples at position 'X'. + /// + public byte[] YTop { get; } + + /// + /// Gets the top u/v samples at position 'X', packed as 16 bytes. + /// + public byte[] UvTop { get; } + + /// + /// Gets the intra mode predictors (4x4 blocks). + /// + public byte[] Preds { get; } + + /// + /// Gets the current start index of the intra mode predictors. + /// + public int PredIdx => this.predIdx; + + /// + /// Gets the non-zero pattern. + /// + public uint[] Nz { get; } + + /// + /// Gets the top diffusion error. + /// + public sbyte[] TopDerr { get; } + + /// + /// Gets 32+5 boundary samples needed by intra4x4. + /// + public byte[] I4Boundary { get; } + + /// + /// Gets or sets the index to the current top boundary sample. + /// + public int I4BoundaryIdx { get; set; } + + /// + /// Gets or sets the current intra4x4 mode being tested. + /// + public int I4 { get; set; } + + /// + /// Gets the top-non-zero context. + /// + public int[] TopNz { get; } + + /// + /// Gets the left-non-zero. leftNz[8] is independent. + /// + public int[] LeftNz { get; } + + /// + /// Gets or sets the macroblock bit-cost for luma. + /// + public long LumaBits { get; set; } + + /// + /// Gets the bit counters for coded levels. + /// + public long[,] BitCount { get; } + + /// + /// Gets or sets the macroblock bit-cost for chroma. + /// + public long UvBits { get; set; } + + /// + /// Gets or sets the number of mb still to be processed. + /// + public int CountDown { get; set; } + + /// + /// Gets the scratch buffer. + /// + public byte[] Scratch { get; } + + public Vp8MacroBlockInfo CurrentMacroBlockInfo => this.Mb[this.currentMbIdx]; + + private Vp8MacroBlockInfo[] Mb { get; } + + public void Init() => this.Reset(); + + public void InitFilter() + { + // TODO: add support for autofilter + } + + public void StartI4() + { + int i; + this.I4 = 0; // first 4x4 sub-block. + this.I4BoundaryIdx = this.vp8TopLeftI4[0]; + + // Import the boundary samples. + for (i = 0; i < 17; i++) + { + // left + this.I4Boundary[i] = this.YLeft[15 - i + 1]; + } + + Span yTop = this.YTop.AsSpan(this.yTopIdx); + for (i = 0; i < 16; i++) + { + // top + this.I4Boundary[17 + i] = yTop[i]; + } + + // top-right samples have a special case on the far right of the picture. + if (this.X < this.mbw - 1) + { + for (i = 16; i < 16 + 4; i++) + { + this.I4Boundary[17 + i] = yTop[i]; + } + } + else + { + // else, replicate the last valid pixel four times + for (i = 16; i < 16 + 4; i++) + { + this.I4Boundary[17 + i] = this.I4Boundary[17 + 15]; + } + } + + this.NzToBytes(); // import the non-zero context. + } + + // Import uncompressed samples from source. + public void Import(Span y, Span u, Span v, int yStride, int uvStride, int width, int height, bool importBoundarySamples) + { + int yStartIdx = ((this.Y * yStride) + this.X) * 16; + int uvStartIdx = ((this.Y * uvStride) + this.X) * 8; + Span ySrc = y.Slice(yStartIdx); + Span uSrc = u.Slice(uvStartIdx); + Span vSrc = v.Slice(uvStartIdx); + int w = Math.Min(width - (this.X * 16), 16); + int h = Math.Min(height - (this.Y * 16), 16); + int uvw = (w + 1) >> 1; + int uvh = (h + 1) >> 1; + + Span yuvIn = this.YuvIn.AsSpan(YOffEnc); + Span uIn = this.YuvIn.AsSpan(UOffEnc); + Span vIn = this.YuvIn.AsSpan(VOffEnc); + this.ImportBlock(ySrc, yStride, yuvIn, w, h, 16); + this.ImportBlock(uSrc, uvStride, uIn, uvw, uvh, 8); + this.ImportBlock(vSrc, uvStride, vIn, uvw, uvh, 8); + + if (!importBoundarySamples) + { + return; + } + + // Import source (uncompressed) samples into boundary. + if (this.X == 0) + { + this.InitLeft(); + } + else + { + Span yLeft = this.YLeft.AsSpan(); + Span uLeft = this.UvLeft.AsSpan(0, 16); + Span vLeft = this.UvLeft.AsSpan(16, 16); + if (this.Y == 0) + { + yLeft[0] = 127; + uLeft[0] = 127; + vLeft[0] = 127; + } + else + { + yLeft[0] = y[yStartIdx - 1 - yStride]; + uLeft[0] = u[uvStartIdx - 1 - uvStride]; + vLeft[0] = v[uvStartIdx - 1 - uvStride]; + } + + this.ImportLine(y.Slice(yStartIdx - 1), yStride, yLeft.Slice(1), h, 16); + this.ImportLine(u.Slice(uvStartIdx - 1), uvStride, uLeft.Slice(1), uvh, 8); + this.ImportLine(v.Slice(uvStartIdx - 1), uvStride, vLeft.Slice(1), uvh, 8); + } + + Span yTop = this.YTop.AsSpan(this.yTopIdx, 16); + if (this.Y == 0) + { + yTop.Fill(127); + this.UvTop.AsSpan(this.uvTopIdx, 16).Fill(127); + } + else + { + this.ImportLine(y.Slice(yStartIdx - yStride), 1, yTop, w, 16); + this.ImportLine(u.Slice(uvStartIdx - uvStride), 1, this.UvTop.AsSpan(this.uvTopIdx, 8), uvw, 8); + this.ImportLine(v.Slice(uvStartIdx - uvStride), 1, this.UvTop.AsSpan(this.uvTopIdx + 8, 8), uvw, 8); + } + } + + public int FastMbAnalyze(int quality) + { + // Empirical cut-off value, should be around 16 (~=block size). We use the + // [8-17] range and favor intra4 at high quality, intra16 for low quality. + int q = quality; + int kThreshold = 8 + ((17 - 8) * q / 100); + int k; + uint[] dc = new uint[16]; + uint m; + uint m2; + for (k = 0; k < 16; k += 4) + { + this.Mean16x4(this.YuvIn.AsSpan(YOffEnc + (k * WebpConstants.Bps)), dc.AsSpan(k)); + } + + for (m = 0, m2 = 0, k = 0; k < 16; ++k) + { + m += dc[k]; + m2 += dc[k] * dc[k]; + } + + if (kThreshold * m2 < m * m) + { + this.SetIntra16Mode(0); // DC16 + } + else + { + byte[] modes = new byte[16]; // DC4 + this.SetIntra4Mode(modes); + } + + return 0; + } + + public int MbAnalyzeBestIntra16Mode() + { + int maxMode = MaxIntra16Mode; + int mode; + int bestAlpha = DefaultAlpha; + int bestMode = 0; + + this.MakeLuma16Preds(); + for (mode = 0; mode < maxMode; ++mode) + { + var histo = new Vp8Histogram(); + histo.CollectHistogram(this.YuvIn.AsSpan(YOffEnc), this.YuvP.AsSpan(Vp8Encoding.Vp8I16ModeOffsets[mode]), 0, 16); + int alpha = histo.GetAlpha(); + if (alpha > bestAlpha) + { + bestAlpha = alpha; + bestMode = mode; + } + } + + this.SetIntra16Mode(bestMode); + return bestAlpha; + } + + public int MbAnalyzeBestIntra4Mode(int bestAlpha) + { + byte[] modes = new byte[16]; + int maxMode = MaxIntra4Mode; + var totalHisto = new Vp8Histogram(); + int curHisto = 0; + this.StartI4(); + do + { + int mode; + int bestModeAlpha = DefaultAlpha; + var histos = new Vp8Histogram[2]; + Span src = this.YuvIn.AsSpan(YOffEnc + WebpLookupTables.Vp8Scan[this.I4]); + + this.MakeIntra4Preds(); + for (mode = 0; mode < maxMode; ++mode) + { + histos[curHisto] = new Vp8Histogram(); + histos[curHisto].CollectHistogram(src, this.YuvP.AsSpan(Vp8Encoding.Vp8I4ModeOffsets[mode]), 0, 1); + + int alpha = histos[curHisto].GetAlpha(); + if (alpha > bestModeAlpha) + { + bestModeAlpha = alpha; + modes[this.I4] = (byte)mode; + + // Keep track of best histo so far. + curHisto ^= 1; + } + } + + // Accumulate best histogram. + histos[curHisto ^ 1].Merge(totalHisto); + } + while (this.RotateI4(this.YuvIn.AsSpan(YOffEnc))); // Note: we reuse the original samples for predictors. + + int i4Alpha = totalHisto.GetAlpha(); + if (i4Alpha > bestAlpha) + { + this.SetIntra4Mode(modes); + bestAlpha = i4Alpha; + } + + return bestAlpha; + } + + public int MbAnalyzeBestUvMode() + { + int bestAlpha = DefaultAlpha; + int smallestAlpha = 0; + int bestMode = 0; + int maxMode = MaxUvMode; + int mode; + + this.MakeChroma8Preds(); + for (mode = 0; mode < maxMode; ++mode) + { + var histo = new Vp8Histogram(); + histo.CollectHistogram(this.YuvIn.AsSpan(UOffEnc), this.YuvP.AsSpan(Vp8Encoding.Vp8UvModeOffsets[mode]), 16, 16 + 4 + 4); + int alpha = histo.GetAlpha(); + if (alpha > bestAlpha) + { + bestAlpha = alpha; + } + + // The best prediction mode tends to be the one with the smallest alpha. + if (mode == 0 || alpha < smallestAlpha) + { + smallestAlpha = alpha; + bestMode = mode; + } + } + + this.SetIntraUvMode(bestMode); + return bestAlpha; + } + + public void SetIntra16Mode(int mode) + { + Span preds = this.Preds.AsSpan(this.predIdx); + for (int y = 0; y < 4; y++) + { + preds.Slice(0, 4).Fill((byte)mode); + preds = preds.Slice(this.predsWidth); + } + + this.CurrentMacroBlockInfo.MacroBlockType = Vp8MacroBlockType.I16X16; + } + + public void SetIntra4Mode(byte[] modes) + { + int modesIdx = 0; + int predIdx = this.predIdx; + for (int y = 4; y > 0; y--) + { + modes.AsSpan(modesIdx, 4).CopyTo(this.Preds.AsSpan(predIdx)); + predIdx += this.predsWidth; + modesIdx += 4; + } + + this.CurrentMacroBlockInfo.MacroBlockType = Vp8MacroBlockType.I4X4; + } + + public int GetCostLuma16(Vp8ModeScore rd, Vp8EncProba proba) + { + var res = new Vp8Residual(); + int r = 0; + + // re-import the non-zero context. + this.NzToBytes(); + + // DC + res.Init(0, 1, proba); + res.SetCoeffs(rd.YDcLevels); + r += res.GetResidualCost(this.TopNz[8] + this.LeftNz[8]); + + // AC + res.Init(1, 0, proba); + for (int y = 0; y < 4; y++) + { + for (int x = 0; x < 4; x++) + { + int ctx = this.TopNz[x] + this.LeftNz[y]; + res.SetCoeffs(rd.YAcLevels.AsSpan((x + (y * 4)) * 16, 16)); + r += res.GetResidualCost(ctx); + this.TopNz[x] = this.LeftNz[y] = res.Last >= 0 ? 1 : 0; + } + } + + return r; + } + + public short[] GetCostModeI4(byte[] modes) + { + int predsWidth = this.predsWidth; + int predIdx = this.predIdx; + int x = this.I4 & 3; + int y = this.I4 >> 2; + int left = x == 0 ? this.Preds[predIdx + (y * predsWidth) - 1] : modes[this.I4 - 1]; + int top = y == 0 ? this.Preds[predIdx - predsWidth + x] : modes[this.I4 - 4]; + return WebpLookupTables.Vp8FixedCostsI4[top, left]; + } + + public int GetCostLuma4(short[] levels, Vp8EncProba proba) + { + int x = this.I4 & 3; + int y = this.I4 >> 2; + var res = new Vp8Residual(); + int r = 0; + + res.Init(0, 3, proba); + int ctx = this.TopNz[x] + this.LeftNz[y]; + res.SetCoeffs(levels); + r += res.GetResidualCost(ctx); + return r; + } + + public int GetCostUv(Vp8ModeScore rd, Vp8EncProba proba) + { + var res = new Vp8Residual(); + int r = 0; + + // re-import the non-zero context. + this.NzToBytes(); + + res.Init(0, 2, proba); + for (int ch = 0; ch <= 2; ch += 2) + { + for (int y = 0; y < 2; y++) + { + for (int x = 0; x < 2; x++) + { + int ctx = this.TopNz[4 + ch + x] + this.LeftNz[4 + ch + y]; + res.SetCoeffs(rd.UvLevels.AsSpan(((ch * 2) + x + (y * 2)) * 16, 16)); + r += res.GetResidualCost(ctx); + this.TopNz[4 + ch + x] = this.LeftNz[4 + ch + y] = res.Last >= 0 ? 1 : 0; + } + } + } + + return r; + } + + public void SetIntraUvMode(int mode) => this.CurrentMacroBlockInfo.UvMode = mode; + + public void SetSkip(bool skip) => this.CurrentMacroBlockInfo.Skip = skip; + + public void SetSegment(int segment) => this.CurrentMacroBlockInfo.Segment = segment; + + public void StoreDiffusionErrors(Vp8ModeScore rd) + { + for (int ch = 0; ch <= 1; ++ch) + { + Span top = this.TopDerr.AsSpan((this.X * 4) + ch, 2); + Span left = this.LeftDerr.AsSpan(ch, 2); + + // restore err1 + left[0] = (sbyte)rd.Derr[ch, 0]; + + // 3/4th of err3 + left[1] = (sbyte)((3 * rd.Derr[ch, 2]) >> 2); + + // err2 + top[0] = (sbyte)rd.Derr[ch, 1]; + + // 1/4th of err3. + top[1] = (sbyte)(rd.Derr[ch, 2] - left[1]); + } + } + + /// + /// Returns true if iteration is finished. + /// + /// True if iterator is finished. + public bool IsDone() => this.CountDown <= 0; + + /// + /// Go to next macroblock. + /// + /// Returns false if not finished. + public bool Next() + { + if (++this.X == this.mbw) + { + this.SetRow(++this.Y); + } + else + { + this.currentMbIdx++; + this.nzIdx++; + this.predIdx += 4; + this.yTopIdx += 16; + this.uvTopIdx += 16; + } + + return --this.CountDown > 0; + } + + public void SaveBoundary() + { + int x = this.X; + int y = this.Y; + Span ySrc = this.YuvOut.AsSpan(YOffEnc); + Span uvSrc = this.YuvOut.AsSpan(UOffEnc); + if (x < this.mbw - 1) + { + // left + for (int i = 0; i < 16; i++) + { + this.YLeft[i + 1] = ySrc[15 + (i * WebpConstants.Bps)]; + } + + for (int i = 0; i < 8; i++) + { + this.UvLeft[i + 1] = uvSrc[7 + (i * WebpConstants.Bps)]; + this.UvLeft[i + 16 + 1] = uvSrc[15 + (i * WebpConstants.Bps)]; + } + + // top-left (before 'top'!) + this.YLeft[0] = this.YTop[this.yTopIdx + 15]; + this.UvLeft[0] = this.UvTop[this.uvTopIdx + 0 + 7]; + this.UvLeft[16] = this.UvTop[this.uvTopIdx + 8 + 7]; + } + + if (y < this.mbh - 1) + { + // top + ySrc.Slice(15 * WebpConstants.Bps, 16).CopyTo(this.YTop.AsSpan(this.yTopIdx)); + uvSrc.Slice(7 * WebpConstants.Bps, 8 + 8).CopyTo(this.UvTop.AsSpan(this.uvTopIdx)); + } + } + + public bool RotateI4(Span yuvOut) + { + Span blk = yuvOut.Slice(WebpLookupTables.Vp8Scan[this.I4]); + Span top = this.I4Boundary.AsSpan(); + int topOffset = this.I4BoundaryIdx; + int i; + + // Update the cache with 7 fresh samples. + for (i = 0; i <= 3; i++) + { + top[topOffset - 4 + i] = blk[i + (3 * WebpConstants.Bps)]; // Store future top samples. + } + + if ((this.I4 & 3) != 3) + { + // if not on the right sub-blocks #3, #7, #11, #15 + for (i = 0; i <= 2; i++) + { + // store future left samples + top[topOffset + i] = blk[3 + ((2 - i) * WebpConstants.Bps)]; + } + } + else + { + // else replicate top-right samples, as says the specs. + for (i = 0; i <= 3; i++) + { + top[topOffset + i] = top[topOffset + i + 4]; + } + } + + // move pointers to next sub-block + ++this.I4; + if (this.I4 == 16) + { + // we're done + return false; + } + + this.I4BoundaryIdx = this.vp8TopLeftI4[this.I4]; + + return true; + } + + public void ResetAfterSkip() + { + if (this.CurrentMacroBlockInfo.MacroBlockType == Vp8MacroBlockType.I16X16) + { + // Reset all predictors. + this.Nz[this.nzIdx] = 0; + this.LeftNz[8] = 0; + } + else + { + // Preserve the dc_nz bit. + this.Nz[this.nzIdx] &= 1 << 24; + } + } + + public void MakeLuma16Preds() + { + Span left = this.X != 0 ? this.YLeft.AsSpan() : null; + Span top = this.Y != 0 ? this.YTop.AsSpan(this.yTopIdx) : null; + Vp8Encoding.EncPredLuma16(this.YuvP, left, top); + } + + public void MakeChroma8Preds() + { + Span left = this.X != 0 ? this.UvLeft.AsSpan() : null; + Span top = this.Y != 0 ? this.UvTop.AsSpan(this.uvTopIdx) : null; + Vp8Encoding.EncPredChroma8(this.YuvP, left, top); + } + + public void MakeIntra4Preds() => Vp8Encoding.EncPredLuma4(this.YuvP, this.I4Boundary, this.I4BoundaryIdx); + + public void SwapOut() + { + byte[] tmp = this.YuvOut; + this.YuvOut = this.YuvOut2; + this.YuvOut2 = tmp; + } + + public void NzToBytes() + { + Span nz = this.Nz.AsSpan(); + + uint lnz = nz[this.nzIdx - 1]; + uint tnz = nz[this.nzIdx]; + Span topNz = this.TopNz; + Span leftNz = this.LeftNz; + + // Top-Y + topNz[0] = this.Bit(tnz, 12); + topNz[1] = this.Bit(tnz, 13); + topNz[2] = this.Bit(tnz, 14); + topNz[3] = this.Bit(tnz, 15); + + // Top-U + topNz[4] = this.Bit(tnz, 18); + topNz[5] = this.Bit(tnz, 19); + + // Top-V + topNz[6] = this.Bit(tnz, 22); + topNz[7] = this.Bit(tnz, 23); + + // DC + topNz[8] = this.Bit(tnz, 24); + + // left-Y + leftNz[0] = this.Bit(lnz, 3); + leftNz[1] = this.Bit(lnz, 7); + leftNz[2] = this.Bit(lnz, 11); + leftNz[3] = this.Bit(lnz, 15); + + // left-U + leftNz[4] = this.Bit(lnz, 17); + leftNz[5] = this.Bit(lnz, 19); + + // left-V + leftNz[6] = this.Bit(lnz, 21); + leftNz[7] = this.Bit(lnz, 23); + + // left-DC is special, iterated separately. + } + + public void BytesToNz() + { + uint nz = 0; + int[] topNz = this.TopNz; + int[] leftNz = this.LeftNz; + + // top + nz |= (uint)((topNz[0] << 12) | (topNz[1] << 13)); + nz |= (uint)((topNz[2] << 14) | (topNz[3] << 15)); + nz |= (uint)((topNz[4] << 18) | (topNz[5] << 19)); + nz |= (uint)((topNz[6] << 22) | (topNz[7] << 23)); + nz |= (uint)(topNz[8] << 24); // we propagate the top bit, esp. for intra4 + + // left + nz |= (uint)((leftNz[0] << 3) | (leftNz[1] << 7)); + nz |= (uint)(leftNz[2] << 11); + nz |= (uint)((leftNz[4] << 17) | (leftNz[6] << 21)); + + this.Nz[this.nzIdx] = nz; + } + + private void Mean16x4(Span input, Span dc) + { + for (int k = 0; k < 4; k++) + { + uint avg = 0; + for (int y = 0; y < 4; y++) + { + for (int x = 0; x < 4; x++) + { + avg += input[x + (y * WebpConstants.Bps)]; + } + } + + dc[k] = avg; + input = input.Slice(4); // go to next 4x4 block. + } + } + + private void ImportBlock(Span src, int srcStride, Span dst, int w, int h, int size) + { + int dstIdx = 0; + int srcIdx = 0; + for (int i = 0; i < h; i++) + { + // memcpy(dst, src, w); + src.Slice(srcIdx, w).CopyTo(dst.Slice(dstIdx)); + if (w < size) + { + // memset(dst + w, dst[w - 1], size - w); + dst.Slice(dstIdx + w, size - w).Fill(dst[dstIdx + w - 1]); + } + + dstIdx += WebpConstants.Bps; + srcIdx += srcStride; + } + + for (int i = h; i < size; i++) + { + // memcpy(dst, dst - BPS, size); + dst.Slice(dstIdx - WebpConstants.Bps, size).CopyTo(dst.Slice(dstIdx)); + dstIdx += WebpConstants.Bps; + } + } + + private void ImportLine(Span src, int srcStride, Span dst, int len, int totalLen) + { + int i; + int srcIdx = 0; + for (i = 0; i < len; i++) + { + dst[i] = src[srcIdx]; + srcIdx += srcStride; + } + + for (; i < totalLen; i++) + { + dst[i] = dst[len - 1]; + } + } + + /// + /// Restart a scan. + /// + private void Reset() + { + this.SetRow(0); + this.SetCountDown(this.mbw * this.mbh); + this.InitTop(); + + Array.Clear(this.BitCount, 0, this.BitCount.Length); + } + + /// + /// Reset iterator position to row 'y'. + /// + /// The y position. + private void SetRow(int y) + { + this.X = 0; + this.Y = y; + this.currentMbIdx = y * this.mbw; + this.nzIdx = 1; // note: in reference source nz starts at -1. + this.yTopIdx = 0; + this.uvTopIdx = 0; + this.predIdx = this.predsWidth + (y * 4 * this.predsWidth); + + this.InitLeft(); + } + + private void InitLeft() + { + Span yLeft = this.YLeft.AsSpan(); + Span uLeft = this.UvLeft.AsSpan(0, 16); + Span vLeft = this.UvLeft.AsSpan(16, 16); + byte val = (byte)(this.Y > 0 ? 129 : 127); + yLeft[0] = val; + uLeft[0] = val; + vLeft[0] = val; + + yLeft.Slice(1, 16).Fill(129); + uLeft.Slice(1, 8).Fill(129); + vLeft.Slice(1, 8).Fill(129); + + this.LeftNz[8] = 0; + + this.LeftDerr.AsSpan().Fill(0); + } + + private void InitTop() + { + int topSize = this.mbw * 16; + this.YTop.AsSpan(0, topSize).Fill(127); + this.UvTop.AsSpan().Fill(127); + this.Nz.AsSpan().Fill(0); + + int predsW = (4 * this.mbw) + 1; + int predsH = (4 * this.mbh) + 1; + int predsSize = predsW * predsH; + this.Preds.AsSpan(predsSize + this.predsWidth, this.mbw).Fill(0); + + this.TopDerr.AsSpan().Fill(0); + } + + private int Bit(uint nz, int n) => (nz & (1 << n)) != 0 ? 1 : 0; + + /// + /// Set count down. + /// + /// Number of iterations to go. + private void SetCountDown(int countDown) => this.CountDown = countDown; + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8EncProba.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8EncProba.cs new file mode 100644 index 000000000..e12839b3d --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8EncProba.cs @@ -0,0 +1,265 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + internal class Vp8EncProba + { + /// + /// Last (inclusive) level with variable cost. + /// + private const int MaxVariableLevel = 67; + + /// + /// Value below which using skipProba is OK. + /// + private const int SkipProbaThreshold = 250; + + /// + /// Initializes a new instance of the class. + /// + public Vp8EncProba() + { + this.Dirty = true; + this.UseSkipProba = false; + this.Segments = new byte[3]; + this.Coeffs = new Vp8BandProbas[WebpConstants.NumTypes][]; + for (int i = 0; i < this.Coeffs.Length; i++) + { + this.Coeffs[i] = new Vp8BandProbas[WebpConstants.NumBands]; + for (int j = 0; j < this.Coeffs[i].Length; j++) + { + this.Coeffs[i][j] = new Vp8BandProbas(); + } + } + + this.Stats = new Vp8Stats[WebpConstants.NumTypes][]; + for (int i = 0; i < this.Coeffs.Length; i++) + { + this.Stats[i] = new Vp8Stats[WebpConstants.NumBands]; + for (int j = 0; j < this.Stats[i].Length; j++) + { + this.Stats[i][j] = new Vp8Stats(); + } + } + + this.LevelCost = new Vp8Costs[WebpConstants.NumTypes][]; + for (int i = 0; i < this.LevelCost.Length; i++) + { + this.LevelCost[i] = new Vp8Costs[WebpConstants.NumBands]; + for (int j = 0; j < this.LevelCost[i].Length; j++) + { + this.LevelCost[i][j] = new Vp8Costs(); + } + } + + this.RemappedCosts = new Vp8Costs[WebpConstants.NumTypes][]; + for (int i = 0; i < this.RemappedCosts.Length; i++) + { + this.RemappedCosts[i] = new Vp8Costs[16]; + for (int j = 0; j < this.RemappedCosts[i].Length; j++) + { + this.RemappedCosts[i][j] = new Vp8Costs(); + } + } + + // Initialize with default probabilities. + this.Segments.AsSpan().Fill(255); + for (int t = 0; t < WebpConstants.NumTypes; ++t) + { + for (int b = 0; b < WebpConstants.NumBands; ++b) + { + for (int c = 0; c < WebpConstants.NumCtx; ++c) + { + Vp8ProbaArray dst = this.Coeffs[t][b].Probabilities[c]; + for (int p = 0; p < WebpConstants.NumProbas; ++p) + { + dst.Probabilities[p] = WebpLookupTables.DefaultCoeffsProba[t, b, c, p]; + } + } + } + } + } + + /// + /// Gets the probabilities for segment tree. + /// + public byte[] Segments { get; } + + /// + /// Gets or sets the final probability of being skipped. + /// + public byte SkipProba { get; set; } + + /// + /// Gets or sets a value indicating whether to use the skip probability. + /// + public bool UseSkipProba { get; set; } + + public Vp8BandProbas[][] Coeffs { get; } + + public Vp8Stats[][] Stats { get; } + + public Vp8Costs[][] LevelCost { get; } + + public Vp8Costs[][] RemappedCosts { get; } + + /// + /// Gets or sets the number of skipped blocks. + /// + public int NbSkip { get; set; } + + /// + /// Gets or sets a value indicating whether CalculateLevelCosts() needs to be called. + /// + public bool Dirty { get; set; } + + public void CalculateLevelCosts() + { + if (!this.Dirty) + { + return; // Nothing to do. + } + + for (int ctype = 0; ctype < WebpConstants.NumTypes; ++ctype) + { + for (int band = 0; band < WebpConstants.NumBands; ++band) + { + for (int ctx = 0; ctx < WebpConstants.NumCtx; ++ctx) + { + Vp8ProbaArray p = this.Coeffs[ctype][band].Probabilities[ctx]; + Vp8CostArray table = this.LevelCost[ctype][band].Costs[ctx]; + int cost0 = ctx > 0 ? LossyUtils.Vp8BitCost(1, p.Probabilities[0]) : 0; + int costBase = LossyUtils.Vp8BitCost(1, p.Probabilities[1]) + cost0; + int v; + table.Costs[0] = (ushort)(LossyUtils.Vp8BitCost(0, p.Probabilities[1]) + cost0); + for (v = 1; v <= MaxVariableLevel; ++v) + { + table.Costs[v] = (ushort)(costBase + VariableLevelCost(v, p.Probabilities)); + } + + // Starting at level 67 and up, the variable part of the cost is actually constant + } + } + + for (int n = 0; n < 16; ++n) + { + for (int ctx = 0; ctx < WebpConstants.NumCtx; ++ctx) + { + Vp8CostArray dst = this.RemappedCosts[ctype][n].Costs[ctx]; + Vp8CostArray src = this.LevelCost[ctype][WebpConstants.Vp8EncBands[n]].Costs[ctx]; + src.Costs.CopyTo(dst.Costs.AsSpan()); + } + } + } + + this.Dirty = false; + } + + public int FinalizeTokenProbas() + { + bool hasChanged = false; + int size = 0; + 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) + { + uint stats = this.Stats[t][b].Stats[c].Stats[p]; + int nb = (int)((stats >> 0) & 0xffff); + int total = (int)((stats >> 16) & 0xffff); + int updateProba = WebpLookupTables.CoeffsUpdateProba[t, b, c, p]; + int oldP = WebpLookupTables.DefaultCoeffsProba[t, b, c, p]; + int newP = CalcTokenProba(nb, total); + int oldCost = BranchCost(nb, total, oldP) + LossyUtils.Vp8BitCost(0, (byte)updateProba); + int newCost = BranchCost(nb, total, newP) + LossyUtils.Vp8BitCost(1, (byte)updateProba) + (8 * 256); + bool useNewP = oldCost > newCost; + size += LossyUtils.Vp8BitCost(useNewP ? 1 : 0, (byte)updateProba); + if (useNewP) + { + // Only use proba that seem meaningful enough. + this.Coeffs[t][b].Probabilities[c].Probabilities[p] = (byte)newP; + hasChanged |= newP != oldP; + size += 8 * 256; + } + else + { + this.Coeffs[t][b].Probabilities[c].Probabilities[p] = (byte)oldP; + } + } + } + } + } + + this.Dirty = hasChanged; + return size; + } + + public int FinalizeSkipProba(int mbw, int mbh) + { + int nbMbs = mbw * mbh; + int nbEvents = this.NbSkip; + this.SkipProba = (byte)CalcSkipProba(nbEvents, nbMbs); + this.UseSkipProba = this.SkipProba < SkipProbaThreshold; + + int size = 256; + if (this.UseSkipProba) + { + size += (nbEvents * LossyUtils.Vp8BitCost(1, this.SkipProba)) + ((nbMbs - nbEvents) * LossyUtils.Vp8BitCost(0, this.SkipProba)); + size += 8 * 256; // cost of signaling the skipProba itself. + } + + return size; + } + + public void ResetTokenStats() + { + 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) + { + this.Stats[t][b].Stats[c].Stats[p] = 0; + } + } + } + } + } + + private static int CalcSkipProba(long nb, long total) => (int)(total != 0 ? (total - nb) * 255 / total : 255); + + private static int VariableLevelCost(int level, Span probas) + { + int pattern = WebpLookupTables.Vp8LevelCodes[level - 1][0]; + int bits = WebpLookupTables.Vp8LevelCodes[level - 1][1]; + int cost = 0; + for (int i = 2; pattern != 0; i++) + { + if ((pattern & 1) != 0) + { + cost += LossyUtils.Vp8BitCost(bits & 1, probas[i]); + } + + bits >>= 1; + pattern >>= 1; + } + + return cost; + } + + // Collect statistics and deduce probabilities for next coding pass. + // Return the total bit-cost for coding the probability updates. + private static int CalcTokenProba(int nb, int total) => nb != 0 ? (255 - (nb * 255 / total)) : 255; + + // Cost of coding 'nb' 1's and 'total-nb' 0's using 'proba' probability. + private static int BranchCost(int nb, int total, int proba) => (nb * LossyUtils.Vp8BitCost(1, (byte)proba)) + ((total - nb) * LossyUtils.Vp8BitCost(0, (byte)proba)); + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8EncSegmentHeader.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8EncSegmentHeader.cs new file mode 100644 index 000000000..033bad02c --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8EncSegmentHeader.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + internal class Vp8EncSegmentHeader + { + /// + /// Initializes a new instance of the class. + /// + /// Number of segments. + public Vp8EncSegmentHeader(int numSegments) + { + this.NumSegments = numSegments; + this.UpdateMap = this.NumSegments > 1; + this.Size = 0; + } + + /// + /// Gets the actual number of segments. 1 segment only = unused. + /// + public int NumSegments { get; } + + /// + /// Gets or sets a value indicating whether to update the segment map or not. Must be false if there's only 1 segment. + /// + public bool UpdateMap { get; set; } + + /// + /// Gets or sets the bit-cost for transmitting the segment map. + /// + public int Size { get; set; } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs new file mode 100644 index 000000000..37808d56c --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -0,0 +1,1102 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.IO; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Webp.BitWriter; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + /// + /// Encoder for lossy webp images. + /// + internal class Vp8Encoder : IDisposable + { + /// + /// The to use for buffer allocations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The global configuration. + /// + private readonly Configuration configuration; + + /// + /// The quality, that will be used to encode the image. + /// + private readonly int quality; + + /// + /// Quality/speed trade-off (0=fast, 6=slower-better). + /// + private readonly WebpEncodingMethod method; + + /// + /// Number of entropy-analysis passes (in [1..10]). + /// + private readonly int entropyPasses; + + /// + /// Specify the strength of the deblocking filter, between 0 (no filtering) and 100 (maximum filtering). A value of 0 will turn off any filtering. + /// + private readonly int filterStrength; + + /// + /// The spatial noise shaping. 0=off, 100=maximum. + /// + private readonly int spatialNoiseShaping; + + /// + /// A bit writer for writing lossy webp streams. + /// + private Vp8BitWriter bitWriter; + + private readonly Vp8RdLevel rdOptLevel; + + private int maxI4HeaderBits; + + /// + /// Global susceptibility. + /// + private int alpha; + + /// + /// U/V quantization susceptibility. + /// + private int uvAlpha; + + private readonly byte[] averageBytesPerMb = { 50, 24, 16, 9, 7, 5, 3, 2 }; + + private const int NumMbSegments = 4; + + private const int MaxItersKMeans = 6; + + // Convergence is considered reached if dq < DqLimit + private const float DqLimit = 0.4f; + + private const ulong Partition0SizeLimit = (WebpConstants.Vp8MaxPartition0Size - 2048UL) << 11; + + private const long HeaderSizeEstimate = WebpConstants.RiffHeaderSize + WebpConstants.ChunkHeaderSize + WebpConstants.Vp8FrameHeaderSize; + + private const int QMin = 0; + + private const int QMax = 100; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The global configuration. + /// The width of the input image. + /// The height of the input image. + /// The encoding quality. + /// Quality/speed trade-off (0=fast, 6=slower-better). + /// Number of entropy-analysis passes (in [1..10]). + /// The filter the strength of the deblocking filter, between 0 (no filtering) and 100 (maximum filtering). + /// The spatial noise shaping. 0=off, 100=maximum. + public Vp8Encoder( + MemoryAllocator memoryAllocator, + Configuration configuration, + int width, + int height, + int quality, + WebpEncodingMethod method, + int entropyPasses, + int filterStrength, + int spatialNoiseShaping) + { + this.memoryAllocator = memoryAllocator; + this.configuration = configuration; + this.Width = width; + this.Height = height; + this.quality = Numerics.Clamp(quality, 0, 100); + this.method = method; + this.entropyPasses = Numerics.Clamp(entropyPasses, 1, 10); + this.filterStrength = Numerics.Clamp(filterStrength, 0, 100); + this.spatialNoiseShaping = Numerics.Clamp(spatialNoiseShaping, 0, 100); + this.rdOptLevel = method is WebpEncodingMethod.BestQuality ? Vp8RdLevel.RdOptTrellisAll + : method >= WebpEncodingMethod.Level5 ? Vp8RdLevel.RdOptTrellis + : method >= WebpEncodingMethod.Level3 ? Vp8RdLevel.RdOptBasic + : Vp8RdLevel.RdOptNone; + + int pixelCount = width * height; + this.Mbw = (width + 15) >> 4; + this.Mbh = (height + 15) >> 4; + int uvSize = ((width + 1) >> 1) * ((height + 1) >> 1); + this.Y = this.memoryAllocator.Allocate(pixelCount); + this.U = this.memoryAllocator.Allocate(uvSize); + this.V = this.memoryAllocator.Allocate(uvSize); + this.YTop = new byte[this.Mbw * 16]; + this.UvTop = new byte[this.Mbw * 16 * 2]; + this.Nz = new uint[this.Mbw + 1]; + this.MbHeaderLimit = 256 * 510 * 8 * 1024 / (this.Mbw * this.Mbh); + this.TopDerr = new sbyte[this.Mbw * 4]; + + // TODO: make partition_limit configurable? + int limit = 100; // original code: limit = 100 - config->partition_limit; + this.maxI4HeaderBits = + 256 * 16 * 16 * limit * limit / (100 * 100); // ... modulated with a quadratic curve. + + this.MbInfo = new Vp8MacroBlockInfo[this.Mbw * this.Mbh]; + for (int i = 0; i < this.MbInfo.Length; i++) + { + this.MbInfo[i] = new Vp8MacroBlockInfo(); + } + + this.SegmentInfos = new Vp8SegmentInfo[4]; + for (int i = 0; i < 4; i++) + { + this.SegmentInfos[i] = new Vp8SegmentInfo(); + } + + this.FilterHeader = new Vp8FilterHeader(); + int predSize = (((4 * this.Mbw) + 1) * ((4 * this.Mbh) + 1)) + this.PredsWidth + 1; + this.PredsWidth = (4 * this.Mbw) + 1; + this.Proba = new Vp8EncProba(); + this.Preds = new byte[predSize + this.PredsWidth + this.Mbw]; + + // Initialize with default values, which the reference c implementation uses, + // to be able to compare to the original and spot differences. + this.Preds.AsSpan().Fill(205); + this.Nz.AsSpan().Fill(3452816845); + + this.ResetBoundaryPredictions(); + } + + public int BaseQuant { get; set; } + + /// + /// Gets the probabilities. + /// + public Vp8EncProba Proba { get; } + + /// + /// Gets the segment features. + /// + public Vp8EncSegmentHeader SegmentHeader { get; private set; } + + /// + /// Gets the segment infos. + /// + public Vp8SegmentInfo[] SegmentInfos { get; } + + /// + /// Gets the macro block info's. + /// + public Vp8MacroBlockInfo[] MbInfo { get; } + + /// + /// Gets the filter header. + /// + public Vp8FilterHeader FilterHeader { get; } + + /// + /// Gets or sets the global susceptibility. + /// + public int Alpha { get; set; } + + /// + /// Gets the width of the image. + /// + public int Width { get; } + + /// + /// Gets the height of the image. + /// + public int Height { get; } + + /// + /// Gets the stride of the prediction plane (=4*mb_w + 1) + /// + public int PredsWidth { get; } + + /// + /// Gets the macroblock width. + /// + public int Mbw { get; } + + /// + /// Gets the macroblock height. + /// + public int Mbh { get; } + + public int DqY1Dc { get; private set; } + + public int DqY2Ac { get; private set; } + + public int DqY2Dc { get; private set; } + + public int DqUvAc { get; private set; } + + public int DqUvDc { get; private set; } + + /// + /// Gets the luma component. + /// + private IMemoryOwner Y { get; } + + /// + /// Gets the chroma U component. + /// + private IMemoryOwner U { get; } + + /// + /// Gets the chroma U component. + /// + private IMemoryOwner V { get; } + + /// + /// Gets the top luma samples. + /// + public byte[] YTop { get; } + + /// + /// Gets the top u/v samples. U and V are packed into 16 bytes (8 U + 8 V). + /// + public byte[] UvTop { get; } + + /// + /// Gets the non-zero pattern. + /// + public uint[] Nz { get; } + + /// + /// Gets the prediction modes: (4*mbw+1) * (4*mbh+1). + /// + public byte[] Preds { get; } + + /// + /// Gets the diffusion error. + /// + public sbyte[] TopDerr { get; } + + /// + /// Gets a rough limit for header bits per MB. + /// + private int MbHeaderLimit { get; } + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + public void Encode(Image image, Stream stream) + where TPixel : unmanaged, IPixel + { + int width = image.Width; + int height = image.Height; + Span y = this.Y.GetSpan(); + Span u = this.U.GetSpan(); + Span v = this.V.GetSpan(); + YuvConversion.ConvertRgbToYuv(image, this.configuration, this.memoryAllocator, y, u, v); + + int yStride = width; + int uvStride = (yStride + 1) >> 1; + + var it = new Vp8EncIterator(this.YTop, this.UvTop, this.Nz, this.MbInfo, this.Preds, this.TopDerr, this.Mbw, this.Mbh); + int[] alphas = new int[WebpConstants.MaxAlpha + 1]; + this.alpha = this.MacroBlockAnalysis(width, height, it, y, u, v, yStride, uvStride, alphas, out this.uvAlpha); + int totalMb = this.Mbw * this.Mbw; + this.alpha /= totalMb; + this.uvAlpha /= totalMb; + + // Analysis is done, proceed to actual encoding. + this.SegmentHeader = new Vp8EncSegmentHeader(4); + this.AssignSegments(alphas); + this.SetLoopParams(this.quality); + + // Initialize the bitwriter. + int averageBytesPerMacroBlock = this.averageBytesPerMb[this.BaseQuant >> 4]; + int expectedSize = this.Mbw * this.Mbh * averageBytesPerMacroBlock; + this.bitWriter = new Vp8BitWriter(expectedSize, this); + + // TODO: EncodeAlpha(); + // Stats-collection loop. + this.StatLoop(width, height, yStride, uvStride); + it.Init(); + it.InitFilter(); + do + { + bool dontUseSkip = !this.Proba.UseSkipProba; + + var info = new Vp8ModeScore(); + it.Import(y, u, v, yStride, uvStride, width, height, false); + + // Warning! order is important: first call VP8Decimate() and + // *then* decide how to code the skip decision if there's one. + if (!this.Decimate(it, ref info, this.rdOptLevel) || dontUseSkip) + { + this.CodeResiduals(it, info); + } + else + { + it.ResetAfterSkip(); + } + + it.SaveBoundary(); + } + while (it.Next()); + + // Store filter stats. + this.AdjustFilterStrength(); + + // Write bytes from the bitwriter buffer to the stream. + image.Metadata.SyncProfiles(); + this.bitWriter.WriteEncodedImageToStream(stream, image.Metadata.ExifProfile, (uint)width, (uint)height); + } + + /// + public void Dispose() + { + this.Y.Dispose(); + this.U.Dispose(); + this.V.Dispose(); + } + + /// + /// Only collect statistics(number of skips, token usage, ...). + /// This is used for deciding optimal probabilities. It also modifies the + /// quantizer value if some target (size, PSNR) was specified. + /// + private void StatLoop(int width, int height, int yStride, int uvStride) + { + int targetSize = 0; // TODO: target size is hardcoded. + float targetPsnr = 0.0f; // TODO: targetPsnr is hardcoded. + bool doSearch = targetSize > 0 || targetPsnr > 0; + bool fastProbe = (this.method == 0 || this.method == WebpEncodingMethod.Level3) && !doSearch; + int numPassLeft = this.entropyPasses; + Vp8RdLevel rdOpt = this.method >= WebpEncodingMethod.Level3 || doSearch ? Vp8RdLevel.RdOptBasic : Vp8RdLevel.RdOptNone; + int nbMbs = this.Mbw * this.Mbh; + + var stats = new PassStats(targetSize, targetPsnr, QMin, QMax, this.quality); + this.Proba.ResetTokenStats(); + + // Fast mode: quick analysis pass over few mbs. Better than nothing. + if (fastProbe) + { + if (this.method == WebpEncodingMethod.Level3) + { + // We need more stats for method 3 to be reliable. + nbMbs = nbMbs > 200 ? nbMbs >> 1 : 100; + } + else + { + nbMbs = nbMbs > 200 ? nbMbs >> 2 : 50; + } + } + + while (numPassLeft-- > 0) + { + bool isLastPass = (MathF.Abs(stats.Dq) <= DqLimit) || (numPassLeft == 0) || (this.maxI4HeaderBits == 0); + long sizeP0 = this.OneStatPass(width, height, yStride, uvStride, rdOpt, nbMbs, stats); + if (sizeP0 == 0) + { + return; + } + + if (this.maxI4HeaderBits > 0 && sizeP0 > (long)Partition0SizeLimit) + { + ++numPassLeft; + this.maxI4HeaderBits >>= 1; // strengthen header bit limitation... + continue; // ...and start over + } + + if (isLastPass) + { + break; + } + + // If no target size: just do several pass without changing 'q' + if (doSearch) + { + stats.ComputeNextQ(); + if (MathF.Abs(stats.Dq) <= DqLimit) + { + break; + } + } + } + + if (!doSearch || !stats.DoSizeSearch) + { + // Need to finalize probas now, since it wasn't done during the search. + this.Proba.FinalizeSkipProba(this.Mbw, this.Mbh); + this.Proba.FinalizeTokenProbas(); + } + + // Finalize costs. + this.Proba.CalculateLevelCosts(); + } + + private long OneStatPass(int width, int height, int yStride, int uvStride, Vp8RdLevel rdOpt, int nbMbs, PassStats stats) + { + Span y = this.Y.GetSpan(); + Span u = this.U.GetSpan(); + Span v = this.V.GetSpan(); + var it = new Vp8EncIterator(this.YTop, this.UvTop, this.Nz, this.MbInfo, this.Preds, this.TopDerr, this.Mbw, this.Mbh); + long size = 0; + long sizeP0 = 0; + long distortion = 0; + long pixelCount = nbMbs * 384; + + it.Init(); + this.SetLoopParams(stats.Q); + do + { + var info = new Vp8ModeScore(); + it.Import(y, u, v, yStride, uvStride, width, height, false); + if (this.Decimate(it, ref info, rdOpt)) + { + // Just record the number of skips and act like skipProba is not used. + ++this.Proba.NbSkip; + } + + this.RecordResiduals(it, info); + size += info.R + info.H; + sizeP0 += info.H; + distortion += info.D; + + it.SaveBoundary(); + } + while (it.Next() && --nbMbs > 0); + + sizeP0 += this.SegmentHeader.Size; + if (stats.DoSizeSearch) + { + size += this.Proba.FinalizeSkipProba(this.Mbw, this.Mbh); + size += this.Proba.FinalizeTokenProbas(); + size = ((size + sizeP0 + 1024) >> 11) + HeaderSizeEstimate; + stats.Value = size; + } + else + { + stats.Value = GetPsnr(distortion, pixelCount); + } + + return sizeP0; + } + + private void SetLoopParams(float q) + { + // Setup segment quantizations and filters. + this.SetSegmentParams(q); + + // Compute segment probabilities. + this.SetSegmentProbas(); + + this.ResetStats(); + } + + private void AdjustFilterStrength() + { + if (this.filterStrength > 0) + { + int maxLevel = 0; + for (int s = 0; s < WebpConstants.NumMbSegments; s++) + { + Vp8SegmentInfo dqm = this.SegmentInfos[s]; + + // this '>> 3' accounts for some inverse WHT scaling + int delta = (dqm.MaxEdge * dqm.Y2.Q[1]) >> 3; + int level = this.FilterStrengthFromDelta(this.FilterHeader.Sharpness, delta); + if (level > dqm.FStrength) + { + dqm.FStrength = level; + } + + if (maxLevel < dqm.FStrength) + { + maxLevel = dqm.FStrength; + } + } + + this.FilterHeader.FilterLevel = maxLevel; + } + } + + private void ResetBoundaryPredictions() + { + Span top = this.Preds.AsSpan(); // original source top starts at: enc->preds_ - enc->preds_w_ + Span left = this.Preds.AsSpan(this.PredsWidth - 1); + for (int i = 0; i < 4 * this.Mbw; i++) + { + top[i] = (int)IntraPredictionMode.DcPrediction; + } + + for (int i = 0; i < 4 * this.Mbh; i++) + { + left[i * this.PredsWidth] = (int)IntraPredictionMode.DcPrediction; + } + + int predsW = (4 * this.Mbw) + 1; + int predsH = (4 * this.Mbh) + 1; + int predsSize = predsW * predsH; + this.Preds.AsSpan(predsSize + this.PredsWidth - 4, 4).Fill(0); + + this.Nz[0] = 0; // constant + } + + // Simplified k-Means, to assign Nb segments based on alpha-histogram. + private void AssignSegments(int[] alphas) + { + int nb = this.SegmentHeader.NumSegments < NumMbSegments ? this.SegmentHeader.NumSegments : NumMbSegments; + int[] centers = new int[NumMbSegments]; + int weightedAverage = 0; + int[] map = new int[WebpConstants.MaxAlpha + 1]; + int n, k; + int[] accum = new int[NumMbSegments]; + int[] distAccum = new int[NumMbSegments]; + + // Bracket the input. + for (n = 0; n <= WebpConstants.MaxAlpha && alphas[n] == 0; ++n) + { + } + + int minA = n; + for (n = WebpConstants.MaxAlpha; n > minA && alphas[n] == 0; --n) + { + } + + int maxA = n; + int rangeA = maxA - minA; + + // Spread initial centers evenly. + for (k = 0, n = 1; k < nb; ++k, n += 2) + { + centers[k] = minA + (n * rangeA / (2 * nb)); + } + + for (k = 0; k < MaxItersKMeans; ++k) + { + // Reset stats. + for (n = 0; n < nb; ++n) + { + accum[n] = 0; + distAccum[n] = 0; + } + + // Assign nearest center for each 'a' + n = 0; // track the nearest center for current 'a' + int a; + for (a = minA; a <= maxA; ++a) + { + if (alphas[a] != 0) + { + while (n + 1 < nb && Math.Abs(a - centers[n + 1]) < Math.Abs(a - centers[n])) + { + n++; + } + + map[a] = n; + + // Accumulate contribution into best centroid. + distAccum[n] += a * alphas[a]; + accum[n] += alphas[a]; + } + } + + // All point are classified. Move the centroids to the center of their respective cloud. + int displaced = 0; + weightedAverage = 0; + int totalWeight = 0; + for (n = 0; n < nb; ++n) + { + if (accum[n] != 0) + { + int newCenter = (distAccum[n] + (accum[n] / 2)) / accum[n]; + displaced += Math.Abs(centers[n] - newCenter); + centers[n] = newCenter; + weightedAverage += newCenter * accum[n]; + totalWeight += accum[n]; + } + } + + weightedAverage = (weightedAverage + (totalWeight / 2)) / totalWeight; + if (displaced < 5) + { + break; // no need to keep on looping... + } + } + + // Map each original value to the closest centroid + for (n = 0; n < this.Mbw * this.Mbh; ++n) + { + Vp8MacroBlockInfo mb = this.MbInfo[n]; + int alpha = mb.Alpha; + mb.Segment = map[alpha]; + mb.Alpha = centers[map[alpha]]; + } + + // TODO: add possibility for SmoothSegmentMap + this.SetSegmentAlphas(centers, weightedAverage); + } + + private void SetSegmentAlphas(int[] centers, int mid) + { + int nb = this.SegmentHeader.NumSegments; + Vp8SegmentInfo[] dqm = this.SegmentInfos; + int min = centers[0], max = centers[0]; + int n; + + if (nb > 1) + { + for (n = 0; n < nb; ++n) + { + if (min > centers[n]) + { + min = centers[n]; + } + + if (max < centers[n]) + { + max = centers[n]; + } + } + } + + if (max == min) + { + max = min + 1; + } + + for (n = 0; n < nb; ++n) + { + int alpha = 255 * (centers[n] - mid) / (max - min); + int beta = 255 * (centers[n] - min) / (max - min); + dqm[n].Alpha = Numerics.Clamp(alpha, -127, 127); + dqm[n].Beta = Numerics.Clamp(beta, 0, 255); + } + } + + private void SetSegmentParams(float quality) + { + int nb = this.SegmentHeader.NumSegments; + Vp8SegmentInfo[] dqm = this.SegmentInfos; + double amp = WebpConstants.SnsToDq * this.spatialNoiseShaping / 100.0d / 128.0d; + double cBase = QualityToCompression(quality / 100.0d); + for (int i = 0; i < nb; i++) + { + // We modulate the base coefficient to accommodate for the quantization + // susceptibility and allow denser segments to be quantized more. + double expn = 1.0d - (amp * dqm[i].Alpha); + double c = Math.Pow(cBase, expn); + int q = (int)(127.0d * (1.0d - c)); + dqm[i].Quant = Numerics.Clamp(q, 0, 127); + } + + // Purely indicative in the bitstream (except for the 1-segment case). + this.BaseQuant = dqm[0].Quant; + + // uvAlpha is normally spread around ~60. The useful range is + // typically ~30 (quite bad) to ~100 (ok to decimate UV more). + // We map it to the safe maximal range of MAX/MIN_DQ_UV for dq_uv. + this.DqUvAc = (this.uvAlpha - WebpConstants.QuantEncMidAlpha) * (WebpConstants.QuantEncMaxDqUv - WebpConstants.QuantEncMinDqUv) / (WebpConstants.QuantEncMaxAlpha - WebpConstants.QuantEncMinAlpha); + + // We rescale by the user-defined strength of adaptation. + this.DqUvAc = this.DqUvAc * this.spatialNoiseShaping / 100; + + // and make it safe. + this.DqUvAc = Numerics.Clamp(this.DqUvAc, WebpConstants.QuantEncMinDqUv, WebpConstants.QuantEncMaxDqUv); + + // We also boost the dc-uv-quant a little, based on sns-strength, since + // U/V channels are quite more reactive to high quants (flat DC-blocks tend to appear, and are unpleasant). + this.DqUvDc = -4 * this.spatialNoiseShaping / 100; + this.DqUvDc = Numerics.Clamp(this.DqUvDc, -15, 15); // 4bit-signed max allowed. + + this.DqY1Dc = 0; + this.DqY2Dc = 0; + this.DqY2Ac = 0; + + // Initialize segments' filtering. + this.SetupFilterStrength(); + + this.SetupMatrices(dqm); + } + + private void SetupFilterStrength() + { + int filterSharpness = 0; // TODO: filterSharpness is hardcoded + int filterType = 1; // TODO: filterType is hardcoded + + // level0 is in [0..500]. Using '-f 50' as filter_strength is mid-filtering. + int level0 = 5 * this.filterStrength; + for (int i = 0; i < WebpConstants.NumMbSegments; i++) + { + Vp8SegmentInfo m = this.SegmentInfos[i]; + + // We focus on the quantization of AC coeffs. + int qstep = WebpLookupTables.AcTable[Numerics.Clamp(m.Quant, 0, 127)] >> 2; + int baseStrength = this.FilterStrengthFromDelta(this.FilterHeader.Sharpness, qstep); + + // Segments with lower complexity ('beta') will be less filtered. + int f = baseStrength * level0 / (256 + m.Beta); + m.FStrength = f < WebpConstants.FilterStrengthCutoff ? 0 : f > 63 ? 63 : f; + } + + // We record the initial strength (mainly for the case of 1-segment only). + this.FilterHeader.FilterLevel = this.SegmentInfos[0].FStrength; + this.FilterHeader.Simple = filterType == 0; + this.FilterHeader.Sharpness = filterSharpness; + } + + private void SetSegmentProbas() + { + int[] p = new int[NumMbSegments]; + int n; + + for (n = 0; n < this.Mbw * this.Mbh; ++n) + { + Vp8MacroBlockInfo mb = this.MbInfo[n]; + ++p[mb.Segment]; + } + + if (this.SegmentHeader.NumSegments > 1) + { + byte[] probas = this.Proba.Segments; + probas[0] = (byte)GetProba(p[0] + p[1], p[2] + p[3]); + probas[1] = (byte)GetProba(p[0], p[1]); + probas[2] = (byte)GetProba(p[2], p[3]); + + this.SegmentHeader.UpdateMap = probas[0] != 255 || probas[1] != 255 || probas[2] != 255; + if (!this.SegmentHeader.UpdateMap) + { + this.ResetSegments(); + } + + this.SegmentHeader.Size = (p[0] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(0, probas[1]))) + + (p[1] * (LossyUtils.Vp8BitCost(0, probas[0]) + LossyUtils.Vp8BitCost(1, probas[1]))) + + (p[2] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(0, probas[2]))) + + (p[3] * (LossyUtils.Vp8BitCost(1, probas[0]) + LossyUtils.Vp8BitCost(1, probas[2]))); + } + else + { + this.SegmentHeader.UpdateMap = false; + this.SegmentHeader.Size = 0; + } + } + + private void ResetSegments() + { + int n; + for (n = 0; n < this.Mbw * this.Mbh; ++n) + { + this.MbInfo[n].Segment = 0; + } + } + + private void ResetStats() + { + Vp8EncProba proba = this.Proba; + proba.CalculateLevelCosts(); + proba.NbSkip = 0; + } + + private void SetupMatrices(Vp8SegmentInfo[] dqm) + { + int tlambdaScale = this.method >= WebpEncodingMethod.Default ? this.spatialNoiseShaping : 0; + for (int i = 0; i < dqm.Length; i++) + { + Vp8SegmentInfo m = dqm[i]; + int q = m.Quant; + + m.Y1 = new Vp8Matrix(); + m.Y2 = new Vp8Matrix(); + m.Uv = new Vp8Matrix(); + + m.Y1.Q[0] = WebpLookupTables.DcTable[Numerics.Clamp(q + this.DqY1Dc, 0, 127)]; + m.Y1.Q[1] = WebpLookupTables.AcTable[Numerics.Clamp(q, 0, 127)]; + + m.Y2.Q[0] = (ushort)(WebpLookupTables.DcTable[Numerics.Clamp(q + this.DqY2Dc, 0, 127)] * 2); + m.Y2.Q[1] = WebpLookupTables.AcTable2[Numerics.Clamp(q + this.DqY2Ac, 0, 127)]; + + m.Uv.Q[0] = WebpLookupTables.DcTable[Numerics.Clamp(q + this.DqUvDc, 0, 117)]; + m.Uv.Q[1] = WebpLookupTables.AcTable[Numerics.Clamp(q + this.DqUvAc, 0, 127)]; + + int qi4 = m.Y1.Expand(0); + int qi16 = m.Y2.Expand(1); + int quv = m.Uv.Expand(2); + + m.LambdaI16 = 3 * qi16 * qi16; + m.LambdaI4 = (3 * qi4 * qi4) >> 7; + m.LambdaUv = (3 * quv * quv) >> 6; + m.LambdaMode = (1 * qi4 * qi4) >> 7; + m.TLambda = (tlambdaScale * qi4) >> 5; + + // none of these constants should be < 1. + m.LambdaI16 = m.LambdaI16 < 1 ? 1 : m.LambdaI16; + m.LambdaI4 = m.LambdaI4 < 1 ? 1 : m.LambdaI4; + m.LambdaUv = m.LambdaUv < 1 ? 1 : m.LambdaUv; + m.LambdaMode = m.LambdaMode < 1 ? 1 : m.LambdaMode; + m.TLambda = m.TLambda < 1 ? 1 : m.TLambda; + + m.MinDisto = 20 * m.Y1.Q[0]; + m.MaxEdge = 0; + + m.I4Penalty = 1000 * qi4 * qi4; + } + } + + private int MacroBlockAnalysis(int width, int height, Vp8EncIterator it, Span y, Span u, Span v, int yStride, int uvStride, int[] alphas, out int uvAlpha) + { + int alpha = 0; + uvAlpha = 0; + if (!it.IsDone()) + { + do + { + it.Import(y, u, v, yStride, uvStride, width, height, true); + int bestAlpha = this.MbAnalyze(it, alphas, out int bestUvAlpha); + + // Accumulate for later complexity analysis. + alpha += bestAlpha; + uvAlpha += bestUvAlpha; + } + while (it.Next()); + } + + return alpha; + } + + private int MbAnalyze(Vp8EncIterator it, int[] alphas, out int bestUvAlpha) + { + it.SetIntra16Mode(0); // default: Intra16, DC_PRED + it.SetSkip(false); // not skipped. + it.SetSegment(0); // default segment, spec-wise. + + int bestAlpha; + if (this.method <= WebpEncodingMethod.Level1) + { + bestAlpha = it.FastMbAnalyze(this.quality); + } + else + { + bestAlpha = it.MbAnalyzeBestIntra16Mode(); + if (this.method >= WebpEncodingMethod.Level5) + { + // We go and make a fast decision for intra4/intra16. + // It's usually not a good and definitive pick, but helps seeding the stats about level bit-cost. + bestAlpha = it.MbAnalyzeBestIntra4Mode(bestAlpha); + } + } + + bestUvAlpha = it.MbAnalyzeBestUvMode(); + + // Final susceptibility mix. + bestAlpha = ((3 * bestAlpha) + bestUvAlpha + 2) >> 2; + bestAlpha = FinalAlphaValue(bestAlpha); + alphas[bestAlpha]++; + it.CurrentMacroBlockInfo.Alpha = bestAlpha; // For later remapping. + + return bestAlpha; // Mixed susceptibility (not just luma). + } + + private bool Decimate(Vp8EncIterator it, ref Vp8ModeScore rd, Vp8RdLevel rdOpt) + { + rd.InitScore(); + + // We can perform predictions for Luma16x16 and Chroma8x8 already. + // Luma4x4 predictions needs to be done as-we-go. + it.MakeLuma16Preds(); + it.MakeChroma8Preds(); + + if (rdOpt > Vp8RdLevel.RdOptNone) + { + QuantEnc.PickBestIntra16(it, ref rd, this.SegmentInfos, this.Proba); + if (this.method >= WebpEncodingMethod.Level2) + { + QuantEnc.PickBestIntra4(it, ref rd, this.SegmentInfos, this.Proba, this.maxI4HeaderBits); + } + + QuantEnc.PickBestUv(it, ref rd, this.SegmentInfos, this.Proba); + } + else + { + // At this point we have heuristically decided intra16 / intra4. + // For method >= 2, pick the best intra4/intra16 based on SSE (~tad slower). + // For method <= 1, we don't re-examine the decision but just go ahead with + // quantization/reconstruction. + QuantEnc.RefineUsingDistortion(it, this.SegmentInfos, rd, this.method >= WebpEncodingMethod.Level2, this.method >= WebpEncodingMethod.Level1, this.MbHeaderLimit); + } + + bool isSkipped = rd.Nz == 0; + it.SetSkip(isSkipped); + + return isSkipped; + } + + private void CodeResiduals(Vp8EncIterator it, Vp8ModeScore rd) + { + int x, y, ch; + var residual = new Vp8Residual(); + bool i16 = it.CurrentMacroBlockInfo.MacroBlockType == Vp8MacroBlockType.I16X16; + int segment = it.CurrentMacroBlockInfo.Segment; + + it.NzToBytes(); + + int pos1 = this.bitWriter.NumBytes(); + if (i16) + { + residual.Init(0, 1, this.Proba); + residual.SetCoeffs(rd.YDcLevels); + int res = this.bitWriter.PutCoeffs(it.TopNz[8] + it.LeftNz[8], residual); + it.TopNz[8] = it.LeftNz[8] = res; + residual.Init(1, 0, this.Proba); + } + else + { + residual.Init(0, 3, this.Proba); + } + + // luma-AC + for (y = 0; y < 4; y++) + { + for (x = 0; x < 4; x++) + { + int ctx = it.TopNz[x] + it.LeftNz[y]; + Span coeffs = rd.YAcLevels.AsSpan(16 * (x + (y * 4)), 16); + residual.SetCoeffs(coeffs); + int res = this.bitWriter.PutCoeffs(ctx, residual); + it.TopNz[x] = it.LeftNz[y] = res; + } + } + + int pos2 = this.bitWriter.NumBytes(); + + // U/V + residual.Init(0, 2, this.Proba); + for (ch = 0; ch <= 2; ch += 2) + { + for (y = 0; y < 2; y++) + { + for (x = 0; x < 2; x++) + { + int ctx = it.TopNz[4 + ch + x] + it.LeftNz[4 + ch + y]; + residual.SetCoeffs(rd.UvLevels.AsSpan(16 * ((ch * 2) + x + (y * 2)), 16)); + int res = this.bitWriter.PutCoeffs(ctx, residual); + it.TopNz[4 + ch + x] = it.LeftNz[4 + ch + y] = res; + } + } + } + + int pos3 = this.bitWriter.NumBytes(); + it.LumaBits = pos2 - pos1; + it.UvBits = pos3 - pos2; + it.BitCount[segment, i16 ? 1 : 0] += it.LumaBits; + it.BitCount[segment, 2] += it.UvBits; + it.BytesToNz(); + } + + /// + /// Same as CodeResiduals, but doesn't actually write anything. + /// Instead, it just records the event distribution. + /// + private void RecordResiduals(Vp8EncIterator it, Vp8ModeScore rd) + { + int x, y, ch; + var residual = new Vp8Residual(); + bool i16 = it.CurrentMacroBlockInfo.MacroBlockType == Vp8MacroBlockType.I16X16; + + it.NzToBytes(); + + if (i16) + { + // i16x16 + residual.Init(0, 1, this.Proba); + residual.SetCoeffs(rd.YDcLevels); + int res = residual.RecordCoeffs(it.TopNz[8] + it.LeftNz[8]); + it.TopNz[8] = res; + it.LeftNz[8] = res; + residual.Init(1, 0, this.Proba); + } + else + { + residual.Init(0, 3, this.Proba); + } + + // luma-AC + for (y = 0; y < 4; y++) + { + for (x = 0; x < 4; x++) + { + int ctx = it.TopNz[x] + it.LeftNz[y]; + Span coeffs = rd.YAcLevels.AsSpan(16 * (x + (y * 4)), 16); + residual.SetCoeffs(coeffs); + int res = residual.RecordCoeffs(ctx); + it.TopNz[x] = res; + it.LeftNz[y] = res; + } + } + + // U/V + residual.Init(0, 2, this.Proba); + for (ch = 0; ch <= 2; ch += 2) + { + for (y = 0; y < 2; y++) + { + for (x = 0; x < 2; x++) + { + int ctx = it.TopNz[4 + ch + x] + it.LeftNz[4 + ch + y]; + residual.SetCoeffs(rd.UvLevels.AsSpan(16 * ((ch * 2) + x + (y * 2)), 16)); + int res = residual.RecordCoeffs(ctx); + it.TopNz[4 + ch + x] = res; + it.LeftNz[4 + ch + y] = res; + } + } + } + + it.BytesToNz(); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int FinalAlphaValue(int alpha) + { + alpha = WebpConstants.MaxAlpha - alpha; + return Numerics.Clamp(alpha, 0, WebpConstants.MaxAlpha); + } + + /// + /// We want to emulate jpeg-like behaviour where the expected "good" quality + /// is around q=75. Internally, our "good" middle is around c=50. So we + /// map accordingly using linear piece-wise function + /// + [MethodImpl(InliningOptions.ShortMethod)] + private static double QualityToCompression(double c) + { + double linearC = c < 0.75 ? c * (2.0d / 3.0d) : (2.0d * c) - 1.0d; + + // The file size roughly scales as pow(quantizer, 3.). Actually, the + // exponent is somewhere between 2.8 and 3.2, but we're mostly interested + // in the mid-quant range. So we scale the compressibility inversely to + // this power-law: quant ~= compression ^ 1/3. This law holds well for + // low quant. Finer modeling for high-quant would make use of AcTable[] + // more explicitly. + double v = Math.Pow(linearC, 1 / 3.0d); + + return v; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private int FilterStrengthFromDelta(int sharpness, int delta) + { + int pos = delta < WebpConstants.MaxDelzaSize ? delta : WebpConstants.MaxDelzaSize - 1; + return WebpLookupTables.LevelsFromDelta[sharpness, pos]; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static double GetPsnr(long mse, long size) => mse > 0 && size > 0 ? 10.0f * Math.Log10(255.0f * 255.0f * size / mse) : 99; + + [MethodImpl(InliningOptions.ShortMethod)] + private static int GetProba(int a, int b) + { + int total = a + b; + return total == 0 ? 255 // that's the default probability. + : ((255 * a) + (total / 2)) / total; // rounded proba + } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoding.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoding.cs new file mode 100644 index 000000000..f8b4853e2 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoding.cs @@ -0,0 +1,655 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + /// + /// Methods for encoding a VP8 frame. + /// + internal static class Vp8Encoding + { + private const int KC1 = 20091 + (1 << 16); + + private const int KC2 = 35468; + + private static readonly byte[] Clip1 = new byte[255 + 510 + 1]; // clips [-255,510] to [0,255] + + private const int I16DC16 = 0 * 16 * WebpConstants.Bps; + + private const int I16TM16 = I16DC16 + 16; + + private const int I16VE16 = 1 * 16 * WebpConstants.Bps; + + private const int I16HE16 = I16VE16 + 16; + + private const int C8DC8 = 2 * 16 * WebpConstants.Bps; + + private const int C8TM8 = C8DC8 + (1 * 16); + + private const int C8VE8 = (2 * 16 * WebpConstants.Bps) + (8 * WebpConstants.Bps); + + private const int C8HE8 = C8VE8 + (1 * 16); + + public static readonly int[] Vp8I16ModeOffsets = { I16DC16, I16TM16, I16VE16, I16HE16 }; + + public static readonly int[] Vp8UvModeOffsets = { C8DC8, C8TM8, C8VE8, C8HE8 }; + + private const int I4DC4 = (3 * 16 * WebpConstants.Bps) + 0; + + private const int I4TM4 = I4DC4 + 4; + + private const int I4VE4 = I4DC4 + 8; + + private const int I4HE4 = I4DC4 + 12; + + private const int I4RD4 = I4DC4 + 16; + + private const int I4VR4 = I4DC4 + 20; + + private const int I4LD4 = I4DC4 + 24; + + private const int I4VL4 = I4DC4 + 28; + + private const int I4HD4 = (3 * 16 * WebpConstants.Bps) + (4 * WebpConstants.Bps); + + private const int I4HU4 = I4HD4 + 4; + + public static readonly int[] Vp8I4ModeOffsets = { I4DC4, I4TM4, I4VE4, I4HE4, I4RD4, I4VR4, I4LD4, I4VL4, I4HD4, I4HU4 }; + + static Vp8Encoding() + { + for (int i = -255; i <= 255 + 255; i++) + { + Clip1[255 + i] = Clip8b(i); + } + } + + public static void ITransform(Span reference, Span input, Span dst, bool doTwo) + { + ITransformOne(reference, input, dst); + if (doTwo) + { + ITransformOne(reference.Slice(4), input.Slice(16), dst.Slice(4)); + } + } + + public static void ITransformOne(Span reference, Span input, Span dst) + { + int i; +#pragma warning disable SA1312 // Variable names should begin with lower-case letter + int[] C = new int[4 * 4]; +#pragma warning restore SA1312 // Variable names should begin with lower-case letter + Span tmp = C.AsSpan(); + for (i = 0; i < 4; i++) + { + // vertical pass. + int a = input[0] + input[8]; + int b = input[0] - input[8]; + int c = Mul(input[4], KC2) - Mul(input[12], KC1); + int d = Mul(input[4], KC1) + Mul(input[12], KC2); + tmp[0] = a + d; + tmp[1] = b + c; + tmp[2] = b - c; + tmp[3] = a - d; + tmp = tmp.Slice(4); + input = input.Slice(1); + } + + tmp = C.AsSpan(); + for (i = 0; i < 4; i++) + { + // horizontal pass. + int dc = tmp[0] + 4; + int a = dc + tmp[8]; + int b = dc - tmp[8]; + int c = Mul(tmp[4], KC2) - Mul(tmp[12], KC1); + int d = Mul(tmp[4], KC1) + Mul(tmp[12], KC2); + Store(dst, reference, 0, i, a + d); + Store(dst, reference, 1, i, b + c); + Store(dst, reference, 2, i, b - c); + Store(dst, reference, 3, i, a - d); + tmp = tmp.Slice(1); + } + } + + public static void FTransform2(Span src, Span reference, Span output, Span output2) + { + FTransform(src, reference, output); + FTransform(src.Slice(4), reference.Slice(4), output2); + } + + public static void FTransform(Span src, Span reference, Span output) + { + int i; + int[] tmp = new int[16]; + int srcIdx = 0; + int refIdx = 0; + for (i = 0; i < 4; i++) + { + int d0 = src[srcIdx] - reference[refIdx]; // 9bit dynamic range ([-255,255]) + int d1 = src[srcIdx + 1] - reference[refIdx + 1]; + int d2 = src[srcIdx + 2] - reference[refIdx + 2]; + int d3 = src[srcIdx + 3] - reference[refIdx + 3]; + int a0 = d0 + d3; // 10b [-510,510] + int a1 = d1 + d2; + int a2 = d1 - d2; + int a3 = d0 - d3; + tmp[0 + (i * 4)] = (a0 + a1) * 8; // 14b [-8160,8160] + tmp[1 + (i * 4)] = ((a2 * 2217) + (a3 * 5352) + 1812) >> 9; // [-7536,7542] + tmp[2 + (i * 4)] = (a0 - a1) * 8; + tmp[3 + (i * 4)] = ((a3 * 2217) - (a2 * 5352) + 937) >> 9; + + srcIdx += WebpConstants.Bps; + refIdx += WebpConstants.Bps; + } + + for (i = 0; i < 4; i++) + { + int a0 = tmp[0 + i] + tmp[12 + i]; // 15b + int a1 = tmp[4 + i] + tmp[8 + i]; + int a2 = tmp[4 + i] - tmp[8 + i]; + int a3 = tmp[0 + i] - tmp[12 + i]; + output[0 + i] = (short)((a0 + a1 + 7) >> 4); // 12b + output[4 + i] = (short)((((a2 * 2217) + (a3 * 5352) + 12000) >> 16) + (a3 != 0 ? 1 : 0)); + output[8 + i] = (short)((a0 - a1 + 7) >> 4); + output[12 + i] = (short)(((a3 * 2217) - (a2 * 5352) + 51000) >> 16); + } + } + + public static void FTransformWht(Span input, Span output) + { + int[] tmp = new int[16]; + int i; + int inputIdx = 0; + for (i = 0; i < 4; i++) + { + int a0 = input[inputIdx + (0 * 16)] + input[inputIdx + (2 * 16)]; // 13b + int a1 = input[inputIdx + (1 * 16)] + input[inputIdx + (3 * 16)]; + int a2 = input[inputIdx + (1 * 16)] - input[inputIdx + (3 * 16)]; + int a3 = input[inputIdx + (0 * 16)] - input[inputIdx + (2 * 16)]; + tmp[0 + (i * 4)] = a0 + a1; // 14b + tmp[1 + (i * 4)] = a3 + a2; + tmp[2 + (i * 4)] = a3 - a2; + tmp[3 + (i * 4)] = a0 - a1; + + inputIdx += 64; + } + + for (i = 0; i < 4; i++) + { + int a0 = tmp[0 + i] + tmp[8 + i]; // 15b + int a1 = tmp[4 + i] + tmp[12 + i]; + int a2 = tmp[4 + i] - tmp[12 + i]; + int a3 = tmp[0 + i] - tmp[8 + i]; + int b0 = a0 + a1; // 16b + int b1 = a3 + a2; + int b2 = a3 - a2; + int b3 = a0 - a1; + output[0 + i] = (short)(b0 >> 1); // 15b + output[4 + i] = (short)(b1 >> 1); + output[8 + i] = (short)(b2 >> 1); + output[12 + i] = (short)(b3 >> 1); + } + } + + // luma 16x16 prediction (paragraph 12.3). + public static void EncPredLuma16(Span dst, Span left, Span top) + { + DcMode(dst.Slice(I16DC16), left, top, 16, 16, 5); + VerticalPred(dst.Slice(I16VE16), top, 16); + HorizontalPred(dst.Slice(I16HE16), left, 16); + TrueMotion(dst.Slice(I16TM16), left, top, 16); + } + + // Chroma 8x8 prediction (paragraph 12.2). + public static void EncPredChroma8(Span dst, Span left, Span top) + { + // U block. + DcMode(dst.Slice(C8DC8), left, top, 8, 8, 4); + VerticalPred(dst.Slice(C8VE8), top, 8); + HorizontalPred(dst.Slice(C8HE8), left, 8); + TrueMotion(dst.Slice(C8TM8), left, top, 8); + + // V block. + dst = dst.Slice(8); + if (top != null) + { + top = top.Slice(8); + } + + if (left != null) + { + left = left.Slice(16); + } + + DcMode(dst.Slice(C8DC8), left, top, 8, 8, 4); + VerticalPred(dst.Slice(C8VE8), top, 8); + HorizontalPred(dst.Slice(C8HE8), left, 8); + TrueMotion(dst.Slice(C8TM8), left, top, 8); + } + + // Left samples are top[-5 .. -2], top_left is top[-1], top are + // located at top[0..3], and top right is top[4..7] + public static void EncPredLuma4(Span dst, Span top, int topOffset) + { + Dc4(dst.Slice(I4DC4), top, topOffset); + Tm4(dst.Slice(I4TM4), top, topOffset); + Ve4(dst.Slice(I4VE4), top, topOffset); + He4(dst.Slice(I4HE4), top, topOffset); + Rd4(dst.Slice(I4RD4), top, topOffset); + Vr4(dst.Slice(I4VR4), top, topOffset); + Ld4(dst.Slice(I4LD4), top, topOffset); + Vl4(dst.Slice(I4VL4), top, topOffset); + Hd4(dst.Slice(I4HD4), top, topOffset); + Hu4(dst.Slice(I4HU4), top, topOffset); + } + + private static void VerticalPred(Span dst, Span top, int size) + { + if (top != null) + { + for (int j = 0; j < size; j++) + { + top.Slice(0, size).CopyTo(dst.Slice(j * WebpConstants.Bps)); + } + } + else + { + Fill(dst, 127, size); + } + } + + public static void HorizontalPred(Span dst, Span left, int size) + { + if (left != null) + { + left = left.Slice(1); // in the reference implementation, left starts at - 1. + for (int j = 0; j < size; j++) + { + dst.Slice(j * WebpConstants.Bps, size).Fill(left[j]); + } + } + else + { + Fill(dst, 129, size); + } + } + + public static void TrueMotion(Span dst, Span left, Span top, int size) + { + if (left != null) + { + if (top != null) + { + Span clip = Clip1.AsSpan(255 - left[0]); // left [0] instead of left[-1], original left starts at -1 + for (int y = 0; y < size; y++) + { + Span clipTable = clip.Slice(left[y + 1]); // left[y] + for (int x = 0; x < size; x++) + { + dst[x] = clipTable[top[x]]; + } + + dst = dst.Slice(WebpConstants.Bps); + } + } + else + { + HorizontalPred(dst, left, size); + } + } + else + { + // true motion without left samples (hence: with default 129 value) + // is equivalent to VE prediction where you just copy the top samples. + // Note that if top samples are not available, the default value is + // then 129, and not 127 as in the VerticalPred case. + if (top != null) + { + VerticalPred(dst, top, size); + } + else + { + Fill(dst, 129, size); + } + } + } + + private static void DcMode(Span dst, Span left, Span top, int size, int round, int shift) + { + int dc = 0; + int j; + if (top != null) + { + for (j = 0; j < size; j++) + { + dc += top[j]; + } + + if (left != null) + { + // top and left present. + left = left.Slice(1); // in the reference implementation, left starts at -1. + for (j = 0; j < size; j++) + { + dc += left[j]; + } + } + else + { + // top, but no left. + dc += dc; + } + + dc = (dc + round) >> shift; + } + else if (left != null) + { + // left but no top. + left = left.Slice(1); // in the reference implementation, left starts at -1. + for (j = 0; j < size; j++) + { + dc += left[j]; + } + + dc += dc; + dc = (dc + round) >> shift; + } + else + { + // no top, no left, nothing. + dc = 0x80; + } + + Fill(dst, dc, size); + } + + private static void Dc4(Span dst, Span top, int topOffset) + { + uint dc = 4; + int i; + for (i = 0; i < 4; i++) + { + dc += (uint)(top[topOffset + i] + top[topOffset - 5 + i]); + } + + Fill(dst, (int)(dc >> 3), 4); + } + + private static void Tm4(Span dst, Span top, int topOffset) + { + Span clip = Clip1.AsSpan(255 - top[topOffset - 1]); + for (int y = 0; y < 4; y++) + { + Span clipTable = clip.Slice(top[topOffset - 2 - y]); + for (int x = 0; x < 4; x++) + { + dst[x] = clipTable[top[topOffset + x]]; + } + + dst = dst.Slice(WebpConstants.Bps); + } + } + + private static void Ve4(Span dst, Span top, int topOffset) + { + // vertical + byte[] vals = + { + LossyUtils.Avg3(top[topOffset - 1], top[topOffset], top[topOffset + 1]), + LossyUtils.Avg3(top[topOffset], top[topOffset + 1], top[topOffset + 2]), + LossyUtils.Avg3(top[topOffset + 1], top[topOffset + 2], top[topOffset + 3]), + LossyUtils.Avg3(top[topOffset + 2], top[topOffset + 3], top[topOffset + 4]) + }; + + for (int i = 0; i < 4; i++) + { + vals.AsSpan().CopyTo(dst.Slice(i * WebpConstants.Bps)); + } + } + + private static void He4(Span dst, Span top, int topOffset) + { + // horizontal + byte x = top[topOffset - 1]; + byte i = top[topOffset - 2]; + byte j = top[topOffset - 3]; + byte k = top[topOffset - 4]; + byte l = top[topOffset - 5]; + + uint val = 0x01010101U * LossyUtils.Avg3(x, i, j); + BinaryPrimitives.WriteUInt32BigEndian(dst, val); + val = 0x01010101U * LossyUtils.Avg3(i, j, k); + BinaryPrimitives.WriteUInt32BigEndian(dst.Slice(1 * WebpConstants.Bps), val); + val = 0x01010101U * LossyUtils.Avg3(j, k, l); + BinaryPrimitives.WriteUInt32BigEndian(dst.Slice(2 * WebpConstants.Bps), val); + val = 0x01010101U * LossyUtils.Avg3(k, l, l); + BinaryPrimitives.WriteUInt32BigEndian(dst.Slice(3 * WebpConstants.Bps), val); + } + + private static void Rd4(Span dst, Span top, int topOffset) + { + byte x = top[topOffset - 1]; + byte i = top[topOffset - 2]; + byte j = top[topOffset - 3]; + byte k = top[topOffset - 4]; + byte l = top[topOffset - 5]; + byte a = top[topOffset]; + byte b = top[topOffset + 1]; + byte c = top[topOffset + 2]; + byte d = top[topOffset + 3]; + + LossyUtils.Dst(dst, 0, 3, LossyUtils.Avg3(j, k, l)); + byte ijk = LossyUtils.Avg3(i, j, k); + LossyUtils.Dst(dst, 0, 2, ijk); + LossyUtils.Dst(dst, 1, 3, ijk); + byte xij = LossyUtils.Avg3(x, i, j); + LossyUtils.Dst(dst, 0, 1, xij); + LossyUtils.Dst(dst, 1, 2, xij); + LossyUtils.Dst(dst, 2, 3, xij); + byte axi = LossyUtils.Avg3(a, x, i); + LossyUtils.Dst(dst, 0, 0, axi); + LossyUtils.Dst(dst, 1, 1, axi); + LossyUtils.Dst(dst, 2, 2, axi); + LossyUtils.Dst(dst, 3, 3, axi); + byte bax = LossyUtils.Avg3(b, a, x); + LossyUtils.Dst(dst, 1, 0, bax); + LossyUtils.Dst(dst, 2, 1, bax); + LossyUtils.Dst(dst, 3, 2, bax); + byte cba = LossyUtils.Avg3(c, b, a); + LossyUtils.Dst(dst, 2, 0, cba); + LossyUtils.Dst(dst, 3, 1, cba); + LossyUtils.Dst(dst, 3, 0, LossyUtils.Avg3(d, c, b)); + } + + private static void Vr4(Span dst, Span top, int topOffset) + { + byte x = top[topOffset - 1]; + byte i = top[topOffset - 2]; + byte j = top[topOffset - 3]; + byte k = top[topOffset - 4]; + byte a = top[topOffset]; + byte b = top[topOffset + 1]; + byte c = top[topOffset + 2]; + byte d = top[topOffset + 3]; + + byte xa = LossyUtils.Avg2(x, a); + LossyUtils.Dst(dst, 0, 0, xa); + LossyUtils.Dst(dst, 1, 2, xa); + byte ab = LossyUtils.Avg2(a, b); + LossyUtils.Dst(dst, 1, 0, ab); + LossyUtils.Dst(dst, 2, 2, ab); + byte bc = LossyUtils.Avg2(b, c); + LossyUtils.Dst(dst, 2, 0, bc); + LossyUtils.Dst(dst, 3, 2, bc); + LossyUtils.Dst(dst, 3, 0, LossyUtils.Avg2(c, d)); + LossyUtils.Dst(dst, 0, 3, LossyUtils.Avg3(k, j, i)); + LossyUtils.Dst(dst, 0, 2, LossyUtils.Avg3(j, i, x)); + byte ixa = LossyUtils.Avg3(i, x, a); + LossyUtils.Dst(dst, 0, 1, ixa); + LossyUtils.Dst(dst, 1, 3, ixa); + byte xab = LossyUtils.Avg3(x, a, b); + LossyUtils.Dst(dst, 1, 1, xab); + LossyUtils.Dst(dst, 2, 3, xab); + byte abc = LossyUtils.Avg3(a, b, c); + LossyUtils.Dst(dst, 2, 1, abc); + LossyUtils.Dst(dst, 3, 3, abc); + LossyUtils.Dst(dst, 3, 1, LossyUtils.Avg3(b, c, d)); + } + + private static void Ld4(Span dst, Span top, int topOffset) + { + byte a = top[topOffset + 0]; + byte b = top[topOffset + 1]; + byte c = top[topOffset + 2]; + byte d = top[topOffset + 3]; + byte e = top[topOffset + 4]; + byte f = top[topOffset + 5]; + byte g = top[topOffset + 6]; + byte h = top[topOffset + 7]; + + LossyUtils.Dst(dst, 0, 0, LossyUtils.Avg3(a, b, c)); + byte bcd = LossyUtils.Avg3(b, c, d); + LossyUtils.Dst(dst, 1, 0, bcd); + LossyUtils.Dst(dst, 0, 1, bcd); + byte cde = LossyUtils.Avg3(c, d, e); + LossyUtils.Dst(dst, 2, 0, cde); + LossyUtils.Dst(dst, 1, 1, cde); + LossyUtils.Dst(dst, 0, 2, cde); + byte def = LossyUtils.Avg3(d, e, f); + LossyUtils.Dst(dst, 3, 0, def); + LossyUtils.Dst(dst, 2, 1, def); + LossyUtils.Dst(dst, 1, 2, def); + LossyUtils.Dst(dst, 0, 3, def); + byte efg = LossyUtils.Avg3(e, f, g); + LossyUtils.Dst(dst, 3, 1, efg); + LossyUtils.Dst(dst, 2, 2, efg); + LossyUtils.Dst(dst, 1, 3, efg); + byte fgh = LossyUtils.Avg3(f, g, h); + LossyUtils.Dst(dst, 3, 2, fgh); + LossyUtils.Dst(dst, 2, 3, fgh); + LossyUtils.Dst(dst, 3, 3, LossyUtils.Avg3(g, h, h)); + } + + private static void Vl4(Span dst, Span top, int topOffset) + { + byte a = top[topOffset + 0]; + byte b = top[topOffset + 1]; + byte c = top[topOffset + 2]; + byte d = top[topOffset + 3]; + byte e = top[topOffset + 4]; + byte f = top[topOffset + 5]; + byte g = top[topOffset + 6]; + byte h = top[topOffset + 7]; + + LossyUtils.Dst(dst, 0, 0, LossyUtils.Avg2(a, b)); + byte bc = LossyUtils.Avg2(b, c); + LossyUtils.Dst(dst, 1, 0, bc); + LossyUtils.Dst(dst, 0, 2, bc); + byte cd = LossyUtils.Avg2(c, d); + LossyUtils.Dst(dst, 2, 0, cd); + LossyUtils.Dst(dst, 1, 2, cd); + byte de = LossyUtils.Avg2(d, e); + LossyUtils.Dst(dst, 3, 0, de); + LossyUtils.Dst(dst, 2, 2, de); + LossyUtils.Dst(dst, 0, 1, LossyUtils.Avg3(a, b, c)); + byte bcd = LossyUtils.Avg3(b, c, d); + LossyUtils.Dst(dst, 1, 1, bcd); + LossyUtils.Dst(dst, 0, 3, bcd); + byte cde = LossyUtils.Avg3(c, d, e); + LossyUtils.Dst(dst, 2, 1, cde); + LossyUtils.Dst(dst, 1, 3, cde); + byte def = LossyUtils.Avg3(d, e, f); + LossyUtils.Dst(dst, 3, 1, def); + LossyUtils.Dst(dst, 2, 3, def); + LossyUtils.Dst(dst, 3, 2, LossyUtils.Avg3(e, f, g)); + LossyUtils.Dst(dst, 3, 3, LossyUtils.Avg3(f, g, h)); + } + + private static void Hd4(Span dst, Span top, int topOffset) + { + byte x = top[topOffset - 1]; + byte i = top[topOffset - 2]; + byte j = top[topOffset - 3]; + byte k = top[topOffset - 4]; + byte l = top[topOffset - 5]; + byte a = top[topOffset]; + byte b = top[topOffset + 1]; + byte c = top[topOffset + 2]; + + byte ix = LossyUtils.Avg2(i, x); + LossyUtils.Dst(dst, 0, 0, ix); + LossyUtils.Dst(dst, 2, 1, ix); + byte ji = LossyUtils.Avg2(j, i); + LossyUtils.Dst(dst, 0, 1, ji); + LossyUtils.Dst(dst, 2, 2, ji); + byte kj = LossyUtils.Avg2(k, j); + LossyUtils.Dst(dst, 0, 2, kj); + LossyUtils.Dst(dst, 2, 3, kj); + LossyUtils.Dst(dst, 0, 3, LossyUtils.Avg2(l, k)); + LossyUtils.Dst(dst, 3, 0, LossyUtils.Avg3(a, b, c)); + LossyUtils.Dst(dst, 2, 0, LossyUtils.Avg3(x, a, b)); + byte ixa = LossyUtils.Avg3(i, x, a); + LossyUtils.Dst(dst, 1, 0, ixa); + LossyUtils.Dst(dst, 3, 1, ixa); + byte jix = LossyUtils.Avg3(j, i, x); + LossyUtils.Dst(dst, 1, 1, jix); + LossyUtils.Dst(dst, 3, 2, jix); + byte kji = LossyUtils.Avg3(k, j, i); + LossyUtils.Dst(dst, 1, 2, kji); + LossyUtils.Dst(dst, 3, 3, kji); + LossyUtils.Dst(dst, 1, 3, LossyUtils.Avg3(l, k, j)); + } + + private static void Hu4(Span dst, Span top, int topOffset) + { + byte i = top[topOffset - 2]; + byte j = top[topOffset - 3]; + byte k = top[topOffset - 4]; + byte l = top[topOffset - 5]; + + LossyUtils.Dst(dst, 0, 0, LossyUtils.Avg2(i, j)); + byte jk = LossyUtils.Avg2(j, k); + LossyUtils.Dst(dst, 2, 0, jk); + LossyUtils.Dst(dst, 0, 1, jk); + byte kl = LossyUtils.Avg2(k, l); + LossyUtils.Dst(dst, 2, 1, kl); + LossyUtils.Dst(dst, 0, 2, kl); + LossyUtils.Dst(dst, 1, 0, LossyUtils.Avg3(i, j, k)); + byte jkl = LossyUtils.Avg3(j, k, l); + LossyUtils.Dst(dst, 3, 0, jkl); + LossyUtils.Dst(dst, 1, 1, jkl); + byte kll = LossyUtils.Avg3(k, l, l); + LossyUtils.Dst(dst, 3, 1, kll); + LossyUtils.Dst(dst, 1, 2, kll); + LossyUtils.Dst(dst, 3, 2, l); + LossyUtils.Dst(dst, 2, 2, l); + LossyUtils.Dst(dst, 0, 3, l); + LossyUtils.Dst(dst, 1, 3, l); + LossyUtils.Dst(dst, 2, 3, l); + LossyUtils.Dst(dst, 3, 3, l); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void Fill(Span dst, int value, int size) + { + for (int j = 0; j < size; j++) + { + dst.Slice(j * WebpConstants.Bps, size).Fill((byte)value); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static byte Clip8b(int v) => (v & ~0xff) == 0 ? (byte)v : v < 0 ? (byte)0 : (byte)255; + + [MethodImpl(InliningOptions.ShortMethod)] + private static void Store(Span dst, Span reference, int x, int y, int v) => dst[x + (y * WebpConstants.Bps)] = LossyUtils.Clip8B(reference[x + (y * WebpConstants.Bps)] + (v >> 3)); + + [MethodImpl(InliningOptions.ShortMethod)] + private static int Mul(int a, int b) => (a * b) >> 16; + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8FilterHeader.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8FilterHeader.cs new file mode 100644 index 000000000..b7d2a1a84 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8FilterHeader.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + internal class Vp8FilterHeader + { + private const int NumRefLfDeltas = 4; + + private const int NumModeLfDeltas = 4; + + private int filterLevel; + + private int sharpness; + + /// + /// Initializes a new instance of the class. + /// + public Vp8FilterHeader() + { + this.RefLfDelta = new int[NumRefLfDeltas]; + this.ModeLfDelta = new int[NumModeLfDeltas]; + } + + /// + /// Gets or sets the loop filter. + /// + public LoopFilter LoopFilter { get; set; } + + /// + /// Gets or sets the filter level. Valid values are [0..63]. + /// + public int FilterLevel + { + get => this.filterLevel; + set + { + Guard.MustBeBetweenOrEqualTo(value, 0, 63, nameof(this.FilterLevel)); + this.filterLevel = value; + } + } + + /// + /// Gets or sets the filter sharpness. Valid values are [0..7]. + /// + public int Sharpness + { + get => this.sharpness; + set + { + Guard.MustBeBetweenOrEqualTo(value, 0, 7, nameof(this.Sharpness)); + this.sharpness = value; + } + } + + /// + /// Gets or sets a value indicating whether the filtering type is: 0=complex, 1=simple. + /// + public bool Simple { get; set; } + + /// + /// Gets or sets delta filter level for i4x4 relative to i16x16. + /// + public int I4x4LfDelta { get; set; } + + public bool UseLfDelta { get; set; } + + public int[] RefLfDelta { get; } + + public int[] ModeLfDelta { get; } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8FilterInfo.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8FilterInfo.cs new file mode 100644 index 000000000..8ddc5f7de --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8FilterInfo.cs @@ -0,0 +1,83 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + /// + /// Filter information. + /// + internal class Vp8FilterInfo : IDeepCloneable + { + private byte limit; + + private byte innerLevel; + + private byte highEdgeVarianceThreshold; + + /// + /// Initializes a new instance of the class. + /// + public Vp8FilterInfo() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The filter info to create a copy 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 => this.limit; + set + { + Guard.MustBeBetweenOrEqualTo(value, (byte)0, (byte)189, nameof(this.Limit)); + this.limit = value; + } + } + + /// + /// Gets or sets the inner limit in [1..63], or 0 if no filtering. + /// + public byte InnerLevel + { + get => this.innerLevel; + set + { + Guard.MustBeBetweenOrEqualTo(value, (byte)0, (byte)63, nameof(this.InnerLevel)); + this.innerLevel = value; + } + } + + /// + /// Gets or sets a value indicating whether to do inner filtering. + /// + public bool UseInnerFiltering { get; set; } + + /// + /// Gets or sets the high edge variance threshold in [0..2]. + /// + public byte HighEdgeVarianceThreshold + { + get => this.highEdgeVarianceThreshold; + set + { + Guard.MustBeBetweenOrEqualTo(value, (byte)0, (byte)2, nameof(this.HighEdgeVarianceThreshold)); + this.highEdgeVarianceThreshold = value; + } + } + + /// + public IDeepCloneable DeepClone() => new Vp8FilterInfo(this); + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8FrameHeader.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8FrameHeader.cs new file mode 100644 index 000000000..de6763b35 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8FrameHeader.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + /// + /// 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/Lossy/Vp8Histogram.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Histogram.cs new file mode 100644 index 000000000..5d048514e --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Histogram.cs @@ -0,0 +1,140 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + internal class Vp8Histogram + { + /// + /// Size of histogram used by CollectHistogram. + /// + private const int MaxCoeffThresh = 31; + + private int maxValue; + + private int lastNonZero; + + /// + /// Initializes a new instance of the class. + /// + public Vp8Histogram() + { + this.maxValue = 0; + this.lastNonZero = 1; + } + + public int GetAlpha() + { + // 'alpha' will later be clipped to [0..MAX_ALPHA] range, clamping outer + // values which happen to be mostly noise. This leaves the maximum precision + // for handling the useful small values which contribute most. + int maxValue = this.maxValue; + int lastNonZero = this.lastNonZero; + int alpha = maxValue > 1 ? WebpConstants.AlphaScale * lastNonZero / maxValue : 0; + return alpha; + } + + public void CollectHistogram(Span reference, Span pred, int startBlock, int endBlock) + { + int j; + int[] distribution = new int[MaxCoeffThresh + 1]; + for (j = startBlock; j < endBlock; j++) + { + short[] output = new short[16]; + + this.Vp8FTransform(reference.Slice(WebpLookupTables.Vp8DspScan[j]), pred.Slice(WebpLookupTables.Vp8DspScan[j]), output); + + // Convert coefficients to bin. + for (int k = 0; k < 16; ++k) + { + int v = Math.Abs(output[k]) >> 3; + int clippedValue = ClipMax(v, MaxCoeffThresh); + ++distribution[clippedValue]; + } + } + + this.SetHistogramData(distribution); + } + + public void Merge(Vp8Histogram other) + { + if (this.maxValue > other.maxValue) + { + other.maxValue = this.maxValue; + } + + if (this.lastNonZero > other.lastNonZero) + { + other.lastNonZero = this.lastNonZero; + } + } + + private void SetHistogramData(int[] distribution) + { + int maxValue = 0; + int lastNonZero = 1; + for (int k = 0; k <= MaxCoeffThresh; ++k) + { + int value = distribution[k]; + if (value > 0) + { + if (value > maxValue) + { + maxValue = value; + } + + lastNonZero = k; + } + } + + this.maxValue = maxValue; + this.lastNonZero = lastNonZero; + } + + private void Vp8FTransform(Span src, Span reference, Span output) + { + int i; + int[] tmp = new int[16]; + for (i = 0; i < 4; i++) + { + int d0 = src[0] - reference[0]; // 9bit dynamic range ([-255,255]) + int d1 = src[1] - reference[1]; + int d2 = src[2] - reference[2]; + int d3 = src[3] - reference[3]; + int a0 = d0 + d3; // 10b [-510,510] + int a1 = d1 + d2; + int a2 = d1 - d2; + int a3 = d0 - d3; + tmp[0 + (i * 4)] = (a0 + a1) * 8; // 14b [-8160,8160] + tmp[1 + (i * 4)] = ((a2 * 2217) + (a3 * 5352) + 1812) >> 9; // [-7536,7542] + tmp[2 + (i * 4)] = (a0 - a1) * 8; + tmp[3 + (i * 4)] = ((a3 * 2217) - (a2 * 5352) + 937) >> 9; + + // Do not change the span in the last iteration. + if (i < 3) + { + src = src.Slice(WebpConstants.Bps); + reference = reference.Slice(WebpConstants.Bps); + } + } + + for (i = 0; i < 4; i++) + { + int a0 = tmp[0 + i] + tmp[12 + i]; // 15b + int a1 = tmp[4 + i] + tmp[8 + i]; + int a2 = tmp[4 + i] - tmp[8 + i]; + int a3 = tmp[0 + i] - tmp[12 + i]; + output[0 + i] = (short)((a0 + a1 + 7) >> 4); // 12b + output[4 + i] = (short)((((a2 * 2217) + (a3 * 5352) + 12000) >> 16) + (a3 != 0 ? 1 : 0)); + output[8 + i] = (short)((a0 - a1 + 7) >> 4); + output[12 + i] = (short)(((a3 * 2217) - (a2 * 5352) + 51000) >> 16); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int ClipMax(int v, int max) => v > max ? max : v; + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Io.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Io.cs new file mode 100644 index 000000000..aa4eb4208 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Io.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + internal ref struct Vp8Io + { + /// + /// Gets or sets the picture width in pixels (invariable). + /// The actual area passed to put() is stored in /> field. + /// + public int Width { get; set; } + + /// + /// Gets or sets the picture height in pixels (invariable). + /// 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 UseScaling { get; set; } + + public int ScaledWidth { get; set; } + + public int ScaledHeight { get; set; } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8MacroBlock.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8MacroBlock.cs new file mode 100644 index 000000000..a57590514 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8MacroBlock.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + /// + /// 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/Lossy/Vp8MacroBlockData.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8MacroBlockData.cs new file mode 100644 index 000000000..e1a8ad1a2 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8MacroBlockData.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + /// + /// Data needed to reconstruct a macroblock. + /// + internal class Vp8MacroBlockData + { + /// + /// Initializes a new instance of the class. + /// + public Vp8MacroBlockData() + { + this.Modes = new byte[16]; + this.Coeffs = new short[384]; + } + + /// + /// Gets or sets the coefficients. 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; } + + /// + /// Gets or sets bit-wise info about the content of each sub-4x4 blocks (in decoding order). + /// Each of the 4x4 blocks for y/u/v is associated with a 2b code according to: + /// code=0 -> no coefficient + /// code=1 -> only DC + /// code=2 -> first three coefficients are non-zero + /// code=3 -> more than three coefficients are non-zero + /// This allows to call specialized transform functions. + /// + public uint NonZeroY { get; set; } + + /// + /// Gets or sets bit-wise info about the content of each sub-4x4 blocks (in decoding order). + /// Each of the 4x4 blocks for y/u/v is associated with a 2b code according to: + /// code=0 -> no coefficient + /// code=1 -> only DC + /// code=2 -> first three coefficients are non-zero + /// code=3 -> more than three coefficients are non-zero + /// This allows to call specialized transform functions. + /// + public uint NonZeroUv { get; set; } + + public bool Skip { get; set; } + + public byte Segment { get; set; } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8MacroBlockInfo.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8MacroBlockInfo.cs new file mode 100644 index 000000000..a348d19a6 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8MacroBlockInfo.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + [DebuggerDisplay("Type: {MacroBlockType}, Alpha: {Alpha}, UvMode: {UvMode}")] + internal class Vp8MacroBlockInfo + { + public Vp8MacroBlockType MacroBlockType { get; set; } + + public int UvMode { get; set; } + + public bool Skip { get; set; } + + public int Segment { get; set; } + + public int Alpha { get; set; } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8MacroBlockType.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8MacroBlockType.cs new file mode 100644 index 000000000..b5f73e73e --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8MacroBlockType.cs @@ -0,0 +1,12 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + internal enum Vp8MacroBlockType + { + I4X4 = 0, + + I16X16 = 1 + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Matrix.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Matrix.cs new file mode 100644 index 000000000..4276b887f --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Matrix.cs @@ -0,0 +1,111 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + internal class Vp8Matrix + { + private static readonly int[][] BiasMatrices = + { + // [luma-ac,luma-dc,chroma][dc,ac] + new[] { 96, 110 }, + new[] { 96, 108 }, + new[] { 110, 115 } + }; + + // Sharpening by (slightly) raising the hi-frequency coeffs. + // Hack-ish but helpful for mid-bitrate range. Use with care. + private static readonly byte[] FreqSharpening = { 0, 30, 60, 90, 30, 60, 90, 90, 60, 90, 90, 90, 90, 90, 90, 90 }; + + /// + /// Number of descaling bits for sharpening bias. + /// + private const int SharpenBits = 11; + + /// + /// Initializes a new instance of the class. + /// + public Vp8Matrix() + { + this.Q = new ushort[16]; + this.IQ = new ushort[16]; + this.Bias = new uint[16]; + this.ZThresh = new uint[16]; + this.Sharpen = new short[16]; + } + + /// + /// Gets the quantizer steps. + /// + public ushort[] Q { get; } + + /// + /// Gets the reciprocals, fixed point. + /// + public ushort[] IQ { get; } + + /// + /// Gets the rounding bias. + /// + public uint[] Bias { get; } + + /// + /// Gets the value below which a coefficient is zeroed. + /// + public uint[] ZThresh { get; } + + /// + /// Gets the frequency boosters for slight sharpening. + /// + public short[] Sharpen { get; } + + /// + /// Returns the average quantizer. + /// + /// The average quantizer. + public int Expand(int type) + { + int sum; + int i; + for (i = 0; i < 2; i++) + { + int isAcCoeff = i > 0 ? 1 : 0; + int bias = BiasMatrices[type][isAcCoeff]; + this.IQ[i] = (ushort)((1 << WebpConstants.QFix) / this.Q[i]); + this.Bias[i] = (uint)this.BIAS(bias); + + // zthresh is the exact value such that QUANTDIV(coeff, iQ, B) is: + // * zero if coeff <= zthresh + // * non-zero if coeff > zthresh + this.ZThresh[i] = ((1 << WebpConstants.QFix) - 1 - this.Bias[i]) / this.IQ[i]; + } + + for (i = 2; i < 16; i++) + { + this.Q[i] = this.Q[1]; + this.IQ[i] = this.IQ[1]; + this.Bias[i] = this.Bias[1]; + this.ZThresh[i] = this.ZThresh[1]; + } + + for (sum = 0, i = 0; i < 16; i++) + { + if (type == 0) + { + // We only use sharpening for AC luma coeffs. + this.Sharpen[i] = (short)((FreqSharpening[i] * this.Q[i]) >> SharpenBits); + } + else + { + this.Sharpen[i] = 0; + } + + sum += this.Q[i]; + } + + return (sum + 8) >> 4; + } + + private int BIAS(int b) => b << (WebpConstants.QFix - 8); + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8ModeScore.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8ModeScore.cs new file mode 100644 index 000000000..7182f6021 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8ModeScore.cs @@ -0,0 +1,128 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + /// + /// Class to accumulate score and info during RD-optimization and mode evaluation. + /// + internal class Vp8ModeScore + { + public const long MaxCost = 0x7fffffffffffffL; + + /// + /// Distortion multiplier (equivalent of lambda). + /// + private const int RdDistoMult = 256; + + /// + /// Initializes a new instance of the class. + /// + public Vp8ModeScore() + { + this.YDcLevels = new short[16]; + this.YAcLevels = new short[16 * 16]; + this.UvLevels = new short[(4 + 4) * 16]; + + this.ModesI4 = new byte[16]; + this.Derr = new int[2, 3]; + } + + /// + /// Gets or sets the distortion. + /// + public long D { get; set; } + + /// + /// Gets or sets the spectral distortion. + /// + public long SD { get; set; } + + /// + /// Gets or sets the header bits. + /// + public long H { get; set; } + + /// + /// Gets or sets the rate. + /// + public long R { get; set; } + + /// + /// Gets or sets the score. + /// + public long Score { get; set; } + + /// + /// Gets the quantized levels for luma-DC. + /// + public short[] YDcLevels { get; } + + /// + /// Gets the quantized levels for luma-AC. + /// + public short[] YAcLevels { get; } + + /// + /// Gets the quantized levels for chroma. + /// + public short[] UvLevels { get; } + + /// + /// Gets or sets the mode number for intra16 prediction. + /// + public int ModeI16 { get; set; } + + /// + /// Gets the mode numbers for intra4 predictions. + /// + public byte[] ModesI4 { get; } + + /// + /// Gets or sets the mode number of chroma prediction. + /// + public int ModeUv { get; set; } + + /// + /// Gets or sets the Non-zero blocks. + /// + public uint Nz { get; set; } + + /// + /// Gets the diffusion errors. + /// + public int[,] Derr { get; } + + public void InitScore() + { + this.D = 0; + this.SD = 0; + this.R = 0; + this.H = 0; + this.Nz = 0; + this.Score = MaxCost; + } + + public void CopyScore(Vp8ModeScore other) + { + this.D = other.D; + this.SD = other.SD; + this.R = other.R; + this.H = other.H; + this.Nz = other.Nz; // note that nz is not accumulated, but just copied. + this.Score = other.Score; + } + + public void AddScore(Vp8ModeScore other) + { + this.D += other.D; + this.SD += other.SD; + this.R += other.R; + this.H += other.H; + this.Nz |= other.Nz; // here, new nz bits are accumulated. + this.Score += other.Score; + } + + public void SetRdScore(int lambda) => this.Score = ((this.R + this.H) * lambda) + (RdDistoMult * (this.D + this.SD)); + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8PictureHeader.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8PictureHeader.cs new file mode 100644 index 000000000..3449c5cd0 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8PictureHeader.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + 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/Lossy/Vp8Proba.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Proba.cs new file mode 100644 index 000000000..d21040b6c --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Proba.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + /// + /// Data for all frame-persistent probabilities. + /// + internal class Vp8Proba + { + private const int MbFeatureTreeProbs = 3; + + /// + /// Initializes a new instance of the class. + /// + public Vp8Proba() + { + this.Segments = new uint[MbFeatureTreeProbs]; + this.Bands = new Vp8BandProbas[WebpConstants.NumTypes, WebpConstants.NumBands]; + this.BandsPtr = new Vp8BandProbas[WebpConstants.NumTypes][]; + + 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++) + { + this.BandsPtr[i] = new Vp8BandProbas[16 + 1]; + } + } + + public uint[] Segments { get; } + + public Vp8BandProbas[,] Bands { get; } + + public Vp8BandProbas[][] BandsPtr { get; } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8ProbaArray.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8ProbaArray.cs new file mode 100644 index 000000000..7bb917a6d --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8ProbaArray.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + /// + /// Probabilities associated to one of the contexts. + /// + internal class Vp8ProbaArray + { + /// + /// Initializes a new instance of the class. + /// + public Vp8ProbaArray() => this.Probabilities = new byte[WebpConstants.NumProbas]; + + /// + /// Gets the probabilities. + /// + public byte[] Probabilities { get; } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8QuantMatrix.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8QuantMatrix.cs new file mode 100644 index 000000000..43aaf6633 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8QuantMatrix.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + internal class Vp8QuantMatrix + { + private int dither; + + 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 => this.dither; + set + { + Guard.MustBeBetweenOrEqualTo(value, 0, 255, nameof(this.Dither)); + this.dither = value; + } + } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8RDLevel.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8RDLevel.cs new file mode 100644 index 000000000..1b077184b --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8RDLevel.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + /// + /// Rate-distortion optimization levels + /// + internal enum Vp8RdLevel + { + /// + /// No rd-opt. + /// + RdOptNone = 0, + + /// + /// Basic scoring (no trellis). + /// + RdOptBasic = 1, + + /// + /// Perform trellis-quant on the final decision only. + /// + RdOptTrellis = 2, + + /// + /// Trellis-quant for every scoring (much slower). + /// + RdOptTrellisAll = 3 + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Residual.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Residual.cs new file mode 100644 index 000000000..93d76e283 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Residual.cs @@ -0,0 +1,171 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + /// + /// On-the-fly info about the current set of residuals. + /// + internal class Vp8Residual + { + public int First { get; set; } + + public int Last { get; set; } + + public int CoeffType { get; set; } + + public short[] Coeffs { get; set; } + + public Vp8BandProbas[] Prob { get; set; } + + public Vp8Stats[] Stats { get; set; } + + public Vp8Costs[] Costs { get; set; } + + public void Init(int first, int coeffType, Vp8EncProba prob) + { + this.First = first; + this.CoeffType = coeffType; + this.Prob = prob.Coeffs[this.CoeffType]; + this.Stats = prob.Stats[this.CoeffType]; + this.Costs = prob.RemappedCosts[this.CoeffType]; + } + + public void SetCoeffs(Span coeffs) + { + int n; + this.Last = -1; + for (n = 15; n >= 0; --n) + { + if (coeffs[n] != 0) + { + this.Last = n; + break; + } + } + + this.Coeffs = coeffs.Slice(0, 16).ToArray(); + } + + // Simulate block coding, but only record statistics. + // Note: no need to record the fixed probas. + public int RecordCoeffs(int ctx) + { + int n = this.First; + Vp8StatsArray s = this.Stats[n].Stats[ctx]; + if (this.Last < 0) + { + this.RecordStats(0, s, 0); + return 0; + } + + while (n <= this.Last) + { + int v; + this.RecordStats(1, s, 0); // order of record doesn't matter + while ((v = this.Coeffs[n++]) == 0) + { + this.RecordStats(0, s, 1); + s = this.Stats[WebpConstants.Vp8EncBands[n]].Stats[0]; + } + + this.RecordStats(1, s, 1); + bool bit = (uint)(v + 1) > 2u; + if (this.RecordStats(bit ? 1 : 0, s, 2) == 0) + { + // v = -1 or 1 + s = this.Stats[WebpConstants.Vp8EncBands[n]].Stats[1]; + } + else + { + v = Math.Abs(v); + if (v > WebpConstants.MaxVariableLevel) + { + v = WebpConstants.MaxVariableLevel; + } + + int bits = WebpLookupTables.Vp8LevelCodes[v - 1][1]; + int pattern = WebpLookupTables.Vp8LevelCodes[v - 1][0]; + int i; + for (i = 0; (pattern >>= 1) != 0; i++) + { + int mask = 2 << i; + if ((pattern & 1) != 0) + { + this.RecordStats((bits & mask) != 0 ? 1 : 0, s, 3 + i); + } + } + + s = this.Stats[WebpConstants.Vp8EncBands[n]].Stats[2]; + } + } + + if (n < 16) + { + this.RecordStats(0, s, 0); + } + + return 1; + } + + public int GetResidualCost(int ctx0) + { + int n = this.First; + int p0 = this.Prob[n].Probabilities[ctx0].Probabilities[0]; + Vp8Costs[] costs = this.Costs; + Vp8CostArray t = costs[n].Costs[ctx0]; + + // bitCost(1, p0) is already incorporated in t[] tables, but only if ctx != 0 + // (as required by the syntax). For ctx0 == 0, we need to add it here or it'll + // be missing during the loop. + int cost = ctx0 == 0 ? LossyUtils.Vp8BitCost(1, (byte)p0) : 0; + + if (this.Last < 0) + { + return LossyUtils.Vp8BitCost(0, (byte)p0); + } + + int v; + for (; n < this.Last; ++n) + { + v = Math.Abs(this.Coeffs[n]); + int ctx = v >= 2 ? 2 : v; + cost += LevelCost(t.Costs, v); + t = costs[n + 1].Costs[ctx]; + } + + // Last coefficient is always non-zero + v = Math.Abs(this.Coeffs[n]); + cost += LevelCost(t.Costs, v); + if (n < 15) + { + int b = WebpConstants.Vp8EncBands[n + 1]; + int ctx = v == 1 ? 1 : 2; + int lastP0 = this.Prob[b].Probabilities[ctx].Probabilities[0]; + cost += LossyUtils.Vp8BitCost(0, (byte)lastP0); + } + + return cost; + } + + private static int LevelCost(Span table, int level) + => WebpLookupTables.Vp8LevelFixedCosts[level] + table[level > WebpConstants.MaxVariableLevel ? WebpConstants.MaxVariableLevel : level]; + + private int RecordStats(int bit, Vp8StatsArray statsArr, int idx) + { + // An overflow is inbound. Note we handle this at 0xfffe0000u instead of + // 0xffff0000u to make sure p + 1u does not overflow. + if (statsArr.Stats[idx] >= 0xfffe0000u) + { + statsArr.Stats[idx] = ((statsArr.Stats[idx] + 1u) >> 1) & 0x7fff7fffu; // -> divide the stats by 2. + } + + // Record bit count (lower 16 bits) and increment total count (upper 16 bits). + statsArr.Stats[idx] += 0x00010000u + (uint)bit; + + return bit; + } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8SegmentHeader.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8SegmentHeader.cs new file mode 100644 index 000000000..231ccf0d9 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8SegmentHeader.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + /// + /// Segment features. + /// + internal class Vp8SegmentHeader + { + private const int NumMbSegments = 4; + + /// + /// Initializes a new instance of the class. + /// + 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; } + + /// + /// Gets the filter strength for segments. + /// + public byte[] FilterStrength { get; } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8SegmentInfo.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8SegmentInfo.cs new file mode 100644 index 000000000..cf2a5c177 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8SegmentInfo.cs @@ -0,0 +1,85 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + internal class Vp8SegmentInfo + { + /// + /// Gets or sets the quantization matrix y1. + /// + public Vp8Matrix Y1 { get; set; } + + /// + /// Gets or sets the quantization matrix y2. + /// + public Vp8Matrix Y2 { get; set; } + + /// + /// Gets or sets the quantization matrix uv. + /// + public Vp8Matrix Uv { get; set; } + + /// + /// Gets or sets the quant-susceptibility, range [-127,127]. Zero is neutral. Lower values indicate a lower risk of blurriness. + /// + public int Alpha { get; set; } + + /// + /// Gets or sets the filter-susceptibility, range [0,255]. + /// + public int Beta { get; set; } + + /// + /// Gets or sets the final segment quantizer. + /// + public int Quant { get; set; } + + /// + /// Gets or sets the final in-loop filtering strength. + /// + public int FStrength { get; set; } + + /// + /// Gets or sets the max edge delta (for filtering strength). + /// + public int MaxEdge { get; set; } + + /// + /// Gets or sets the penalty for using Intra4. + /// + public long I4Penalty { get; set; } + + /// + /// Gets or sets the minimum distortion required to trigger filtering record. + /// + public int MinDisto { get; set; } + + public int LambdaI16 { get; set; } + + public int LambdaI4 { get; set; } + + public int TLambda { get; set; } + + public int LambdaUv { get; set; } + + public int LambdaMode { get; set; } + + public void StoreMaxDelta(Span dcs) + { + // We look at the first three AC coefficients to determine what is the average + // delta between each sub-4x4 block. + int v0 = Math.Abs(dcs[1]); + int v1 = Math.Abs(dcs[2]); + int v2 = Math.Abs(dcs[4]); + int maxV = v1 > v0 ? v1 : v0; + maxV = v2 > maxV ? v2 : maxV; + if (maxV > this.MaxEdge) + { + this.MaxEdge = maxV; + } + } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Stats.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Stats.cs new file mode 100644 index 000000000..c9738cf0c --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Stats.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + internal class Vp8Stats + { + /// + /// Initializes a new instance of the class. + /// + public Vp8Stats() + { + this.Stats = new Vp8StatsArray[WebpConstants.NumCtx]; + for (int i = 0; i < WebpConstants.NumCtx; i++) + { + this.Stats[i] = new Vp8StatsArray(); + } + } + + public Vp8StatsArray[] Stats { get; } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8StatsArray.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8StatsArray.cs new file mode 100644 index 000000000..88cc24728 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8StatsArray.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + internal class Vp8StatsArray + { + /// + /// Initializes a new instance of the class. + /// + public Vp8StatsArray() => this.Stats = new uint[WebpConstants.NumProbas]; + + public uint[] Stats { get; } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8TopSamples.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8TopSamples.cs new file mode 100644 index 000000000..ffae8abf3 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8TopSamples.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + 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/Lossy/WebpLossyDecoder.cs b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs new file mode 100644 index 000000000..ebb0b0aa4 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs @@ -0,0 +1,1376 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Webp.BitReader; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + /// + /// Decoder for lossy webp images. This code is a port of libwebp, which can be found here: https://chromium.googlesource.com/webm/libwebp + /// + /// + /// The lossy specification can be found here: https://tools.ietf.org/html/rfc6386 + /// + 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 == 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 decodedPixels) + where TPixel : unmanaged, IPixel + { + int widthMul3 = width * 3; + for (int y = 0; y < height; y++) + { + Span row = pixelData.Slice(y * widthMul3, widthMul3); + Span decodedPixelRow = decodedPixels.GetRowSpan(y); + PixelOperations.Instance.FromBgr24Bytes( + this.configuration, + row, + decodedPixelRow, + width); + } + } + + private void DecodePixelValues(int width, int height, Span pixelData, Buffer2D decodedPixels, IMemoryOwner alpha) + where TPixel : unmanaged, IPixel + { + TPixel color = default; + Span alphaSpan = alpha.Memory.Span; + Span pixelsBgr = MemoryMarshal.Cast(pixelData); + for (int y = 0; y < height; y++) + { + int yMulWidth = y * width; + Span decodedPixelRow = decodedPixels.GetRowSpan(y); + for (int x = 0; x < width; x++) + { + int offset = yMulWidth + x; + Bgr24 bgr = pixelsBgr[offset]; + color.FromBgra32(new Bgra32(bgr.R, bgr.G, bgr.B, alphaSpan[offset])); + decodedPixelRow[x] = color; + } + } + } + + 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); + } + + while (dec.MbX < dec.MbWidth) + { + this.DecodeMacroBlock(dec, bitreader); + ++dec.MbX; + } + + // 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]) == 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 = this.bitReader.GetBit(dec.SkipProbability) == 1; + } + + block.IsI4x4 = this.bitReader.GetBit(145) == 0; + if (!block.IsI4x4) + { + // Hardcoded 16x16 intra-mode decision tree. + int yMode = this.bitReader.GetBit(156) != 0 ? + this.bitReader.GetBit(128) != 0 ? (int)IntraPredictionMode.TrueMotion : (int)IntraPredictionMode.HPrediction : + this.bitReader.GetBit(163) != 0 ? (int)IntraPredictionMode.VPrediction : (int)IntraPredictionMode.DcPrediction; + 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) == 0 ? 0 : + this.bitReader.GetBit(114) == 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; + const int yOff = (WebpConstants.Bps * 1) + 8; + const int uOff = yOff + (WebpConstants.Bps * 16) + WebpConstants.Bps; + const 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. + int end = 16 * WebpConstants.Bps; + for (int i = 0; i < end; i += WebpConstants.Bps) + { + yuv[i - 1 + yOff] = 129; + } + + end = 8 * WebpConstants.Bps; + for (int i = 0; i < end; i += WebpConstants.Bps) + { + yuv[i - 1 + uOff] = 129; + yuv[i - 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. + byte topYuv15 = topYuv.Y[15]; + topRight[0] = topYuv15; + topRight[1] = topYuv15; + topRight[2] = topYuv15; + topRight[3] = topYuv15; + } + 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, 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++) + { + int jUvStride = j * dec.CacheUvStride; + uDst.Slice(j * WebpConstants.Bps, Math.Min(8, uOut.Length)).CopyTo(uOut.Slice(jUvStride)); + vDst.Slice(j * WebpConstants.Bps, Math.Min(8, vOut.Length)).CopyTo(vOut.Slice(jUvStride)); + } + } + } + + 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 == 0) + { + return; + } + + if (dec.Filter == LoopFilter.Simple) + { + int offset = dec.CacheYOffset + (mbx * 16); + if (mbx > 0) + { + LossyUtils.SimpleHFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4); + } + + if (filterInfo.UseInnerFiltering) + { + LossyUtils.SimpleHFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit); + } + + if (mby > 0) + { + LossyUtils.SimpleVFilter16(dec.CacheY.Memory.Span, offset, yBps, limit + 4); + } + + if (filterInfo.UseInnerFiltering) + { + LossyUtils.SimpleVFilter16i(dec.CacheY.Memory.Span, offset, yBps, limit); + } + } + else if (dec.Filter == LoopFilter.Complex) + { + int uvBps = dec.CacheUvStride; + int yOffset = dec.CacheYOffset + (mbx * 16); + int uvOffset = dec.CacheUvOffset + (mbx * 8); + int hevThresh = filterInfo.HighEdgeVarianceThreshold; + if (mbx > 0) + { + LossyUtils.HFilter16(dec.CacheY.Memory.Span, yOffset, yBps, limit + 4, iLevel, hevThresh); + LossyUtils.HFilter8(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit + 4, iLevel, hevThresh); + } + + if (filterInfo.UseInnerFiltering) + { + LossyUtils.HFilter16i(dec.CacheY.Memory.Span, yOffset, yBps, limit, iLevel, hevThresh); + LossyUtils.HFilter8i(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit, iLevel, hevThresh); + } + + if (mby > 0) + { + LossyUtils.VFilter16(dec.CacheY.Memory.Span, yOffset, yBps, limit + 4, iLevel, hevThresh); + LossyUtils.VFilter8(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit + 4, iLevel, hevThresh); + } + + if (filterInfo.UseInnerFiltering) + { + LossyUtils.VFilter16i(dec.CacheY.Memory.Span, yOffset, yBps, limit, iLevel, hevThresh); + LossyUtils.VFilter8i(dec.CacheU.Memory.Span, dec.CacheV.Memory.Span, uvOffset, uvBps, limit, iLevel, hevThresh); + } + } + } + + 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 == 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.Height) + { + yEnd = io.Height; // make sure we don't overflow on last row. + } + + if (yStart < yEnd) + { + io.MbY = yStart; + io.MbW = io.Width; + 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 == 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. + int bufferStride2 = 2 * bufferStride; + int ioStride2 = 2 * io.YStride; + 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(ioStride2), topU, topV, curU, curV, dst.Slice(bufferStride), dst.Slice(bufferStride2), mbw); + curY = curY.Slice(ioStride2); + dst = dst.Slice(bufferStride2); + } + + // Move to last row. + curY = curY.Slice(io.YStride); + if (yEnd < io.Height) + { + // 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) == 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; + int xMul2 = x * 2; + LossyUtils.YuvToBgr(topY[xMul2 - 1], (int)(uv0 & 0xff), (int)(uv0 >> 16), topDst.Slice((xMul2 - 1) * xStep)); + LossyUtils.YuvToBgr(topY[xMul2 - 0], (int)(uv1 & 0xff), (int)(uv1 >> 16), topDst.Slice((xMul2 - 0) * xStep)); + + if (bottomY != null) + { + uv0 = (diag03 + luv) >> 1; + uv1 = (diag12 + uv) >> 1; + LossyUtils.YuvToBgr(bottomY[xMul2 - 1], (int)(uv0 & 0xff), (int)(uv0 >> 16), bottomDst.Slice((xMul2 - 1) * xStep)); + LossyUtils.YuvToBgr(bottomY[xMul2 + 0], (int)(uv1 & 0xff), (int)(uv1 >> 16), bottomDst.Slice((xMul2 + 0) * xStep)); + } + + tluv = tuv; + luv = uv; + } + + if ((len & 1) == 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.TransformOne(src, dst); + break; + case 2: + LossyUtils.TransformAc3(src, dst); + break; + case 1: + LossyUtils.TransformDc(src, dst); + 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; + bool skip = dec.UseSkipProbability && blockData.Skip; + + if (!skip) + { + skip = 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 |= !skip; + } + } + + 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) + { + first = 0; + acProba = bands[3]; + } + else + { + // Parse DC + short[] dc = new short[16]; + int ctx = (int)(mb.NoneZeroDcCoeffs + leftMb.NoneZeroDcCoeffs); + int nz = this.GetCoeffs(br, 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. + LossyUtils.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 = bands[0]; + } + + 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; + int chPlus4 = 4 + ch; + tnz = (byte)(mb.NoneZeroAcDcCoeffs >> chPlus4); + lnz = (byte)(leftMb.NoneZeroAcDcCoeffs >> chPlus4); + 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, 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) == 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]) == 0) + { + // Previous coeff was last non-zero coeff. + return n; + } + + // Sequence of zero coeffs. + while (br.GetBit(p.Probabilities[1]) == 0) + { + p = prob[++n].Probabilities[0]; + if (n == 16) + { + return 16; + } + } + + // Non zero coeffs. + int v; + if (br.GetBit(p.Probabilities[2]) == 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]) == 0) + { + if (br.GetBit(p[4]) == 0) + { + v = 2; + } + else + { + v = 3 + br.GetBit(p[5]); + } + } + else + { + if (br.GetBit(p[6]) == 0) + { + if (br.GetBit(p[7]) == 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; + } + + 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(); + byte quantizeValue = (byte)(hasValue ? this.bitReader.ReadSignedValue(7) : 0); + vp8SegmentHeader.Quantizer[i] = quantizeValue; + } + + for (int i = 0; i < vp8SegmentHeader.FilterStrength.Length; i++) + { + hasValue = this.bitReader.ReadBool(); + byte filterStrengthValue = (byte)(hasValue ? this.bitReader.ReadSignedValue(6) : 0); + vp8SegmentHeader.FilterStrength[i] = 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.FilterLevel = (int)this.bitReader.ReadValue(6); + vp8FilterHeader.Sharpness = (int)this.bitReader.ReadValue(3); + vp8FilterHeader.UseLfDelta = this.bitReader.ReadBool(); + + dec.Filter = vp8FilterHeader.FilterLevel == 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.Slice(startIdx); + int sizeLeft = (int)size; + dec.NumPartsMinusOne = (1 << (int)this.bitReader.ReadValue(2)) - 1; + int lastPart = dec.NumPartsMinusOne; + + int lastPartMul3 = lastPart * 3; + int partStart = startIdx + lastPartMul3; + sizeLeft -= lastPartMul3; + 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]; + byte v = (byte)(this.bitReader.GetBit(prob) != 0 + ? this.bitReader.ReadValue(8) + : WebpLookupTables.DefaultCoeffsProba[t, b, c, p]); + proba.Bands[t, b].Probabilities[c].Probabilities[p] = v; + } + } + } + + for (int b = 0; b < 16 + 1; ++b) + { + proba.BandsPtr[t][b] = proba.Bands[t, WebpConstants.Vp8EncBands[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.UseScaling = false; + io.ScaledWidth = io.Width; + io.ScaledHeight = io.ScaledHeight; + io.MbW = io.Width; + io.MbH = io.Height; + uint strideLength = (pictureHeader.Width + 15) >> 4; + io.YStride = (int)(16 * strideLength); + io.UvStride = (int)(8 * strideLength); + + int intraPredModeSize = 4 * dec.MbWidth; + dec.IntraT = new byte[intraPredModeSize]; + + int extraPixels = WebpConstants.FilterExtraRows[(int)dec.Filter]; + if (dec.Filter == LoopFilter.Complex) + { + // For complex filter, we need to preserve the dependency chain. + dec.TopLeftMbX = 0; + dec.TopLeftMbY = 0; + } + else + { + // For simple filter, we include 'extraPixels' on the other side of the boundary, + // since vertical or horizontal filtering of the previous macroblock can modify some abutting pixels. + int extraShift4 = -extraPixels >> 4; + dec.TopLeftMbX = extraShift4; + dec.TopLeftMbY = extraShift4; + 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.Height + 15 + extraPixels) >> 4; + dec.BottomRightMbX = (io.Width + 15 + extraPixels) >> 4; + if (dec.BottomRightMbX > dec.MbWidth) + { + dec.BottomRightMbX = dec.MbWidth; + } + + if (dec.BottomRightMbY > dec.MbHeight) + { + dec.BottomRightMbY = dec.MbHeight; + } + + return io; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint NzCodeBits(uint nzCoeffs, int nz, int dcNz) + { + nzCoeffs <<= 2; + nzCoeffs |= (uint)(nz > 3 ? 3 : nz > 1 ? 2 : dcNz); + return nzCoeffs; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int CheckMode(int mbx, int mby, int mode) + { + // B_DC_PRED + if (mode == 0) + { + if (mbx == 0) + { + return mby == 0 + ? 6 // B_DC_PRED_NOTOPLEFT + : 5; // B_DC_PRED_NOLEFT + } + + return mby == 0 + ? 4 // B_DC_PRED_NOTOP + : 0; // B_DC_PRED + } + + return mode; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int Clip(int value, int max) => value < 0 ? 0 : value > max ? max : value; + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs b/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs new file mode 100644 index 000000000..ed03c2e71 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs @@ -0,0 +1,303 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Webp.Lossy +{ + internal static class YuvConversion + { + /// + /// Fixed-point precision for RGB->YUV. + /// + private const int YuvFix = 16; + + private const int YuvHalf = 1 << (YuvFix - 1); + + /// + /// Converts the RGB values of the image to YUV. + /// + /// The pixel type of the image. + /// The image to convert. + /// The global configuration. + /// The memory allocator. + /// Span to store the luma component of the image. + /// Span to store the u component of the image. + /// Span to store the v component of the image. + public static void ConvertRgbToYuv(Image image, Configuration configuration, MemoryAllocator memoryAllocator, Span y, Span u, Span v) + where TPixel : unmanaged, IPixel + { + int width = image.Width; + int height = image.Height; + int uvWidth = (width + 1) >> 1; + + // Temporary storage for accumulated R/G/B values during conversion to U/V. + using IMemoryOwner tmpRgb = memoryAllocator.Allocate(4 * uvWidth); + using IMemoryOwner bgraRow0Buffer = memoryAllocator.Allocate(width); + using IMemoryOwner bgraRow1Buffer = memoryAllocator.Allocate(width); + Span tmpRgbSpan = tmpRgb.GetSpan(); + Span bgraRow0 = bgraRow0Buffer.GetSpan(); + Span bgraRow1 = bgraRow1Buffer.GetSpan(); + int uvRowIndex = 0; + int rowIndex; + for (rowIndex = 0; rowIndex < height - 1; rowIndex += 2) + { + Span rowSpan = image.GetPixelRowSpan(rowIndex); + Span nextRowSpan = image.GetPixelRowSpan(rowIndex + 1); + PixelOperations.Instance.ToBgra32(configuration, rowSpan, bgraRow0); + PixelOperations.Instance.ToBgra32(configuration, nextRowSpan, bgraRow1); + + bool rowsHaveAlpha = WebpCommonUtils.CheckNonOpaque(bgraRow0) && WebpCommonUtils.CheckNonOpaque(bgraRow1); + + // Downsample U/V planes, two rows at a time. + if (!rowsHaveAlpha) + { + AccumulateRgb(bgraRow0, bgraRow1, tmpRgbSpan, width); + } + else + { + AccumulateRgba(bgraRow0, bgraRow1, tmpRgbSpan, width); + } + + ConvertRgbaToUv(tmpRgbSpan, u.Slice(uvRowIndex * uvWidth), v.Slice(uvRowIndex * uvWidth), uvWidth); + uvRowIndex++; + + ConvertRgbaToY(bgraRow0, y.Slice(rowIndex * width), width); + ConvertRgbaToY(bgraRow1, y.Slice((rowIndex + 1) * width), width); + } + + // Extra last row. + if ((height & 1) != 0) + { + Span rowSpan = image.GetPixelRowSpan(rowIndex); + PixelOperations.Instance.ToBgra32(configuration, rowSpan, bgraRow0); + ConvertRgbaToY(bgraRow0, y.Slice(rowIndex * width), width); + + if (!WebpCommonUtils.CheckNonOpaque(bgraRow0)) + { + AccumulateRgb(bgraRow0, bgraRow0, tmpRgbSpan, width); + } + else + { + AccumulateRgba(bgraRow0, bgraRow0, tmpRgbSpan, width); + } + + ConvertRgbaToUv(tmpRgbSpan, u.Slice(uvRowIndex * uvWidth), v.Slice(uvRowIndex * uvWidth), uvWidth); + } + } + + /// + /// Converts a rgba pixel row to Y. + /// + /// The row span to convert. + /// The destination span for y. + /// The width. + [MethodImpl(InliningOptions.ShortMethod)] + public static void ConvertRgbaToY(Span rowSpan, Span y, int width) + { + for (int x = 0; x < width; x++) + { + y[x] = (byte)RgbToY(rowSpan[x].R, rowSpan[x].G, rowSpan[x].B, YuvHalf); + } + } + + /// + /// Converts a rgb row of pixels to UV. + /// + /// The RGB pixel row. + /// The destination span for u. + /// The destination span for v. + /// The width. + public static void ConvertRgbaToUv(Span rgb, Span u, Span v, int width) + { + int rgbIdx = 0; + for (int i = 0; i < width; i += 1, rgbIdx += 4) + { + int r = rgb[rgbIdx], g = rgb[rgbIdx + 1], b = rgb[rgbIdx + 2]; + u[i] = (byte)RgbToU(r, g, b, YuvHalf << 2); + v[i] = (byte)RgbToV(r, g, b, YuvHalf << 2); + } + } + + public static void AccumulateRgb(Span rowSpan, Span nextRowSpan, Span dst, int width) + { + Bgra32 bgra0; + Bgra32 bgra1; + int i, j; + int dstIdx = 0; + for (i = 0, j = 0; i < (width >> 1); i += 1, j += 2, dstIdx += 4) + { + bgra0 = rowSpan[j]; + bgra1 = rowSpan[j + 1]; + Bgra32 bgra2 = nextRowSpan[j]; + Bgra32 bgra3 = nextRowSpan[j + 1]; + + dst[dstIdx] = (ushort)LinearToGamma( + GammaToLinear(bgra0.R) + + GammaToLinear(bgra1.R) + + GammaToLinear(bgra2.R) + + GammaToLinear(bgra3.R), + 0); + dst[dstIdx + 1] = (ushort)LinearToGamma( + GammaToLinear(bgra0.G) + + GammaToLinear(bgra1.G) + + GammaToLinear(bgra2.G) + + GammaToLinear(bgra3.G), + 0); + dst[dstIdx + 2] = (ushort)LinearToGamma( + GammaToLinear(bgra0.B) + + GammaToLinear(bgra1.B) + + GammaToLinear(bgra2.B) + + GammaToLinear(bgra3.B), + 0); + } + + if ((width & 1) != 0) + { + bgra0 = rowSpan[j]; + bgra1 = nextRowSpan[j]; + + dst[dstIdx] = (ushort)LinearToGamma(GammaToLinear(bgra0.R) + GammaToLinear(bgra1.R), 1); + dst[dstIdx + 1] = (ushort)LinearToGamma(GammaToLinear(bgra0.G) + GammaToLinear(bgra1.G), 1); + dst[dstIdx + 2] = (ushort)LinearToGamma(GammaToLinear(bgra0.B) + GammaToLinear(bgra1.B), 1); + } + } + + public static void AccumulateRgba(Span rowSpan, Span nextRowSpan, Span dst, int width) + { + Bgra32 bgra0; + Bgra32 bgra1; + int i, j; + int dstIdx = 0; + for (i = 0, j = 0; i < width >> 1; i += 1, j += 2, dstIdx += 4) + { + bgra0 = rowSpan[j]; + bgra1 = rowSpan[j + 1]; + Bgra32 bgra2 = nextRowSpan[j]; + Bgra32 bgra3 = nextRowSpan[j + 1]; + uint a = (uint)(bgra0.A + bgra1.A + bgra2.A + bgra3.A); + int r, g, b; + if (a is 4 * 0xff or 0) + { + r = (ushort)LinearToGamma( + GammaToLinear(bgra0.R) + + GammaToLinear(bgra1.R) + + GammaToLinear(bgra2.R) + + GammaToLinear(bgra3.R), + 0); + g = (ushort)LinearToGamma( + GammaToLinear(bgra0.G) + + GammaToLinear(bgra1.G) + + GammaToLinear(bgra2.G) + + GammaToLinear(bgra3.G), + 0); + b = (ushort)LinearToGamma( + GammaToLinear(bgra0.B) + + GammaToLinear(bgra1.B) + + GammaToLinear(bgra2.B) + + GammaToLinear(bgra3.B), + 0); + } + else + { + r = LinearToGammaWeighted(bgra0.R, bgra1.R, bgra2.R, bgra3.R, bgra0.A, bgra1.A, bgra2.A, bgra3.A, a); + g = LinearToGammaWeighted(bgra0.G, bgra1.G, bgra2.G, bgra3.G, bgra0.A, bgra1.A, bgra2.A, bgra3.A, a); + b = LinearToGammaWeighted(bgra0.B, bgra1.B, bgra2.B, bgra3.B, bgra0.A, bgra1.A, bgra2.A, bgra3.A, a); + } + + dst[dstIdx] = (ushort)r; + dst[dstIdx + 1] = (ushort)g; + dst[dstIdx + 2] = (ushort)b; + dst[dstIdx + 3] = (ushort)a; + } + + if ((width & 1) != 0) + { + bgra0 = rowSpan[j]; + bgra1 = nextRowSpan[j]; + uint a = (uint)(2u * (bgra0.A + bgra1.A)); + int r, g, b; + if (a is 4 * 0xff or 0) + { + r = (ushort)LinearToGamma(GammaToLinear(bgra0.R) + GammaToLinear(bgra1.R), 1); + g = (ushort)LinearToGamma(GammaToLinear(bgra0.G) + GammaToLinear(bgra1.G), 1); + b = (ushort)LinearToGamma(GammaToLinear(bgra0.B) + GammaToLinear(bgra1.B), 1); + } + else + { + r = LinearToGammaWeighted(bgra0.R, bgra1.R, bgra0.R, bgra1.R, bgra0.A, bgra1.A, bgra0.A, bgra1.A, a); + g = LinearToGammaWeighted(bgra0.G, bgra1.G, bgra0.G, bgra1.G, bgra0.A, bgra1.A, bgra0.A, bgra1.A, a); + b = LinearToGammaWeighted(bgra0.B, bgra1.B, bgra0.B, bgra1.B, bgra0.A, bgra1.A, bgra0.A, bgra1.A, a); + } + + dst[dstIdx] = (ushort)r; + dst[dstIdx + 1] = (ushort)g; + dst[dstIdx + 2] = (ushort)b; + dst[dstIdx + 3] = (ushort)a; + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int LinearToGammaWeighted(byte rgb0, byte rgb1, byte rgb2, byte rgb3, byte a0, byte a1, byte a2, byte a3, uint totalA) + { + uint sum = (a0 * GammaToLinear(rgb0)) + (a1 * GammaToLinear(rgb1)) + (a2 * GammaToLinear(rgb2)) + (a3 * GammaToLinear(rgb3)); + return LinearToGamma((sum * WebpLookupTables.InvAlpha[totalA]) >> (WebpConstants.AlphaFix - 2), 0); + } + + // Convert a linear value 'v' to YUV_FIX+2 fixed-point precision + // U/V value, suitable for RGBToU/V calls. + [MethodImpl(InliningOptions.ShortMethod)] + private static int LinearToGamma(uint baseValue, int shift) + { + int y = Interpolate((int)(baseValue << shift)); // Final uplifted value. + return (y + WebpConstants.GammaTabRounder) >> WebpConstants.GammaTabFix; // Descale. + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static uint GammaToLinear(byte v) => WebpLookupTables.GammaToLinearTab[v]; + + [MethodImpl(InliningOptions.ShortMethod)] + private static int Interpolate(int v) + { + int tabPos = v >> (WebpConstants.GammaTabFix + 2); // integer part. + int x = v & ((WebpConstants.GammaTabScale << 2) - 1); // fractional part. + int v0 = WebpLookupTables.LinearToGammaTab[tabPos]; + int v1 = WebpLookupTables.LinearToGammaTab[tabPos + 1]; + int y = (v1 * x) + (v0 * ((WebpConstants.GammaTabScale << 2) - x)); // interpolate + + return y; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int RgbToY(byte r, byte g, byte b, int rounding) + { + int luma = (16839 * r) + (33059 * g) + (6420 * b); + return (luma + rounding + (16 << YuvFix)) >> YuvFix; // No need to clip. + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int RgbToU(int r, int g, int b, int rounding) + { + int u = (-9719 * r) - (19081 * g) + (28800 * b); + return ClipUv(u, rounding); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int RgbToV(int r, int g, int b, int rounding) + { + int v = (+28800 * r) - (24116 * g) - (4684 * b); + return ClipUv(v, rounding); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int ClipUv(int uv, int rounding) + { + uv = (uv + rounding + (128 << (YuvFix + 2))) >> (YuvFix + 2); + return (uv & ~0xff) == 0 ? uv : uv < 0 ? 0 : 255; + } + } +} diff --git a/src/ImageSharp/Formats/Webp/Lossy/rfc6386_lossy_specification.pdf b/src/ImageSharp/Formats/Webp/Lossy/rfc6386_lossy_specification.pdf new file mode 100644 index 000000000..d421b34cc Binary files /dev/null and b/src/ImageSharp/Formats/Webp/Lossy/rfc6386_lossy_specification.pdf differ diff --git a/src/ImageSharp/Formats/Webp/MetadataExtensions.cs b/src/ImageSharp/Formats/Webp/MetadataExtensions.cs new file mode 100644 index 000000000..63f8e3427 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/MetadataExtensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.Metadata; + +namespace SixLabors.ImageSharp +{ + /// + /// Extension methods for the type. + /// + public static partial class MetadataExtensions + { + /// + /// Gets the webp format specific metadata for the image. + /// + /// The metadata this method extends. + /// The . + public static WebpMetadata GetWebpMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(WebpFormat.Instance); + } +} diff --git a/src/ImageSharp/Formats/Webp/Readme.md b/src/ImageSharp/Formats/Webp/Readme.md new file mode 100644 index 000000000..38c1cad9d --- /dev/null +++ b/src/ImageSharp/Formats/Webp/Readme.md @@ -0,0 +1,10 @@ +# 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 Spec, Lossy](http://tools.ietf.org/html/rfc6386) +- [Webp VP8L Spec, Lossless](https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification) +- [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/WebpAlphaCompressionMethod.cs b/src/ImageSharp/Formats/Webp/WebpAlphaCompressionMethod.cs new file mode 100644 index 000000000..8875a3c89 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/WebpAlphaCompressionMethod.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp +{ + internal enum WebpAlphaCompressionMethod + { + /// + /// No compression. + /// + NoCompression = 0, + + /// + /// Compressed using the Webp lossless format. + /// + WebpLosslessCompression = 1 + } +} diff --git a/src/ImageSharp/Formats/Webp/WebpAlphaFilterType.cs b/src/ImageSharp/Formats/Webp/WebpAlphaFilterType.cs new file mode 100644 index 000000000..a301239c0 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/WebpAlphaFilterType.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// 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 000000000..fe2ad79fc --- /dev/null +++ b/src/ImageSharp/Formats/Webp/WebpBitsPerPixel.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// 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 000000000..be17b420c --- /dev/null +++ b/src/ImageSharp/Formats/Webp/WebpChunkType.cs @@ -0,0 +1,57 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp +{ + /// + /// Contains a list of different webp chunk types. + /// + /// See Webp Container Specification for more details: https://developers.google.com/speed/webp/docs/riff_container + internal enum WebpChunkType : uint + { + /// + /// Header signaling the use of the VP8 format. + /// + Vp8 = 0x56503820U, + + /// + /// Header signaling the image uses lossless encoding. + /// + 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, + } +} diff --git a/src/ImageSharp/Formats/Webp/WebpCommonUtils.cs b/src/ImageSharp/Formats/Webp/WebpCommonUtils.cs new file mode 100644 index 000000000..d6e8d0a06 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/WebpCommonUtils.cs @@ -0,0 +1,178 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats; +#if SUPPORTS_RUNTIME_INTRINSICS +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +#endif + +namespace SixLabors.ImageSharp.Formats.Webp +{ + /// + /// Utility methods for lossy and lossless webp format. + /// + internal static class WebpCommonUtils + { + /// + /// Checks if the pixel row is not opaque. + /// + /// The row to check. + /// Returns true if alpha has non-0xff values. + public static unsafe bool CheckNonOpaque(Span row) + { +#if SUPPORTS_RUNTIME_INTRINSICS + if (Avx2.IsSupported) + { + ReadOnlySpan rowBytes = MemoryMarshal.AsBytes(row); + var alphaMaskVector256 = Vector256.Create(0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255); + Vector256 all0x80Vector256 = Vector256.Create((byte)0x80).AsByte(); + var alphaMask = Vector128.Create(0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255); + Vector128 all0x80 = Vector128.Create((byte)0x80).AsByte(); + + int i = 0; + int length = (row.Length * 4) - 3; + fixed (byte* src = rowBytes) + { + for (; i + 128 <= length; i += 128) + { + Vector256 a0 = Avx.LoadVector256(src + i).AsByte(); + Vector256 a1 = Avx.LoadVector256(src + i + 32).AsByte(); + Vector256 a2 = Avx.LoadVector256(src + i + 64).AsByte(); + Vector256 a3 = Avx.LoadVector256(src + i + 96).AsByte(); + Vector256 b0 = Avx2.And(a0, alphaMaskVector256).AsInt32(); + Vector256 b1 = Avx2.And(a1, alphaMaskVector256).AsInt32(); + Vector256 b2 = Avx2.And(a2, alphaMaskVector256).AsInt32(); + Vector256 b3 = Avx2.And(a3, alphaMaskVector256).AsInt32(); + Vector256 c0 = Avx2.PackSignedSaturate(b0, b1).AsInt16(); + Vector256 c1 = Avx2.PackSignedSaturate(b2, b3).AsInt16(); + Vector256 d = Avx2.PackSignedSaturate(c0, c1).AsByte(); + Vector256 bits = Avx2.CompareEqual(d, all0x80Vector256); + int mask = Avx2.MoveMask(bits); + if (mask != -1) + { + return true; + } + } + + for (; i + 64 <= length; i += 64) + { + if (IsNoneOpaque64Bytes(src, i, alphaMask, all0x80)) + { + return true; + } + } + + for (; i + 32 <= length; i += 32) + { + if (IsNoneOpaque32Bytes(src, i, alphaMask, all0x80)) + { + return true; + } + } + + for (; i <= length; i += 4) + { + if (src[i + 3] != 0xFF) + { + return true; + } + } + } + } + else if (Sse2.IsSupported) + { + ReadOnlySpan rowBytes = MemoryMarshal.AsBytes(row); + var alphaMask = Vector128.Create(0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255); + Vector128 all0x80 = Vector128.Create((byte)0x80).AsByte(); + + int i = 0; + int length = (row.Length * 4) - 3; + fixed (byte* src = rowBytes) + { + for (; i + 64 <= length; i += 64) + { + if (IsNoneOpaque64Bytes(src, i, alphaMask, all0x80)) + { + return true; + } + } + + for (; i + 32 <= length; i += 32) + { + if (IsNoneOpaque32Bytes(src, i, alphaMask, all0x80)) + { + return true; + } + } + + for (; i <= length; i += 4) + { + if (src[i + 3] != 0xFF) + { + return true; + } + } + } + } + else +#endif + { + for (int x = 0; x < row.Length; x++) + { + if (row[x].A != 0xFF) + { + return true; + } + } + } + + return false; + } + +#if SUPPORTS_RUNTIME_INTRINSICS + private static unsafe bool IsNoneOpaque64Bytes(byte* src, int i, Vector128 alphaMask, Vector128 all0x80) + { + Vector128 a0 = Sse2.LoadVector128(src + i).AsByte(); + Vector128 a1 = Sse2.LoadVector128(src + i + 16).AsByte(); + Vector128 a2 = Sse2.LoadVector128(src + i + 32).AsByte(); + Vector128 a3 = Sse2.LoadVector128(src + i + 48).AsByte(); + Vector128 b0 = Sse2.And(a0, alphaMask).AsInt32(); + Vector128 b1 = Sse2.And(a1, alphaMask).AsInt32(); + Vector128 b2 = Sse2.And(a2, alphaMask).AsInt32(); + Vector128 b3 = Sse2.And(a3, alphaMask).AsInt32(); + Vector128 c0 = Sse2.PackSignedSaturate(b0, b1).AsInt16(); + Vector128 c1 = Sse2.PackSignedSaturate(b2, b3).AsInt16(); + Vector128 d = Sse2.PackSignedSaturate(c0, c1).AsByte(); + Vector128 bits = Sse2.CompareEqual(d, all0x80); + int mask = Sse2.MoveMask(bits); + if (mask != 0xFFFF) + { + return true; + } + + return false; + } + + private static unsafe bool IsNoneOpaque32Bytes(byte* src, int i, Vector128 alphaMask, Vector128 all0x80) + { + Vector128 a0 = Sse2.LoadVector128(src + i).AsByte(); + Vector128 a1 = Sse2.LoadVector128(src + i + 16).AsByte(); + Vector128 b0 = Sse2.And(a0, alphaMask).AsInt32(); + Vector128 b1 = Sse2.And(a1, alphaMask).AsInt32(); + Vector128 c = Sse2.PackSignedSaturate(b0, b1).AsInt16(); + Vector128 d = Sse2.PackSignedSaturate(c, c).AsByte(); + Vector128 bits = Sse2.CompareEqual(d, all0x80); + int mask = Sse2.MoveMask(bits); + if (mask != 0xFFFF) + { + return true; + } + + return false; + } +#endif + } +} diff --git a/src/ImageSharp/Formats/Webp/WebpConfigurationModule.cs b/src/ImageSharp/Formats/Webp/WebpConfigurationModule.cs new file mode 100644 index 000000000..b8e74a873 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/WebpConfigurationModule.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// 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.SetEncoder(WebpFormat.Instance, new WebpEncoder()); + configuration.ImageFormatsManager.AddImageFormatDetector(new WebpImageFormatDetector()); + } + } +} diff --git a/src/ImageSharp/Formats/Webp/WebpConstants.cs b/src/ImageSharp/Formats/Webp/WebpConstants.cs new file mode 100644 index 000000000..fd46bde2b --- /dev/null +++ b/src/ImageSharp/Formats/Webp/WebpConstants.cs @@ -0,0 +1,358 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Webp +{ + /// + /// Constants used for encoding and 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[] Vp8HeaderMagicBytes = + { + 0x9D, + 0x01, + 0x2A + }; + + /// + /// Signature byte which identifies a VP8L header. + /// + public const byte Vp8LHeaderMagicByte = 0x2F; + + /// + /// Signature bytes identifying a lossy image. + /// + public static readonly byte[] Vp8MagicBytes = + { + 0x56, // V + 0x50, // P + 0x38, // 8 + 0x20 // ' ' + }; + + /// + /// Signature bytes identifying a lossless image. + /// + public static readonly byte[] Vp8LMagicBytes = + { + 0x56, // V + 0x50, // P + 0x38, // 8 + 0x4C // L + }; + + /// + /// Signature bytes identifying a VP8X header. + /// + public static readonly byte[] Vp8XMagicBytes = + { + 0x56, // V + 0x50, // P + 0x38, // 8 + 0x58 // X + }; + + /// + /// The header bytes identifying RIFF file. + /// + 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 + }; + + /// + /// 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; + + /// + /// Size of the frame header within VP8 data. + /// + public const int Vp8FrameHeaderSize = 10; + + /// + /// Size of a VP8X chunk in bytes. + /// + public const int Vp8XChunkSize = 10; + + /// + /// Size of a chunk header. + /// + public const int ChunkHeaderSize = 8; + + /// + /// Size of the RIFF header ("RIFFnnnnWEBP"). + /// + public const int RiffHeaderSize = 12; + + /// + /// Size of a chunk tag (e.g. "VP8L"). + /// + public const int TagSize = 4; + + /// + /// The Vp8L version 0. + /// + public const int Vp8LVersion = 0; + + /// + /// Maximum number of histogram images (sub-blocks). + /// + public const int MaxHuffImageSize = 2600; + + /// + /// Minimum number of Huffman bits. + /// + public const int MinHuffmanBits = 2; + + /// + /// Maximum number of Huffman bits. + /// + public const int MaxHuffmanBits = 9; + + /// + /// The maximum number of colors for a paletted images. + /// + public const int MaxPaletteSize = 256; + + /// + /// Maximum number of color cache bits is 10. + /// + public const int MaxColorCacheBits = 10; + + /// + /// The maximum number of allowed transforms in a VP8L bitstream. + /// + public const int MaxNumberOfTransforms = 4; + + /// + /// Maximum value of transformBits in VP8LEncoder. + /// + public const int MaxTransformBits = 6; + + /// + /// The bit to be written when next data to be read is a transform. + /// + public const int TransformPresent = 1; + + /// + /// The maximum allowed width or height of a webp image. + /// + public const int MaxDimension = 16383; + + public const int MaxAllowedCodeLength = 15; + + public const int DefaultCodeLength = 8; + + public const int HuffmanCodesPerMetaCode = 5; + + public const uint ArgbBlack = 0xff000000; + + public const int NumArgbCacheRows = 16; + + public const int NumLiteralCodes = 256; + + public const int NumLengthCodes = 24; + + public const int NumDistanceCodes = 40; + + public const int CodeLengthCodes = 19; + + 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 + }; + + 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 NumPredModes = 4; + + public const int NumBModes = 10; + + public const int NumCtx = 3; + + public const int MaxVariableLevel = 67; + + public const int FlatnessLimitI16 = 0; + + public const int FlatnessLimitIUv = 2; + + public const int FlatnessLimitI4 = 3; + + public const int FlatnessPenality = 140; + + // This is the common stride for enc/dec. + public const int Bps = 32; + + // gamma-compensates loss of resolution during chroma subsampling. + public const double Gamma = 0.80d; + + public const int GammaFix = 12; // Fixed-point precision for linear values. + + public const int GammaScale = (1 << GammaFix) - 1; + + public const int GammaTabFix = 7; // Fixed-point fractional bits precision. + + public const int GammaTabSize = 1 << (GammaFix - GammaTabFix); + + public const int GammaTabScale = 1 << GammaTabFix; + + public const int GammaTabRounder = GammaTabScale >> 1; + + public const int AlphaFix = 19; + + /// + /// 8b of precision for susceptibilities. + /// + public const int MaxAlpha = 255; + + /// + /// Scaling factor for alpha. + /// + public const int AlphaScale = 2 * MaxAlpha; + + /// + /// Neutral value for susceptibility. + /// + public const int QuantEncMidAlpha = 64; + + /// + /// Lowest usable value for susceptibility. + /// + public const int QuantEncMinAlpha = 30; + + /// + /// Higher meaningful value for susceptibility. + /// + public const int QuantEncMaxAlpha = 100; + + /// + /// Scaling constant between the sns (Spatial Noise Shaping) value and the QP power-law modulation. Must be strictly less than 1. + /// + public const double SnsToDq = 0.9; + + public const int QuantEncMaxDqUv = 6; + + public const int QuantEncMinDqUv = -4; + + public const int QFix = 17; + + public const int MaxDelzaSize = 64; + + /// + /// Very small filter-strength values have close to no visual effect. So we can + /// save a little decoding-CPU by turning filtering off for these. + /// + public const int FilterStrengthCutoff = 2; + + /// + /// Max size of mode partition. + /// + public const int Vp8MaxPartition0Size = 1 << 19; + + public static readonly short[] Vp8FixedCostsUv = { 302, 984, 439, 642 }; + + public static readonly short[] Vp8FixedCostsI16 = { 663, 919, 872, 919 }; + + /// + /// Distortion multiplier (equivalent of lambda). + /// + public const int RdDistoMult = 256; + + /// + /// 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[] Vp8EncBands = + { + 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 000000000..b4e6cecd0 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/WebpDecoder.cs @@ -0,0 +1,88 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +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)); + + var decoder = new WebpDecoderCore(configuration, this); + + try + { + return decoder.Decode(configuration, stream); + } + catch (InvalidMemoryOperationException ex) + { + Size dims = decoder.Dimensions; + + throw new InvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex); + } + } + + /// + public IImageInfo Identify(Configuration configuration, Stream stream) + { + Guard.NotNull(stream, nameof(stream)); + + return new WebpDecoderCore(configuration, this).Identify(configuration, stream); + } + + /// + public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); + + /// + public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(stream, nameof(stream)); + + var decoder = new WebpDecoderCore(configuration, this); + + try + { + using var bufferedStream = new BufferedReadStream(configuration, stream); + return decoder.DecodeAsync(configuration, bufferedStream, cancellationToken); + } + catch (InvalidMemoryOperationException ex) + { + Size dims = decoder.Dimensions; + + throw new InvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex); + } + } + + /// + public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => await this.DecodeAsync(configuration, stream, cancellationToken).ConfigureAwait(false); + + /// + public Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(stream, nameof(stream)); + + using var bufferedStream = new BufferedReadStream(configuration, stream); + return new WebpDecoderCore(configuration, this).IdentifyAsync(configuration, bufferedStream, cancellationToken); + } + } +} diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs new file mode 100644 index 000000000..44a55a4c6 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -0,0 +1,537 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers.Binary; +using System.IO; +using System.Threading; +using SixLabors.ImageSharp.Formats.Webp.BitReader; +using SixLabors.ImageSharp.Formats.Webp.Lossless; +using SixLabors.ImageSharp.Formats.Webp.Lossy; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Webp +{ + /// + /// Performs the webp decoding operation. + /// + internal sealed class WebpDecoderCore : IImageDecoderInternals + { + /// + /// Reusable buffer. + /// + private readonly byte[] buffer = new byte[4]; + + /// + /// 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; + + /// + /// Information about the webp image. + /// + private WebpImageInfo webImageInfo; + + /// + /// 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; } + + /// + public Configuration Configuration { get; } + + /// + /// Gets the dimensions of the image. + /// + public Size Dimensions => new((int)this.webImageInfo.Width, (int)this.webImageInfo.Height); + + /// + public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + this.Metadata = new ImageMetadata(); + this.currentStream = stream; + + uint fileSize = this.ReadImageHeader(); + + using (this.webImageInfo = this.ReadVp8Info()) + { + if (this.webImageInfo.Features is { Animation: true }) + { + WebpThrowHelper.ThrowNotSupportedException("Animations are not supported"); + } + + var image = new Image(this.Configuration, (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, this.Metadata); + Buffer2D pixels = image.GetRootFramePixelBuffer(); + if (this.webImageInfo.IsLossless) + { + var losslessDecoder = new WebpLosslessDecoder(this.webImageInfo.Vp8LBitReader, this.memoryAllocator, this.Configuration); + losslessDecoder.Decode(pixels, image.Width, image.Height); + } + else + { + var lossyDecoder = new WebpLossyDecoder(this.webImageInfo.Vp8BitReader, this.memoryAllocator, this.Configuration); + lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo); + } + + // There can be optional chunks after the image data, like EXIF and XMP. + if (this.webImageInfo.Features != null) + { + this.ParseOptionalChunks(this.webImageInfo.Features); + } + + return image; + } + } + + /// + public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + { + this.currentStream = stream; + + this.ReadImageHeader(); + using (this.webImageInfo = this.ReadVp8Info()) + { + return new ImageInfo(new PixelTypeInfo((int)this.webImageInfo.BitsPerPixel), (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, this.Metadata); + } + } + + /// + /// Reads and skips over the image header. + /// + /// The file 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 fileSize = this.ReadChunkSize(); + + // Skip 'WEBP' from the header. + this.currentStream.Skip(4); + + return fileSize; + } + + /// + /// Reads information present in the image header, about the image content and how to decode the image. + /// + /// Information about the webp image. + 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(); + default: + WebpThrowHelper.ThrowImageFormatException("Unrecognized VP8 header"); + return new WebpImageInfo(); // this return will never be reached, because throw helper will throw an exception. + } + } + + /// + /// 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() + { + var features = new WebpFeatures(); + uint fileSize = this.ReadChunkSize(); + + // The first byte contains information about the image features used. + byte imageFeatures = (byte)this.currentStream.ReadByte(); + + // The first two bit of it are reserved and should be 0. + if (imageFeatures >> 6 != 0) + { + WebpThrowHelper.ThrowImageFormatException("first two bits of the VP8X header are expected to be zero"); + } + + // If bit 3 is set, a ICC Profile Chunk should be present. + features.IccProfile = (imageFeatures & (1 << 5)) != 0; + + // If bit 4 is set, any of the frames of the image contain transparency information ("alpha" chunk). + features.Alpha = (imageFeatures & (1 << 4)) != 0; + + // If bit 5 is set, a EXIF metadata should be present. + features.ExifProfile = (imageFeatures & (1 << 3)) != 0; + + // If bit 6 is set, XMP metadata should be present. + features.XmpMetaData = (imageFeatures & (1 << 2)) != 0; + + // If bit 7 is set, animation should be present. + features.Animation = (imageFeatures & (1 << 1)) != 0; + + // 3 reserved bytes should follow which are supposed to be zero. + this.currentStream.Read(this.buffer, 0, 3); + if (this.buffer[0] != 0 || this.buffer[1] != 0 || this.buffer[2] != 0) + { + WebpThrowHelper.ThrowImageFormatException("reserved bytes should be zero"); + } + + // 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 = this.ReadChunkType(); + while (IsOptionalVp8XChunk(chunkType)) + { + this.ParseOptionalExtendedChunks(chunkType, features); + chunkType = this.ReadChunkType(); + } + + if (features.Animation) + { + // TODO: Animations are not yet supported. + return new WebpImageInfo() { Width = width, Height = height, Features = features }; + } + + 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.FileFormat = WebpFileFormatType.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 isNoKeyFrame = (frameTag & 0x1) == 1; + if (isNoKeyFrame) + { + 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 invisibleFrame = ((frameTag >> 4) & 0x1) == 0; + if (invisibleFrame) + { + 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.Vp8HeaderMagicBytes)) + { + 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 == 0 || height == 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) + { + Remaining = remaining + }; + + return new WebpImageInfo() + { + Width = width, + Height = height, + XScale = xScale, + YScale = yScale, + BitsPerPixel = features?.Alpha == 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.FileFormat = WebpFileFormatType.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.Vp8LHeaderMagicByte) + { + 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 == 0 || height == 0) + { + WebpThrowHelper.ThrowImageFormatException("invalid width or height read"); + } + + // 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 VP8X chunks, which can be ICCP, ANIM or ALPH chunks. + /// + /// The chunk type. + /// The webp image features. + private void ParseOptionalExtendedChunks(WebpChunkType chunkType, WebpFeatures features) + { + switch (chunkType) + { + case WebpChunkType.Iccp: + uint iccpChunkSize = this.ReadChunkSize(); + if (this.IgnoreMetadata) + { + this.currentStream.Skip((int)iccpChunkSize); + } + else + { + byte[] iccpData = new byte[iccpChunkSize]; + this.currentStream.Read(iccpData, 0, (int)iccpChunkSize); + var profile = new IccProfile(iccpData); + if (profile.CheckIsValid()) + { + this.Metadata.IccProfile = profile; + } + } + + break; + + case WebpChunkType.Animation: + // TODO: Decoding animation is not implemented yet. + break; + + case WebpChunkType.Alpha: + uint alphaChunkSize = this.ReadChunkSize(); + features.AlphaChunkHeader = (byte)this.currentStream.ReadByte(); + int alphaDataSize = (int)(alphaChunkSize - 1); + features.AlphaData = this.memoryAllocator.Allocate(alphaDataSize); + this.currentStream.Read(features.AlphaData.Memory.Span, 0, alphaDataSize); + break; + } + } + + /// + /// 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 == false && features.XmpMetaData == false)) + { + return; + } + + long streamLength = this.currentStream.Length; + while (this.currentStream.Position < streamLength) + { + // Read chunk header. + WebpChunkType chunkType = this.ReadChunkType(); + uint chunkLength = this.ReadChunkSize(); + + if (chunkType == WebpChunkType.Exif && this.Metadata.ExifProfile == null) + { + byte[] exifData = new byte[chunkLength]; + this.currentStream.Read(exifData, 0, (int)chunkLength); + this.Metadata.ExifProfile = new ExifProfile(exifData); + } + else + { + // Skip XMP chunk data or any duplicate EXIF chunk. + 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) == 4) + { + var chunkType = (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(this.buffer); + 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) == 4) + { + uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + return (chunkSize % 2 == 0) ? chunkSize : chunkSize + 1; + } + + throw new ImageFormatException("Invalid Webp data."); + } + + /// + /// Determines if the chunk type is an optional VP8X chunk. + /// + /// The chunk type. + /// True, if its an optional chunk type. + private static bool IsOptionalVp8XChunk(WebpChunkType chunkType) => chunkType switch + { + WebpChunkType.Alpha => true, + WebpChunkType.Animation => true, + WebpChunkType.Iccp => true, + _ => false + }; + } +} diff --git a/src/ImageSharp/Formats/Webp/WebpEncoder.cs b/src/ImageSharp/Formats/Webp/WebpEncoder.cs new file mode 100644 index 000000000..f85f65b63 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/WebpEncoder.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Webp +{ + /// + /// Image encoder for writing an image to a stream in the Webp format. + /// + public sealed class WebpEncoder : IImageEncoder, IWebpEncoderOptions + { + /// + public WebpFileFormatType? FileFormat { get; set; } + + /// + public int Quality { get; set; } = 75; + + /// + public WebpEncodingMethod Method { get; set; } = WebpEncodingMethod.Default; + + /// + public bool UseAlphaCompression { get; set; } + + /// + public int EntropyPasses { get; set; } + + /// + public int SpatialNoiseShaping { get; set; } = 50; + + /// + public int FilterStrength { get; set; } = 60; + + /// + public WebpTransparentColorMode TransparentColorMode { get; set; } = WebpTransparentColorMode.Clear; + + /// + public bool NearLossless { get; set; } + + /// + public int NearLosslessQuality { get; set; } = 100; + + /// + public void Encode(Image image, Stream stream) + where TPixel : unmanaged, IPixel + { + var encoder = new WebpEncoderCore(this, image.GetMemoryAllocator()); + encoder.Encode(image, stream); + } + + /// + public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + var encoder = new WebpEncoderCore(this, image.GetMemoryAllocator()); + return encoder.EncodeAsync(image, stream, cancellationToken); + } + } +} diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs new file mode 100644 index 000000000..a61fc7253 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -0,0 +1,158 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using System.Threading; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Webp.Lossless; +using SixLabors.ImageSharp.Formats.Webp.Lossy; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Webp +{ + /// + /// Image encoder for writing an image to a stream in the Webp format. + /// + internal sealed class WebpEncoderCore : IImageEncoderInternals + { + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// TODO: not used at the moment. + /// Indicating whether the alpha plane should be compressed with Webp lossless format. + /// + private readonly bool alphaCompression; + + /// + /// Compression quality. Between 0 and 100. + /// + private readonly int quality; + + /// + /// Quality/speed trade-off (0=fast, 6=slower-better). + /// + private readonly WebpEncodingMethod method; + + /// + /// The number of entropy-analysis passes (in [1..10]). + /// + private readonly int entropyPasses; + + /// + /// Spatial Noise Shaping. 0=off, 100=maximum. + /// + private readonly int spatialNoiseShaping; + + /// + /// The filter the strength of the deblocking filter, between 0 (no filtering) and 100 (maximum filtering). + /// + private readonly int filterStrength; + + /// + /// Flag indicating whether to preserve the exact RGB values under transparent area. Otherwise, discard this invisible + /// RGB information for better compression. + /// + private readonly WebpTransparentColorMode transparentColorMode; + + /// + /// Indicating whether near lossless mode should be used. + /// + private readonly bool nearLossless; + + /// + /// The near lossless quality. The range is 0 (maximum preprocessing) to 100 (no preprocessing, the default). + /// + private readonly int nearLosslessQuality; + + /// + /// Indicating what file format compression should be used. + /// + private readonly WebpFileFormatType? fileFormat; + + /// + /// The global configuration. + /// + private Configuration configuration; + + /// + /// Initializes a new instance of the class. + /// + /// The encoder options. + /// The memory manager. + public WebpEncoderCore(IWebpEncoderOptions options, MemoryAllocator memoryAllocator) + { + this.memoryAllocator = memoryAllocator; + this.alphaCompression = options.UseAlphaCompression; + this.fileFormat = options.FileFormat; + this.quality = options.Quality; + this.method = options.Method; + this.entropyPasses = options.EntropyPasses; + this.spatialNoiseShaping = options.SpatialNoiseShaping; + this.filterStrength = options.FilterStrength; + this.transparentColorMode = options.TransparentColorMode; + this.nearLossless = options.NearLossless; + this.nearLosslessQuality = options.NearLosslessQuality; + } + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// The token to monitor for cancellation requests. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(stream, nameof(stream)); + + this.configuration = image.GetConfiguration(); + bool lossy; + if (this.fileFormat is not null) + { + lossy = this.fileFormat == WebpFileFormatType.Lossy; + } + else + { + WebpMetadata webpMetadata = image.Metadata.GetWebpMetadata(); + lossy = webpMetadata.FileFormat == WebpFileFormatType.Lossy; + } + + if (lossy) + { + using var enc = new Vp8Encoder( + this.memoryAllocator, + this.configuration, + image.Width, + image.Height, + this.quality, + this.method, + this.entropyPasses, + this.filterStrength, + this.spatialNoiseShaping); + enc.Encode(image, stream); + } + else + { + using var enc = new Vp8LEncoder( + this.memoryAllocator, + this.configuration, + image.Width, + image.Height, + this.quality, + this.method, + this.transparentColorMode, + this.nearLossless, + this.nearLosslessQuality); + enc.Encode(image, stream); + } + } + } +} diff --git a/src/ImageSharp/Formats/Webp/WebpEncodingMethod.cs b/src/ImageSharp/Formats/Webp/WebpEncodingMethod.cs new file mode 100644 index 000000000..7d245a7e7 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/WebpEncodingMethod.cs @@ -0,0 +1,61 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp +{ + /// + /// Quality/speed trade-off for the encoding process (0=fast, 6=slower-better). + /// + public enum WebpEncodingMethod + { + /// + /// Fastest, but quality compromise. Equivalent to . + /// + Level0 = 0, + + /// + /// Fastest, but quality compromise. + /// + Fastest = Level0, + + /// + /// Level1. + /// + Level1 = 1, + + /// + /// Level 2. + /// + Level2 = 2, + + /// + /// Level 3. + /// + Level3 = 3, + + /// + /// Level 4. Equivalent to . + /// + Level4 = 4, + + /// + /// BestQuality trade off between speed and quality. + /// + Default = Level4, + + /// + /// Level 5. + /// + Level5 = 5, + + /// + /// Slowest option, but best quality. Equivalent to . + /// + Level6 = 6, + + /// + /// Slowest option, but best quality. + /// + BestQuality = Level6 + } +} diff --git a/src/ImageSharp/Formats/Webp/WebpFeatures.cs b/src/ImageSharp/Formats/Webp/WebpFeatures.cs new file mode 100644 index 000000000..b26e4101e --- /dev/null +++ b/src/ImageSharp/Formats/Webp/WebpFeatures.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; + +namespace SixLabors.ImageSharp.Formats.Webp +{ + /// + /// Image features of a VP8X image. + /// + internal class WebpFeatures : IDisposable + { + /// + /// Gets or sets a value indicating whether this image has an ICC Profile. + /// + public bool IccProfile { get; set; } + + /// + /// Gets or sets a value indicating whether this image has an alpha channel. + /// + public bool Alpha { get; set; } + + /// + /// Gets or sets the alpha data, if an ALPH chunk is present. + /// + public IMemoryOwner 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 an 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 an animation. + /// + public bool Animation { get; set; } + + /// + public void Dispose() => this.AlphaData?.Dispose(); + } +} diff --git a/src/ImageSharp/Formats/Webp/WebpFileFormatType.cs b/src/ImageSharp/Formats/Webp/WebpFileFormatType.cs new file mode 100644 index 000000000..c485f0969 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/WebpFileFormatType.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp +{ + /// + /// Info about the webp file format used. + /// + public enum WebpFileFormatType + { + /// + /// The lossless webp format. + /// + Lossless, + + /// + /// The lossy webp format. + /// + Lossy, + } +} diff --git a/src/ImageSharp/Formats/Webp/WebpFormat.cs b/src/ImageSharp/Formats/Webp/WebpFormat.cs new file mode 100644 index 000000000..1f27c4d84 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/WebpFormat.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// 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 + { + private WebpFormat() + { + } + + /// + /// 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/WebpImageFormatDetector.cs b/src/ImageSharp/Formats/Webp/WebpImageFormatDetector.cs new file mode 100644 index 000000000..4bb794f56 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/WebpImageFormatDetector.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// 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) => this.IsSupportedFileFormat(header) ? WebpFormat.Instance : null; + + private bool IsSupportedFileFormat(ReadOnlySpan header) => 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) => 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) => 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 000000000..530f5c0a5 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/WebpImageInfo.cs @@ -0,0 +1,69 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Webp.BitReader; +using SixLabors.ImageSharp.Formats.Webp.Lossy; + +namespace SixLabors.ImageSharp.Formats.Webp +{ + internal class WebpImageInfo : IDisposable + { + /// + /// Gets or sets the bitmap width in pixels. + /// + public uint Width { get; set; } + + /// + /// Gets or sets the bitmap height in pixels. + /// + 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 a 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; + + /// + public void Dispose() + { + this.Vp8BitReader?.Dispose(); + this.Vp8LBitReader?.Dispose(); + this.Features?.AlphaData?.Dispose(); + } + } +} diff --git a/src/ImageSharp/Formats/Webp/WebpLookupTables.cs b/src/ImageSharp/Formats/Webp/WebpLookupTables.cs new file mode 100644 index 000000000..57b5739c7 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/WebpLookupTables.cs @@ -0,0 +1,1473 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats.Webp +{ +#pragma warning disable SA1201 // Elements should appear in the correct order + 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 ushort[] GammaToLinearTab = new ushort[256]; + + public static readonly int[] LinearToGammaTab = new int[WebpConstants.GammaTabSize + 1]; + + public static readonly short[,][] Vp8FixedCostsI4 = new short[10, 10][]; + + // Compute susceptibility based on DCT-coeff histograms: + // the higher, the "easier" the macroblock is to compress. + public static readonly int[] Vp8DspScan = + { + // Luma + 0 + (0 * WebpConstants.Bps), 4 + (0 * WebpConstants.Bps), 8 + (0 * WebpConstants.Bps), 12 + (0 * WebpConstants.Bps), + 0 + (4 * WebpConstants.Bps), 4 + (4 * WebpConstants.Bps), 8 + (4 * WebpConstants.Bps), 12 + (4 * WebpConstants.Bps), + 0 + (8 * WebpConstants.Bps), 4 + (8 * WebpConstants.Bps), 8 + (8 * WebpConstants.Bps), 12 + (8 * WebpConstants.Bps), + 0 + (12 * WebpConstants.Bps), 4 + (12 * WebpConstants.Bps), 8 + (12 * WebpConstants.Bps), 12 + (12 * WebpConstants.Bps), + + 0 + (0 * WebpConstants.Bps), 4 + (0 * WebpConstants.Bps), 0 + (4 * WebpConstants.Bps), 4 + (4 * WebpConstants.Bps), // U + 8 + (0 * WebpConstants.Bps), 12 + (0 * WebpConstants.Bps), 8 + (4 * WebpConstants.Bps), 12 + (4 * WebpConstants.Bps) // V + }; + + public static readonly short[] Vp8Scan = + { + // Luma + 0 + (0 * WebpConstants.Bps), 4 + (0 * WebpConstants.Bps), 8 + (0 * WebpConstants.Bps), 12 + (0 * WebpConstants.Bps), + 0 + (4 * WebpConstants.Bps), 4 + (4 * WebpConstants.Bps), 8 + (4 * WebpConstants.Bps), 12 + (4 * WebpConstants.Bps), + 0 + (8 * WebpConstants.Bps), 4 + (8 * WebpConstants.Bps), 8 + (8 * WebpConstants.Bps), 12 + (8 * WebpConstants.Bps), + 0 + (12 * WebpConstants.Bps), 4 + (12 * WebpConstants.Bps), 8 + (12 * WebpConstants.Bps), 12 + (12 * WebpConstants.Bps), + }; + + public static readonly short[] Vp8ScanUv = + { + 0 + (0 * WebpConstants.Bps), 4 + (0 * WebpConstants.Bps), 0 + (4 * WebpConstants.Bps), 4 + (4 * WebpConstants.Bps), // U + 8 + (0 * WebpConstants.Bps), 12 + (0 * WebpConstants.Bps), 8 + (4 * WebpConstants.Bps), 12 + (4 * WebpConstants.Bps) // V + }; + + // fixed costs for coding levels, deduce from the coding tree. + // This is only the part that doesn't depend on the probability state. + public static readonly short[] Vp8LevelFixedCosts = + { + 0, 256, 256, 256, 256, 432, 618, 630, 731, 640, 640, 828, 901, 948, 1021, 1101, 1174, 1221, 1294, 1042, + 1085, 1115, 1158, 1202, 1245, 1275, 1318, 1337, 1380, 1410, 1453, 1497, 1540, 1570, 1613, 1280, 1295, + 1317, 1332, 1358, 1373, 1395, 1410, 1454, 1469, 1491, 1506, 1532, 1547, 1569, 1584, 1601, 1616, 1638, + 1653, 1679, 1694, 1716, 1731, 1775, 1790, 1812, 1827, 1853, 1868, 1890, 1905, 1727, 1733, 1742, 1748, + 1759, 1765, 1774, 1780, 1800, 1806, 1815, 1821, 1832, 1838, 1847, 1853, 1878, 1884, 1893, 1899, 1910, + 1916, 1925, 1931, 1951, 1957, 1966, 1972, 1983, 1989, 1998, 2004, 2027, 2033, 2042, 2048, 2059, 2065, + 2074, 2080, 2100, 2106, 2115, 2121, 2132, 2138, 2147, 2153, 2178, 2184, 2193, 2199, 2210, 2216, 2225, + 2231, 2251, 2257, 2266, 2272, 2283, 2289, 2298, 2304, 2168, 2174, 2183, 2189, 2200, 2206, 2215, 2221, + 2241, 2247, 2256, 2262, 2273, 2279, 2288, 2294, 2319, 2325, 2334, 2340, 2351, 2357, 2366, 2372, 2392, + 2398, 2407, 2413, 2424, 2430, 2439, 2445, 2468, 2474, 2483, 2489, 2500, 2506, 2515, 2521, 2541, 2547, + 2556, 2562, 2573, 2579, 2588, 2594, 2619, 2625, 2634, 2640, 2651, 2657, 2666, 2672, 2692, 2698, 2707, + 2713, 2724, 2730, 2739, 2745, 2540, 2546, 2555, 2561, 2572, 2578, 2587, 2593, 2613, 2619, 2628, 2634, + 2645, 2651, 2660, 2666, 2691, 2697, 2706, 2712, 2723, 2729, 2738, 2744, 2764, 2770, 2779, 2785, 2796, + 2802, 2811, 2817, 2840, 2846, 2855, 2861, 2872, 2878, 2887, 2893, 2913, 2919, 2928, 2934, 2945, 2951, + 2960, 2966, 2991, 2997, 3006, 3012, 3023, 3029, 3038, 3044, 3064, 3070, 3079, 3085, 3096, 3102, 3111, + 3117, 2981, 2987, 2996, 3002, 3013, 3019, 3028, 3034, 3054, 3060, 3069, 3075, 3086, 3092, 3101, 3107, + 3132, 3138, 3147, 3153, 3164, 3170, 3179, 3185, 3205, 3211, 3220, 3226, 3237, 3243, 3252, 3258, 3281, + 3287, 3296, 3302, 3313, 3319, 3328, 3334, 3354, 3360, 3369, 3375, 3386, 3392, 3401, 3407, 3432, 3438, + 3447, 3453, 3464, 3470, 3479, 3485, 3505, 3511, 3520, 3526, 3537, 3543, 3552, 3558, 2816, 2822, 2831, + 2837, 2848, 2854, 2863, 2869, 2889, 2895, 2904, 2910, 2921, 2927, 2936, 2942, 2967, 2973, 2982, 2988, + 2999, 3005, 3014, 3020, 3040, 3046, 3055, 3061, 3072, 3078, 3087, 3093, 3116, 3122, 3131, 3137, 3148, + 3154, 3163, 3169, 3189, 3195, 3204, 3210, 3221, 3227, 3236, 3242, 3267, 3273, 3282, 3288, 3299, 3305, + 3314, 3320, 3340, 3346, 3355, 3361, 3372, 3378, 3387, 3393, 3257, 3263, 3272, 3278, 3289, 3295, 3304, + 3310, 3330, 3336, 3345, 3351, 3362, 3368, 3377, 3383, 3408, 3414, 3423, 3429, 3440, 3446, 3455, 3461, + 3481, 3487, 3496, 3502, 3513, 3519, 3528, 3534, 3557, 3563, 3572, 3578, 3589, 3595, 3604, 3610, 3630, + 3636, 3645, 3651, 3662, 3668, 3677, 3683, 3708, 3714, 3723, 3729, 3740, 3746, 3755, 3761, 3781, 3787, + 3796, 3802, 3813, 3819, 3828, 3834, 3629, 3635, 3644, 3650, 3661, 3667, 3676, 3682, 3702, 3708, 3717, + 3723, 3734, 3740, 3749, 3755, 3780, 3786, 3795, 3801, 3812, 3818, 3827, 3833, 3853, 3859, 3868, 3874, + 3885, 3891, 3900, 3906, 3929, 3935, 3944, 3950, 3961, 3967, 3976, 3982, 4002, 4008, 4017, 4023, 4034, + 4040, 4049, 4055, 4080, 4086, 4095, 4101, 4112, 4118, 4127, 4133, 4153, 4159, 4168, 4174, 4185, 4191, + 4200, 4206, 4070, 4076, 4085, 4091, 4102, 4108, 4117, 4123, 4143, 4149, 4158, 4164, 4175, 4181, 4190, + 4196, 4221, 4227, 4236, 4242, 4253, 4259, 4268, 4274, 4294, 4300, 4309, 4315, 4326, 4332, 4341, 4347, + 4370, 4376, 4385, 4391, 4402, 4408, 4417, 4423, 4443, 4449, 4458, 4464, 4475, 4481, 4490, 4496, 4521, + 4527, 4536, 4542, 4553, 4559, 4568, 4574, 4594, 4600, 4609, 4615, 4626, 4632, 4641, 4647, 3515, 3521, + 3530, 3536, 3547, 3553, 3562, 3568, 3588, 3594, 3603, 3609, 3620, 3626, 3635, 3641, 3666, 3672, 3681, + 3687, 3698, 3704, 3713, 3719, 3739, 3745, 3754, 3760, 3771, 3777, 3786, 3792, 3815, 3821, 3830, 3836, + 3847, 3853, 3862, 3868, 3888, 3894, 3903, 3909, 3920, 3926, 3935, 3941, 3966, 3972, 3981, 3987, 3998, + 4004, 4013, 4019, 4039, 4045, 4054, 4060, 4071, 4077, 4086, 4092, 3956, 3962, 3971, 3977, 3988, 3994, + 4003, 4009, 4029, 4035, 4044, 4050, 4061, 4067, 4076, 4082, 4107, 4113, 4122, 4128, 4139, 4145, 4154, + 4160, 4180, 4186, 4195, 4201, 4212, 4218, 4227, 4233, 4256, 4262, 4271, 4277, 4288, 4294, 4303, 4309, + 4329, 4335, 4344, 4350, 4361, 4367, 4376, 4382, 4407, 4413, 4422, 4428, 4439, 4445, 4454, 4460, 4480, + 4486, 4495, 4501, 4512, 4518, 4527, 4533, 4328, 4334, 4343, 4349, 4360, 4366, 4375, 4381, 4401, 4407, + 4416, 4422, 4433, 4439, 4448, 4454, 4479, 4485, 4494, 4500, 4511, 4517, 4526, 4532, 4552, 4558, 4567, + 4573, 4584, 4590, 4599, 4605, 4628, 4634, 4643, 4649, 4660, 4666, 4675, 4681, 4701, 4707, 4716, 4722, + 4733, 4739, 4748, 4754, 4779, 4785, 4794, 4800, 4811, 4817, 4826, 4832, 4852, 4858, 4867, 4873, 4884, + 4890, 4899, 4905, 4769, 4775, 4784, 4790, 4801, 4807, 4816, 4822, 4842, 4848, 4857, 4863, 4874, 4880, + 4889, 4895, 4920, 4926, 4935, 4941, 4952, 4958, 4967, 4973, 4993, 4999, 5008, 5014, 5025, 5031, 5040, + 5046, 5069, 5075, 5084, 5090, 5101, 5107, 5116, 5122, 5142, 5148, 5157, 5163, 5174, 5180, 5189, 5195, + 5220, 5226, 5235, 5241, 5252, 5258, 5267, 5273, 5293, 5299, 5308, 5314, 5325, 5331, 5340, 5346, 4604, + 4610, 4619, 4625, 4636, 4642, 4651, 4657, 4677, 4683, 4692, 4698, 4709, 4715, 4724, 4730, 4755, 4761, + 4770, 4776, 4787, 4793, 4802, 4808, 4828, 4834, 4843, 4849, 4860, 4866, 4875, 4881, 4904, 4910, 4919, + 4925, 4936, 4942, 4951, 4957, 4977, 4983, 4992, 4998, 5009, 5015, 5024, 5030, 5055, 5061, 5070, 5076, + 5087, 5093, 5102, 5108, 5128, 5134, 5143, 5149, 5160, 5166, 5175, 5181, 5045, 5051, 5060, 5066, 5077, + 5083, 5092, 5098, 5118, 5124, 5133, 5139, 5150, 5156, 5165, 5171, 5196, 5202, 5211, 5217, 5228, 5234, + 5243, 5249, 5269, 5275, 5284, 5290, 5301, 5307, 5316, 5322, 5345, 5351, 5360, 5366, 5377, 5383, 5392, + 5398, 5418, 5424, 5433, 5439, 5450, 5456, 5465, 5471, 5496, 5502, 5511, 5517, 5528, 5534, 5543, 5549, + 5569, 5575, 5584, 5590, 5601, 5607, 5616, 5622, 5417, 5423, 5432, 5438, 5449, 5455, 5464, 5470, 5490, + 5496, 5505, 5511, 5522, 5528, 5537, 5543, 5568, 5574, 5583, 5589, 5600, 5606, 5615, 5621, 5641, 5647, + 5656, 5662, 5673, 5679, 5688, 5694, 5717, 5723, 5732, 5738, 5749, 5755, 5764, 5770, 5790, 5796, 5805, + 5811, 5822, 5828, 5837, 5843, 5868, 5874, 5883, 5889, 5900, 5906, 5915, 5921, 5941, 5947, 5956, 5962, + 5973, 5979, 5988, 5994, 5858, 5864, 5873, 5879, 5890, 5896, 5905, 5911, 5931, 5937, 5946, 5952, 5963, + 5969, 5978, 5984, 6009, 6015, 6024, 6030, 6041, 6047, 6056, 6062, 6082, 6088, 6097, 6103, 6114, 6120, + 6129, 6135, 6158, 6164, 6173, 6179, 6190, 6196, 6205, 6211, 6231, 6237, 6246, 6252, 6263, 6269, 6278, + 6284, 6309, 6315, 6324, 6330, 6341, 6347, 6356, 6362, 6382, 6388, 6397, 6403, 6414, 6420, 6429, 6435, + 3515, 3521, 3530, 3536, 3547, 3553, 3562, 3568, 3588, 3594, 3603, 3609, 3620, 3626, 3635, 3641, 3666, + 3672, 3681, 3687, 3698, 3704, 3713, 3719, 3739, 3745, 3754, 3760, 3771, 3777, 3786, 3792, 3815, 3821, + 3830, 3836, 3847, 3853, 3862, 3868, 3888, 3894, 3903, 3909, 3920, 3926, 3935, 3941, 3966, 3972, 3981, + 3987, 3998, 4004, 4013, 4019, 4039, 4045, 4054, 4060, 4071, 4077, 4086, 4092, 3956, 3962, 3971, 3977, + 3988, 3994, 4003, 4009, 4029, 4035, 4044, 4050, 4061, 4067, 4076, 4082, 4107, 4113, 4122, 4128, 4139, + 4145, 4154, 4160, 4180, 4186, 4195, 4201, 4212, 4218, 4227, 4233, 4256, 4262, 4271, 4277, 4288, 4294, + 4303, 4309, 4329, 4335, 4344, 4350, 4361, 4367, 4376, 4382, 4407, 4413, 4422, 4428, 4439, 4445, 4454, + 4460, 4480, 4486, 4495, 4501, 4512, 4518, 4527, 4533, 4328, 4334, 4343, 4349, 4360, 4366, 4375, 4381, + 4401, 4407, 4416, 4422, 4433, 4439, 4448, 4454, 4479, 4485, 4494, 4500, 4511, 4517, 4526, 4532, 4552, + 4558, 4567, 4573, 4584, 4590, 4599, 4605, 4628, 4634, 4643, 4649, 4660, 4666, 4675, 4681, 4701, 4707, + 4716, 4722, 4733, 4739, 4748, 4754, 4779, 4785, 4794, 4800, 4811, 4817, 4826, 4832, 4852, 4858, 4867, + 4873, 4884, 4890, 4899, 4905, 4769, 4775, 4784, 4790, 4801, 4807, 4816, 4822, 4842, 4848, 4857, 4863, + 4874, 4880, 4889, 4895, 4920, 4926, 4935, 4941, 4952, 4958, 4967, 4973, 4993, 4999, 5008, 5014, 5025, + 5031, 5040, 5046, 5069, 5075, 5084, 5090, 5101, 5107, 5116, 5122, 5142, 5148, 5157, 5163, 5174, 5180, + 5189, 5195, 5220, 5226, 5235, 5241, 5252, 5258, 5267, 5273, 5293, 5299, 5308, 5314, 5325, 5331, 5340, + 5346, 4604, 4610, 4619, 4625, 4636, 4642, 4651, 4657, 4677, 4683, 4692, 4698, 4709, 4715, 4724, 4730, + 4755, 4761, 4770, 4776, 4787, 4793, 4802, 4808, 4828, 4834, 4843, 4849, 4860, 4866, 4875, 4881, 4904, + 4910, 4919, 4925, 4936, 4942, 4951, 4957, 4977, 4983, 4992, 4998, 5009, 5015, 5024, 5030, 5055, 5061, + 5070, 5076, 5087, 5093, 5102, 5108, 5128, 5134, 5143, 5149, 5160, 5166, 5175, 5181, 5045, 5051, 5060, + 5066, 5077, 5083, 5092, 5098, 5118, 5124, 5133, 5139, 5150, 5156, 5165, 5171, 5196, 5202, 5211, 5217, + 5228, 5234, 5243, 5249, 5269, 5275, 5284, 5290, 5301, 5307, 5316, 5322, 5345, 5351, 5360, 5366, 5377, + 5383, 5392, 5398, 5418, 5424, 5433, 5439, 5450, 5456, 5465, 5471, 5496, 5502, 5511, 5517, 5528, 5534, + 5543, 5549, 5569, 5575, 5584, 5590, 5601, 5607, 5616, 5622, 5417, 5423, 5432, 5438, 5449, 5455, 5464, + 5470, 5490, 5496, 5505, 5511, 5522, 5528, 5537, 5543, 5568, 5574, 5583, 5589, 5600, 5606, 5615, 5621, + 5641, 5647, 5656, 5662, 5673, 5679, 5688, 5694, 5717, 5723, 5732, 5738, 5749, 5755, 5764, 5770, 5790, + 5796, 5805, 5811, 5822, 5828, 5837, 5843, 5868, 5874, 5883, 5889, 5900, 5906, 5915, 5921, 5941, 5947, + 5956, 5962, 5973, 5979, 5988, 5994, 5858, 5864, 5873, 5879, 5890, 5896, 5905, 5911, 5931, 5937, 5946, + 5952, 5963, 5969, 5978, 5984, 6009, 6015, 6024, 6030, 6041, 6047, 6056, 6062, 6082, 6088, 6097, 6103, + 6114, 6120, 6129, 6135, 6158, 6164, 6173, 6179, 6190, 6196, 6205, 6211, 6231, 6237, 6246, 6252, 6263, + 6269, 6278, 6284, 6309, 6315, 6324, 6330, 6341, 6347, 6356, 6362, 6382, 6388, 6397, 6403, 6414, 6420, + 6429, 6435, 5303, 5309, 5318, 5324, 5335, 5341, 5350, 5356, 5376, 5382, 5391, 5397, 5408, 5414, 5423, + 5429, 5454, 5460, 5469, 5475, 5486, 5492, 5501, 5507, 5527, 5533, 5542, 5548, 5559, 5565, 5574, 5580, + 5603, 5609, 5618, 5624, 5635, 5641, 5650, 5656, 5676, 5682, 5691, 5697, 5708, 5714, 5723, 5729, 5754, + 5760, 5769, 5775, 5786, 5792, 5801, 5807, 5827, 5833, 5842, 5848, 5859, 5865, 5874, 5880, 5744, 5750, + 5759, 5765, 5776, 5782, 5791, 5797, 5817, 5823, 5832, 5838, 5849, 5855, 5864, 5870, 5895, 5901, 5910, + 5916, 5927, 5933, 5942, 5948, 5968, 5974, 5983, 5989, 6000, 6006, 6015, 6021, 6044, 6050, 6059, 6065, + 6076, 6082, 6091, 6097, 6117, 6123, 6132, 6138, 6149, 6155, 6164, 6170, 6195, 6201, 6210, 6216, 6227, + 6233, 6242, 6248, 6268, 6274, 6283, 6289, 6300, 6306, 6315, 6321, 6116, 6122, 6131, 6137, 6148, 6154, + 6163, 6169, 6189, 6195, 6204, 6210, 6221, 6227, 6236, 6242, 6267, 6273, 6282, 6288, 6299, 6305, 6314, + 6320, 6340, 6346, 6355, 6361, 6372, 6378, 6387, 6393, 6416, 6422, 6431, 6437, 6448, 6454, 6463, 6469, + 6489, 6495, 6504, 6510, 6521, 6527, 6536, 6542, 6567, 6573, 6582, 6588, 6599, 6605, 6614, 6620, 6640, + 6646, 6655, 6661, 6672, 6678, 6687, 6693, 6557, 6563, 6572, 6578, 6589, 6595, 6604, 6610, 6630, 6636, + 6645, 6651, 6662, 6668, 6677, 6683, 6708, 6714, 6723, 6729, 6740, 6746, 6755, 6761, 6781, 6787, 6796, + 6802, 6813, 6819, 6828, 6834, 6857, 6863, 6872, 6878, 6889, 6895, 6904, 6910, 6930, 6936, 6945, 6951, + 6962, 6968, 6977, 6983, 7008, 7014, 7023, 7029, 7040, 7046, 7055, 7061, 7081, 7087, 7096, 7102, 7113, + 7119, 7128, 7134, 6392, 6398, 6407, 6413, 6424, 6430, 6439, 6445, 6465, 6471, 6480, 6486, 6497, 6503, + 6512, 6518, 6543, 6549, 6558, 6564, 6575, 6581, 6590, 6596, 6616, 6622, 6631, 6637, 6648, 6654, 6663, + 6669, 6692, 6698, 6707, 6713, 6724, 6730, 6739, 6745, 6765, 6771, 6780, 6786, 6797, 6803, 6812, 6818, + 6843, 6849, 6858, 6864, 6875, 6881, 6890, 6896, 6916, 6922, 6931, 6937, 6948, 6954, 6963, 6969, 6833, + 6839, 6848, 6854, 6865, 6871, 6880, 6886, 6906, 6912, 6921, 6927, 6938, 6944, 6953, 6959, 6984, 6990, + 6999, 7005, 7016, 7022, 7031, 7037, 7057, 7063, 7072, 7078, 7089, 7095, 7104, 7110, 7133, 7139, 7148, + 7154, 7165, 7171, 7180, 7186, 7206, 7212, 7221, 7227, 7238, 7244, 7253, 7259, 7284, 7290, 7299, 7305, + 7316, 7322, 7331, 7337, 7357, 7363, 7372, 7378, 7389, 7395, 7404, 7410, 7205, 7211, 7220, 7226, 7237, + 7243, 7252, 7258, 7278, 7284, 7293, 7299, 7310, 7316, 7325, 7331, 7356, 7362, 7371, 7377, 7388, 7394, + 7403, 7409, 7429, 7435, 7444, 7450, 7461, 7467, 7476, 7482, 7505, 7511, 7520, 7526, 7537, 7543, 7552, + 7558, 7578, 7584, 7593, 7599, 7610, 7616, 7625, 7631, 7656, 7662, 7671, 7677, 7688, 7694, 7703, 7709, + 7729, 7735, 7744, 7750, 7761 + }; + + // This table gives, for a given sharpness, the filtering strength to be + // used (at least) in order to filter a given edge step delta. + public static readonly byte[,] LevelsFromDelta = + { + { + 0, 1, 2, 3, 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, 59, 60, 61, 62, 63 + }, + { + 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 17, 18, + 20, 21, 23, 24, 26, 27, 29, 30, 32, 33, 35, 36, 38, 39, 41, 42, + 44, 45, 47, 48, 50, 51, 53, 54, 56, 57, 59, 60, 62, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63 + }, + { + 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 16, 17, 19, + 20, 22, 23, 25, 26, 28, 29, 31, 32, 34, 35, 37, 38, 40, 41, 43, + 44, 46, 47, 49, 50, 52, 53, 55, 56, 58, 59, 61, 62, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63 + }, + { + 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 15, 16, 18, 19, + 21, 22, 24, 25, 27, 28, 30, 31, 33, 34, 36, 37, 39, 40, 42, 43, + 45, 46, 48, 49, 51, 52, 54, 55, 57, 58, 60, 61, 63, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63 + }, + { + 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 14, 15, 17, 18, 20, + 21, 23, 24, 26, 27, 29, 30, 32, 33, 35, 36, 38, 39, 41, 42, 44, + 45, 47, 48, 50, 51, 53, 54, 56, 57, 59, 60, 62, 63, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63 + }, + { + 0, 1, 2, 4, 5, 7, 8, 9, 11, 12, 13, 15, 16, 17, 19, 20, + 22, 23, 25, 26, 28, 29, 31, 32, 34, 35, 37, 38, 40, 41, 43, 44, + 46, 47, 49, 50, 52, 53, 55, 56, 58, 59, 61, 62, 63, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63 + }, + { + 0, 1, 2, 4, 5, 7, 8, 9, 11, 12, 13, 15, 16, 18, 19, 21, + 22, 24, 25, 27, 28, 30, 31, 33, 34, 36, 37, 39, 40, 42, 43, 45, + 46, 48, 49, 51, 52, 54, 55, 57, 58, 60, 61, 63, 63, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63 + }, + { + 0, 1, 2, 4, 5, 7, 8, 9, 11, 12, 14, 15, 17, 18, 20, 21, + 23, 24, 26, 27, 29, 30, 32, 33, 35, 36, 38, 39, 41, 42, 44, 45, + 47, 48, 50, 51, 53, 54, 56, 57, 59, 60, 62, 63, 63, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63 + } + }; + + public static readonly byte[] Norm = + { + // renorm_sizes[i] = 8 - log2(i) + 7, 6, 6, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 0 + }; + + public static readonly byte[] NewRange = + { + // range = ((range + 1) << kVP8Log2Range[range]) - 1 + 127, 127, 191, 127, 159, 191, 223, 127, 143, 159, 175, 191, 207, 223, 239, + 127, 135, 143, 151, 159, 167, 175, 183, 191, 199, 207, 215, 223, 231, 239, + 247, 127, 131, 135, 139, 143, 147, 151, 155, 159, 163, 167, 171, 175, 179, + 183, 187, 191, 195, 199, 203, 207, 211, 215, 219, 223, 227, 231, 235, 239, + 243, 247, 251, 127, 129, 131, 133, 135, 137, 139, 141, 143, 145, 147, 149, + 151, 153, 155, 157, 159, 161, 163, 165, 167, 169, 171, 173, 175, 177, 179, + 181, 183, 185, 187, 189, 191, 193, 195, 197, 199, 201, 203, 205, 207, 209, + 211, 213, 215, 217, 219, 221, 223, 225, 227, 229, 231, 233, 235, 237, 239, + 241, 243, 245, 247, 249, 251, 253, 127 + }; + + public static readonly ushort[] Vp8EntropyCost = + { + 1792, 1792, 1792, 1536, 1536, 1408, 1366, 1280, 1280, 1216, + 1178, 1152, 1110, 1076, 1061, 1024, 1024, 992, 968, 951, + 939, 911, 896, 878, 871, 854, 838, 820, 811, 794, + 786, 768, 768, 752, 740, 732, 720, 709, 704, 690, + 683, 672, 666, 655, 647, 640, 631, 622, 615, 607, + 598, 592, 586, 576, 572, 564, 559, 555, 547, 541, + 534, 528, 522, 512, 512, 504, 500, 494, 488, 483, + 477, 473, 467, 461, 458, 452, 448, 443, 438, 434, + 427, 424, 419, 415, 410, 406, 403, 399, 394, 390, + 384, 384, 377, 374, 370, 366, 362, 359, 355, 351, + 347, 342, 342, 336, 333, 330, 326, 323, 320, 316, + 312, 308, 305, 302, 299, 296, 293, 288, 287, 283, + 280, 277, 274, 272, 268, 266, 262, 256, 256, 256, + 251, 248, 245, 242, 240, 237, 234, 232, 228, 226, + 223, 221, 218, 216, 214, 211, 208, 205, 203, 201, + 198, 196, 192, 191, 188, 187, 183, 181, 179, 176, + 175, 171, 171, 168, 165, 163, 160, 159, 156, 154, + 152, 150, 148, 146, 144, 142, 139, 138, 135, 133, + 131, 128, 128, 125, 123, 121, 119, 117, 115, 113, + 111, 110, 107, 105, 103, 102, 100, 98, 96, 94, + 92, 91, 89, 86, 86, 83, 82, 80, 77, 76, + 74, 73, 71, 69, 67, 66, 64, 63, 61, 59, + 57, 55, 54, 52, 51, 49, 47, 46, 44, 43, + 41, 40, 38, 36, 35, 33, 32, 30, 29, 27, + 25, 24, 22, 21, 19, 18, 16, 15, 13, 12, + 10, 9, 7, 6, 4, 3 + }; + + public static readonly ushort[][] Vp8LevelCodes = + { + new ushort[] { 0x001, 0x000 }, new ushort[] { 0x007, 0x001 }, new ushort[] { 0x00f, 0x005 }, + new ushort[] { 0x00f, 0x00d }, new ushort[] { 0x033, 0x003 }, new ushort[] { 0x033, 0x003 }, new ushort[] { 0x033, 0x023 }, + new ushort[] { 0x033, 0x023 }, new ushort[] { 0x033, 0x023 }, new ushort[] { 0x033, 0x023 }, new ushort[] { 0x0d3, 0x013 }, + new ushort[] { 0x0d3, 0x013 }, new ushort[] { 0x0d3, 0x013 }, new ushort[] { 0x0d3, 0x013 }, new ushort[] { 0x0d3, 0x013 }, + new ushort[] { 0x0d3, 0x013 }, new ushort[] { 0x0d3, 0x013 }, new ushort[] { 0x0d3, 0x013 }, new ushort[] { 0x0d3, 0x093 }, + new ushort[] { 0x0d3, 0x093 }, new ushort[] { 0x0d3, 0x093 }, new ushort[] { 0x0d3, 0x093 }, new ushort[] { 0x0d3, 0x093 }, + new ushort[] { 0x0d3, 0x093 }, new ushort[] { 0x0d3, 0x093 }, new ushort[] { 0x0d3, 0x093 }, new ushort[] { 0x0d3, 0x093 }, + new ushort[] { 0x0d3, 0x093 }, new ushort[] { 0x0d3, 0x093 }, new ushort[] { 0x0d3, 0x093 }, new ushort[] { 0x0d3, 0x093 }, + new ushort[] { 0x0d3, 0x093 }, new ushort[] { 0x0d3, 0x093 }, new ushort[] { 0x0d3, 0x093 }, new ushort[] { 0x153, 0x053 }, + new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, + new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, + new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, + new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, + new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, + new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, + new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, + new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x153 }, + }; + + /// + /// Lookup table for small values of log2(int). + /// + public static readonly float[] Log2Table = + { + 0.0000000000000000f, 0.0000000000000000f, + 1.0000000000000000f, 1.5849625007211560f, + 2.0000000000000000f, 2.3219280948873621f, + 2.5849625007211560f, 2.8073549220576041f, + 3.0000000000000000f, 3.1699250014423121f, + 3.3219280948873621f, 3.4594316186372973f, + 3.5849625007211560f, 3.7004397181410921f, + 3.8073549220576041f, 3.9068905956085187f, + 4.0000000000000000f, 4.0874628412503390f, + 4.1699250014423121f, 4.2479275134435852f, + 4.3219280948873626f, 4.3923174227787606f, + 4.4594316186372973f, 4.5235619560570130f, + 4.5849625007211560f, 4.6438561897747243f, + 4.7004397181410917f, 4.7548875021634682f, + 4.8073549220576037f, 4.8579809951275718f, + 4.9068905956085187f, 4.9541963103868749f, + 5.0000000000000000f, 5.0443941193584533f, + 5.0874628412503390f, 5.1292830169449663f, + 5.1699250014423121f, 5.2094533656289501f, + 5.2479275134435852f, 5.2854022188622487f, + 5.3219280948873626f, 5.3575520046180837f, + 5.3923174227787606f, 5.4262647547020979f, + 5.4594316186372973f, 5.4918530963296747f, + 5.5235619560570130f, 5.5545888516776376f, + 5.5849625007211560f, 5.6147098441152083f, + 5.6438561897747243f, 5.6724253419714951f, + 5.7004397181410917f, 5.7279204545631987f, + 5.7548875021634682f, 5.7813597135246599f, + 5.8073549220576037f, 5.8328900141647412f, + 5.8579809951275718f, 5.8826430493618415f, + 5.9068905956085187f, 5.9307373375628866f, + 5.9541963103868749f, 5.9772799234999167f, + 6.0000000000000000f, 6.0223678130284543f, + 6.0443941193584533f, 6.0660891904577720f, + 6.0874628412503390f, 6.1085244567781691f, + 6.1292830169449663f, 6.1497471195046822f, + 6.1699250014423121f, 6.1898245588800175f, + 6.2094533656289501f, 6.2288186904958804f, + 6.2479275134435852f, 6.2667865406949010f, + 6.2854022188622487f, 6.3037807481771030f, + 6.3219280948873626f, 6.3398500028846243f, + 6.3575520046180837f, 6.3750394313469245f, + 6.3923174227787606f, 6.4093909361377017f, + 6.4262647547020979f, 6.4429434958487279f, + 6.4594316186372973f, 6.4757334309663976f, + 6.4918530963296747f, 6.5077946401986963f, + 6.5235619560570130f, 6.5391588111080309f, + 6.5545888516776376f, 6.5698556083309478f, + 6.5849625007211560f, 6.5999128421871278f, + 6.6147098441152083f, 6.6293566200796094f, + 6.6438561897747243f, 6.6582114827517946f, + 6.6724253419714951f, 6.6865005271832185f, + 6.7004397181410917f, 6.7142455176661224f, + 6.7279204545631987f, 6.7414669864011464f, + 6.7548875021634682f, 6.7681843247769259f, + 6.7813597135246599f, 6.7944158663501061f, + 6.8073549220576037f, 6.8201789624151878f, + 6.8328900141647412f, 6.8454900509443747f, + 6.8579809951275718f, 6.8703647195834047f, + 6.8826430493618415f, 6.8948177633079437f, + 6.9068905956085187f, 6.9188632372745946f, + 6.9307373375628866f, 6.9425145053392398f, + 6.9541963103868749f, 6.9657842846620869f, + 6.9772799234999167f, 6.9886846867721654f, + 7.0000000000000000f, 7.0112272554232539f, + 7.0223678130284543f, 7.0334230015374501f, + 7.0443941193584533f, 7.0552824355011898f, + 7.0660891904577720f, 7.0768155970508308f, + 7.0874628412503390f, 7.0980320829605263f, + 7.1085244567781691f, 7.1189410727235076f, + 7.1292830169449663f, 7.1395513523987936f, + 7.1497471195046822f, 7.1598713367783890f, + 7.1699250014423121f, 7.1799090900149344f, + 7.1898245588800175f, 7.1996723448363644f, + 7.2094533656289501f, 7.2191685204621611f, + 7.2288186904958804f, 7.2384047393250785f, + 7.2479275134435852f, 7.2573878426926521f, + 7.2667865406949010f, 7.2761244052742375f, + 7.2854022188622487f, 7.2946207488916270f, + 7.3037807481771030f, 7.3128829552843557f, + 7.3219280948873626f, 7.3309168781146167f, + 7.3398500028846243f, 7.3487281542310771f, + 7.3575520046180837f, 7.3663222142458160f, + 7.3750394313469245f, 7.3837042924740519f, + 7.3923174227787606f, 7.4008794362821843f, + 7.4093909361377017f, 7.4178525148858982f, + 7.4262647547020979f, 7.4346282276367245f, + 7.4429434958487279f, 7.4512111118323289f, + 7.4594316186372973f, 7.4676055500829976f, + 7.4757334309663976f, 7.4838157772642563f, + 7.4918530963296747f, 7.4998458870832056f, + 7.5077946401986963f, 7.5156998382840427f, + 7.5235619560570130f, 7.5313814605163118f, + 7.5391588111080309f, 7.5468944598876364f, + 7.5545888516776376f, 7.5622424242210728f, + 7.5698556083309478f, 7.5774288280357486f, + 7.5849625007211560f, 7.5924570372680806f, + 7.5999128421871278f, 7.6073303137496104f, + 7.6147098441152083f, 7.6220518194563764f, + 7.6293566200796094f, 7.6366246205436487f, + 7.6438561897747243f, 7.6510516911789281f, + 7.6582114827517946f, 7.6653359171851764f, + 7.6724253419714951f, 7.6794800995054464f, + 7.6865005271832185f, 7.6934869574993252f, + 7.7004397181410917f, 7.7073591320808825f, + 7.7142455176661224f, 7.7210991887071855f, + 7.7279204545631987f, 7.7347096202258383f, + 7.7414669864011464f, 7.7481928495894605f, + 7.7548875021634682f, 7.7615512324444795f, + 7.7681843247769259f, 7.7747870596011736f, + 7.7813597135246599f, 7.7879025593914317f, + 7.7944158663501061f, 7.8008998999203047f, + 7.8073549220576037f, 7.8137811912170374f, + 7.8201789624151878f, 7.8265484872909150f, + 7.8328900141647412f, 7.8392037880969436f, + 7.8454900509443747f, 7.8517490414160571f, + 7.8579809951275718f, 7.8641861446542797f, + 7.8703647195834047f, 7.8765169465649993f, + 7.8826430493618415f, 7.8887432488982591f, + 7.8948177633079437f, 7.9008668079807486f, + 7.9068905956085187f, 7.9128893362299619f, + 7.9188632372745946f, 7.9248125036057812f, + 7.9307373375628866f, 7.9366379390025709f, + 7.9425145053392398f, 7.9483672315846778f, + 7.9541963103868749f, 7.9600019320680805f, + 7.9657842846620869f, 7.9715435539507719f, + 7.9772799234999167f, 7.9829935746943103f, + 7.9886846867721654f, 7.9943534368588577f + }; + + public static readonly float[] SLog2Table = + { + 0.00000000f, 0.00000000f, 2.00000000f, 4.75488750f, + 8.00000000f, 11.60964047f, 15.50977500f, 19.65148445f, + 24.00000000f, 28.52932501f, 33.21928095f, 38.05374781f, + 43.01955001f, 48.10571634f, 53.30296891f, 58.60335893f, + 64.00000000f, 69.48686830f, 75.05865003f, 80.71062276f, + 86.43856190f, 92.23866588f, 98.10749561f, 104.04192499f, + 110.03910002f, 116.09640474f, 122.21143267f, 128.38196256f, + 134.60593782f, 140.88144886f, 147.20671787f, 153.58008562f, + 160.00000000f, 166.46500594f, 172.97373660f, 179.52490559f, + 186.11730005f, 192.74977453f, 199.42124551f, 206.13068654f, + 212.87712380f, 219.65963219f, 226.47733176f, 233.32938445f, + 240.21499122f, 247.13338933f, 254.08384998f, 261.06567603f, + 268.07820003f, 275.12078236f, 282.19280949f, 289.29369244f, + 296.42286534f, 303.57978409f, 310.76392512f, 317.97478424f, + 325.21187564f, 332.47473081f, 339.76289772f, 347.07593991f, + 354.41343574f, 361.77497759f, 369.16017124f, 376.56863518f, + 384.00000000f, 391.45390785f, 398.93001188f, 406.42797576f, + 413.94747321f, 421.48818752f, 429.04981119f, 436.63204548f, + 444.23460010f, 451.85719280f, 459.49954906f, 467.16140179f, + 474.84249102f, 482.54256363f, 490.26137307f, 497.99867911f, + 505.75424759f, 513.52785023f, 521.31926438f, 529.12827280f, + 536.95466351f, 544.79822957f, 552.65876890f, 560.53608414f, + 568.42998244f, 576.34027536f, 584.26677867f, 592.20931226f, + 600.16769996f, 608.14176943f, 616.13135206f, 624.13628279f, + 632.15640007f, 640.19154569f, 648.24156472f, 656.30630539f, + 664.38561898f, 672.47935976f, 680.58738488f, 688.70955430f, + 696.84573069f, 704.99577935f, 713.15956818f, 721.33696754f, + 729.52785023f, 737.73209140f, 745.94956849f, 754.18016116f, + 762.42375127f, 770.68022275f, 778.94946161f, 787.23135586f, + 795.52579543f, 803.83267219f, 812.15187982f, 820.48331383f, + 828.82687147f, 837.18245171f, 845.54995518f, 853.92928416f, + 862.32034249f, 870.72303558f, 879.13727036f, 887.56295522f, + 896.00000000f, 904.44831595f, 912.90781569f, 921.37841320f, + 929.86002376f, 938.35256392f, 946.85595152f, 955.37010560f, + 963.89494641f, 972.43039537f, 980.97637504f, 989.53280911f, + 998.09962237f, 1006.67674069f, 1015.26409097f, 1023.86160116f, + 1032.46920021f, 1041.08681805f, 1049.71438560f, 1058.35183469f, + 1066.99909811f, 1075.65610955f, 1084.32280357f, 1092.99911564f, + 1101.68498204f, 1110.38033993f, 1119.08512727f, 1127.79928282f, + 1136.52274614f, 1145.25545758f, 1153.99735821f, 1162.74838989f, + 1171.50849518f, 1180.27761738f, 1189.05570047f, 1197.84268914f, + 1206.63852876f, 1215.44316535f, 1224.25654560f, 1233.07861684f, + 1241.90932703f, 1250.74862473f, 1259.59645914f, 1268.45278005f, + 1277.31753781f, 1286.19068338f, 1295.07216828f, 1303.96194457f, + 1312.85996488f, 1321.76618236f, 1330.68055071f, 1339.60302413f, + 1348.53355734f, 1357.47210556f, 1366.41862452f, 1375.37307041f, + 1384.33539991f, 1393.30557020f, 1402.28353887f, 1411.26926400f, + 1420.26270412f, 1429.26381818f, 1438.27256558f, 1447.28890615f, + 1456.31280014f, 1465.34420819f, 1474.38309138f, 1483.42941118f, + 1492.48312945f, 1501.54420843f, 1510.61261078f, 1519.68829949f, + 1528.77123795f, 1537.86138993f, 1546.95871952f, 1556.06319119f, + 1565.17476976f, 1574.29342040f, 1583.41910860f, 1592.55180020f, + 1601.69146137f, 1610.83805860f, 1619.99155871f, 1629.15192882f, + 1638.31913637f, 1647.49314911f, 1656.67393509f, 1665.86146266f, + 1675.05570047f, 1684.25661744f, 1693.46418280f, 1702.67836605f, + 1711.89913698f, 1721.12646563f, 1730.36032233f, 1739.60067768f, + 1748.84750254f, 1758.10076802f, 1767.36044551f, 1776.62650662f, + 1785.89892323f, 1795.17766747f, 1804.46271172f, 1813.75402857f, + 1823.05159087f, 1832.35537170f, 1841.66534438f, 1850.98148244f, + 1860.30375965f, 1869.63214999f, 1878.96662767f, 1888.30716711f, + 1897.65374295f, 1907.00633003f, 1916.36490342f, 1925.72943838f, + 1935.09991037f, 1944.47629506f, 1953.85856831f, 1963.24670620f, + 1972.64068498f, 1982.04048108f, 1991.44607117f, 2000.85743204f, + 2010.27454072f, 2019.69737440f, 2029.12591044f, 2038.56012640f + }; + + 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 + }; + + public static readonly uint[] PlaneToCodeLut = + { + 96, 73, 55, 39, 23, 13, 5, 1, 255, 255, 255, 255, 255, 255, 255, 255, + 101, 78, 58, 42, 26, 16, 8, 2, 0, 3, 9, 17, 27, 43, 59, 79, + 102, 86, 62, 46, 32, 20, 10, 6, 4, 7, 11, 21, 33, 47, 63, 87, + 105, 90, 70, 52, 37, 28, 18, 14, 12, 15, 19, 29, 38, 53, 71, 91, + 110, 99, 82, 66, 48, 35, 30, 24, 22, 25, 31, 36, 49, 67, 83, 100, + 115, 108, 94, 76, 64, 50, 44, 40, 34, 41, 45, 51, 65, 77, 95, 109, + 118, 113, 103, 92, 80, 68, 60, 56, 54, 57, 61, 69, 81, 93, 104, 114, + 119, 116, 111, 106, 97, 88, 84, 74, 72, 75, 85, 89, 98, 107, 112, 117 + }; + + // 31 ^ clz(i) + public static ReadOnlySpan LogTable8Bit => new byte[] + { + 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 byte[] 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 ushort[] 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 + }; + + public static readonly ushort[] AcTable2 = + { + 8, 8, 9, 10, 12, 13, 15, 17, + 18, 20, 21, 23, 24, 26, 27, 29, + 31, 32, 34, 35, 37, 38, 40, 41, + 43, 44, 46, 48, 49, 51, 52, 54, + 55, 57, 58, 60, 62, 63, 65, 66, + 68, 69, 71, 72, 74, 75, 77, 79, + 80, 82, 83, 85, 86, 88, 89, 93, + 96, 99, 102, 105, 108, 111, 114, 117, + 120, 124, 127, 130, 133, 136, 139, 142, + 145, 148, 151, 155, 158, 161, 164, 167, + 170, 173, 176, 179, 184, 189, 193, 198, + 203, 207, 212, 217, 221, 226, 230, 235, + 240, 244, 249, 254, 258, 263, 268, 274, + 280, 286, 292, 299, 305, 311, 317, 323, + 330, 336, 342, 348, 354, 362, 370, 379, + 385, 393, 401, 409, 416, 424, 432, 440 + }; + + // 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 } + } + } + }; + + public static readonly (int Code, int ExtraBits)[] PrefixEncodeCode = + { + (0, 0), (0, 0), (1, 0), (2, 0), (3, 0), (4, 1), (4, 1), (5, 1), + (5, 1), (6, 2), (6, 2), (6, 2), (6, 2), (7, 2), (7, 2), (7, 2), + (7, 2), (8, 3), (8, 3), (8, 3), (8, 3), (8, 3), (8, 3), (8, 3), + (8, 3), (9, 3), (9, 3), (9, 3), (9, 3), (9, 3), (9, 3), (9, 3), + (9, 3), (10, 4), (10, 4), (10, 4), (10, 4), (10, 4), (10, 4), (10, 4), + (10, 4), (10, 4), (10, 4), (10, 4), (10, 4), (10, 4), (10, 4), (10, 4), + (10, 4), (11, 4), (11, 4), (11, 4), (11, 4), (11, 4), (11, 4), (11, 4), + (11, 4), (11, 4), (11, 4), (11, 4), (11, 4), (11, 4), (11, 4), (11, 4), + (11, 4), (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), + (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), + (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), + (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), (12, 5), + (12, 5), (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), + (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), + (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), + (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), (13, 5), + (13, 5), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), + (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), + (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), + (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), + (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), + (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), + (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), + (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), (14, 6), + (14, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), + (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), + (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), + (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), + (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), + (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), + (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), + (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), (15, 6), + (15, 6), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), (16, 7), + (16, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), (17, 7), + }; + + public static readonly byte[] PrefixEncodeExtraBitsValue = + { + 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 2, 3, 0, 1, 2, 3, + 0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 0, 1, 2, 3, 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, + 0, 1, 2, 3, 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, + 0, 1, 2, 3, 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, 59, 60, 61, 62, 63, + 0, 1, 2, 3, 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, 59, 60, 61, 62, 63, + 0, 1, 2, 3, 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, 59, 60, 61, 62, 63, + 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, + 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, + 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, + 123, 124, 125, 126, 127, 0, 1, 2, 3, 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, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, + 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, + 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, + 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, + 118, 119, 120, 121, 122, 123, 124, 125, 126 + }; + + // Following table is (1 << AlphaFix) / a. The (v * InvAlpha[a]) >> AlphaFix + // formula is then equal to v / a in most (99.6%) cases. Note that this table + // and constant are adjusted very tightly to fit 32b arithmetic. + // In particular, they use the fact that the operands for 'v / a' are actually + // derived as v = (a0.p0 + a1.p1 + a2.p2 + a3.p3) and a = a0 + a1 + a2 + a3 + // with ai in [0..255] and pi in [0..1<(); + 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(); + InitializeFixedCostsI4(); + } + + 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 }; + } + + private static void InitializeFixedCostsI4() + { + Vp8FixedCostsI4[0, 0] = new short[] { 40, 1151, 1723, 1874, 2103, 2019, 1628, 1777, 2226, 2137 }; + Vp8FixedCostsI4[0, 1] = new short[] { 192, 469, 1296, 1308, 1849, 1794, 1781, 1703, 1713, 1522 }; + Vp8FixedCostsI4[0, 2] = new short[] { 142, 910, 762, 1684, 1849, 1576, 1460, 1305, 1801, 1657 }; + Vp8FixedCostsI4[0, 3] = new short[] { 559, 641, 1370, 421, 1182, 1569, 1612, 1725, 863, 1007 }; + Vp8FixedCostsI4[0, 4] = new short[] { 299, 1059, 1256, 1108, 636, 1068, 1581, 1883, 869, 1142 }; + Vp8FixedCostsI4[0, 5] = new short[] { 277, 1111, 707, 1362, 1089, 672, 1603, 1541, 1545, 1291 }; + Vp8FixedCostsI4[0, 6] = new short[] { 214, 781, 1609, 1303, 1632, 2229, 726, 1560, 1713, 918 }; + Vp8FixedCostsI4[0, 7] = new short[] { 152, 1037, 1046, 1759, 1983, 2174, 1358, 742, 1740, 1390 }; + Vp8FixedCostsI4[0, 8] = new short[] { 512, 1046, 1420, 753, 752, 1297, 1486, 1613, 460, 1207 }; + Vp8FixedCostsI4[0, 9] = new short[] { 424, 827, 1362, 719, 1462, 1202, 1199, 1476, 1199, 538 }; + Vp8FixedCostsI4[1, 0] = new short[] { 240, 402, 1134, 1491, 1659, 1505, 1517, 1555, 1979, 2099 }; + Vp8FixedCostsI4[1, 1] = new short[] { 467, 242, 960, 1232, 1714, 1620, 1834, 1570, 1676, 1391 }; + Vp8FixedCostsI4[1, 2] = new short[] { 500, 455, 463, 1507, 1699, 1282, 1564, 982, 2114, 2114 }; + Vp8FixedCostsI4[1, 3] = new short[] { 672, 643, 1372, 331, 1589, 1667, 1453, 1938, 996, 876 }; + Vp8FixedCostsI4[1, 4] = new short[] { 458, 783, 1037, 911, 738, 968, 1165, 1518, 859, 1033 }; + Vp8FixedCostsI4[1, 5] = new short[] { 504, 815, 504, 1139, 1219, 719, 1506, 1085, 1268, 1268 }; + Vp8FixedCostsI4[1, 6] = new short[] { 333, 630, 1445, 1239, 1883, 3672, 799, 1548, 1865, 598 }; + Vp8FixedCostsI4[1, 7] = new short[] { 399, 644, 746, 1342, 1856, 1350, 1493, 613, 1855, 1015 }; + Vp8FixedCostsI4[1, 8] = new short[] { 622, 749, 1205, 608, 1066, 1408, 1290, 1406, 546, 971 }; + Vp8FixedCostsI4[1, 9] = new short[] { 500, 753, 1041, 668, 1230, 1617, 1297, 1425, 1383, 523 }; + Vp8FixedCostsI4[2, 0] = new short[] { 394, 553, 523, 1502, 1536, 981, 1608, 1142, 1666, 2181 }; + Vp8FixedCostsI4[2, 1] = new short[] { 655, 430, 375, 1411, 1861, 1220, 1677, 1135, 1978, 1553 }; + Vp8FixedCostsI4[2, 2] = new short[] { 690, 640, 245, 1954, 2070, 1194, 1528, 982, 1972, 2232 }; + Vp8FixedCostsI4[2, 3] = new short[] { 559, 834, 741, 867, 1131, 980, 1225, 852, 1092, 784 }; + Vp8FixedCostsI4[2, 4] = new short[] { 690, 875, 516, 959, 673, 894, 1056, 1190, 1528, 1126 }; + Vp8FixedCostsI4[2, 5] = new short[] { 740, 951, 384, 1277, 1177, 492, 1579, 1155, 1846, 1513 }; + Vp8FixedCostsI4[2, 6] = new short[] { 323, 775, 1062, 1776, 3062, 1274, 813, 1188, 1372, 655 }; + Vp8FixedCostsI4[2, 7] = new short[] { 488, 971, 484, 1767, 1515, 1775, 1115, 503, 1539, 1461 }; + Vp8FixedCostsI4[2, 8] = new short[] { 740, 1006, 998, 709, 851, 1230, 1337, 788, 741, 721 }; + Vp8FixedCostsI4[2, 9] = new short[] { 522, 1073, 573, 1045, 1346, 887, 1046, 1146, 1203, 697 }; + Vp8FixedCostsI4[3, 0] = new short[] { 105, 864, 1442, 1009, 1934, 1840, 1519, 1920, 1673, 1579 }; + Vp8FixedCostsI4[3, 1] = new short[] { 534, 305, 1193, 683, 1388, 2164, 1802, 1894, 1264, 1170 }; + Vp8FixedCostsI4[3, 2] = new short[] { 305, 518, 877, 1108, 1426, 3215, 1425, 1064, 1320, 1242 }; + Vp8FixedCostsI4[3, 3] = new short[] { 683, 732, 1927, 257, 1493, 2048, 1858, 1552, 1055, 947 }; + Vp8FixedCostsI4[3, 4] = new short[] { 394, 814, 1024, 660, 959, 1556, 1282, 1289, 893, 1047 }; + Vp8FixedCostsI4[3, 5] = new short[] { 528, 615, 996, 940, 1201, 635, 1094, 2515, 803, 1358 }; + Vp8FixedCostsI4[3, 6] = new short[] { 347, 614, 1609, 1187, 3133, 1345, 1007, 1339, 1017, 667 }; + Vp8FixedCostsI4[3, 7] = new short[] { 218, 740, 878, 1605, 3650, 3650, 1345, 758, 1357, 1617 }; + Vp8FixedCostsI4[3, 8] = new short[] { 672, 750, 1541, 558, 1257, 1599, 1870, 2135, 402, 1087 }; + Vp8FixedCostsI4[3, 9] = new short[] { 592, 684, 1161, 430, 1092, 1497, 1475, 1489, 1095, 822 }; + Vp8FixedCostsI4[4, 0] = new short[] { 228, 1056, 1059, 1368, 752, 982, 1512, 1518, 987, 1782 }; + Vp8FixedCostsI4[4, 1] = new short[] { 494, 514, 818, 942, 965, 892, 1610, 1356, 1048, 1363 }; + Vp8FixedCostsI4[4, 2] = new short[] { 512, 648, 591, 1042, 761, 991, 1196, 1454, 1309, 1463 }; + Vp8FixedCostsI4[4, 3] = new short[] { 683, 749, 1043, 676, 841, 1396, 1133, 1138, 654, 939 }; + Vp8FixedCostsI4[4, 4] = new short[] { 622, 1101, 1126, 994, 361, 1077, 1203, 1318, 877, 1219 }; + Vp8FixedCostsI4[4, 5] = new short[] { 631, 1068, 857, 1650, 651, 477, 1650, 1419, 828, 1170 }; + Vp8FixedCostsI4[4, 6] = new short[] { 555, 727, 1068, 1335, 3127, 1339, 820, 1331, 1077, 429 }; + Vp8FixedCostsI4[4, 7] = new short[] { 504, 879, 624, 1398, 889, 889, 1392, 808, 891, 1406 }; + Vp8FixedCostsI4[4, 8] = new short[] { 683, 1602, 1289, 977, 578, 983, 1280, 1708, 406, 1122 }; + Vp8FixedCostsI4[4, 9] = new short[] { 399, 865, 1433, 1070, 1072, 764, 968, 1477, 1223, 678 }; + Vp8FixedCostsI4[5, 0] = new short[] { 333, 760, 935, 1638, 1010, 529, 1646, 1410, 1472, 2219 }; + Vp8FixedCostsI4[5, 1] = new short[] { 512, 494, 750, 1160, 1215, 610, 1870, 1868, 1628, 1169 }; + Vp8FixedCostsI4[5, 2] = new short[] { 572, 646, 492, 1934, 1208, 603, 1580, 1099, 1398, 1995 }; + Vp8FixedCostsI4[5, 3] = new short[] { 786, 789, 942, 581, 1018, 951, 1599, 1207, 731, 768 }; + Vp8FixedCostsI4[5, 4] = new short[] { 690, 1015, 672, 1078, 582, 504, 1693, 1438, 1108, 2897 }; + Vp8FixedCostsI4[5, 5] = new short[] { 768, 1267, 571, 2005, 1243, 244, 2881, 1380, 1786, 1453 }; + Vp8FixedCostsI4[5, 6] = new short[] { 452, 899, 1293, 903, 1311, 3100, 465, 1311, 1319, 813 }; + Vp8FixedCostsI4[5, 7] = new short[] { 394, 927, 942, 1103, 1358, 1104, 946, 593, 1363, 1109 }; + Vp8FixedCostsI4[5, 8] = new short[] { 559, 1005, 1007, 1016, 658, 1173, 1021, 1164, 623, 1028 }; + Vp8FixedCostsI4[5, 9] = new short[] { 564, 796, 632, 1005, 1014, 863, 2316, 1268, 938, 764 }; + Vp8FixedCostsI4[6, 0] = new short[] { 266, 606, 1098, 1228, 1497, 1243, 948, 1030, 1734, 1461 }; + Vp8FixedCostsI4[6, 1] = new short[] { 366, 585, 901, 1060, 1407, 1247, 876, 1134, 1620, 1054 }; + Vp8FixedCostsI4[6, 2] = new short[] { 452, 565, 542, 1729, 1479, 1479, 1016, 886, 2938, 1150 }; + Vp8FixedCostsI4[6, 3] = new short[] { 555, 1088, 1533, 950, 1354, 895, 834, 1019, 1021, 496 }; + Vp8FixedCostsI4[6, 4] = new short[] { 704, 815, 1193, 971, 973, 640, 1217, 2214, 832, 578 }; + Vp8FixedCostsI4[6, 5] = new short[] { 672, 1245, 579, 871, 875, 774, 872, 1273, 1027, 949 }; + Vp8FixedCostsI4[6, 6] = new short[] { 296, 1134, 2050, 1784, 1636, 3425, 442, 1550, 2076, 722 }; + Vp8FixedCostsI4[6, 7] = new short[] { 342, 982, 1259, 1846, 1848, 1848, 622, 568, 1847, 1052 }; + Vp8FixedCostsI4[6, 8] = new short[] { 555, 1064, 1304, 828, 746, 1343, 1075, 1329, 1078, 494 }; + Vp8FixedCostsI4[6, 9] = new short[] { 288, 1167, 1285, 1174, 1639, 1639, 833, 2254, 1304, 509 }; + Vp8FixedCostsI4[7, 0] = new short[] { 342, 719, 767, 1866, 1757, 1270, 1246, 550, 1746, 2151 }; + Vp8FixedCostsI4[7, 1] = new short[] { 483, 653, 694, 1509, 1459, 1410, 1218, 507, 1914, 1266 }; + Vp8FixedCostsI4[7, 2] = new short[] { 488, 757, 447, 2979, 1813, 1268, 1654, 539, 1849, 2109 }; + Vp8FixedCostsI4[7, 3] = new short[] { 522, 1097, 1085, 851, 1365, 1111, 851, 901, 961, 605 }; + Vp8FixedCostsI4[7, 4] = new short[] { 709, 716, 841, 728, 736, 945, 941, 862, 2845, 1057 }; + Vp8FixedCostsI4[7, 5] = new short[] { 512, 1323, 500, 1336, 1083, 681, 1342, 717, 1604, 1350 }; + Vp8FixedCostsI4[7, 6] = new short[] { 452, 1155, 1372, 1900, 1501, 3290, 311, 944, 1919, 922 }; + Vp8FixedCostsI4[7, 7] = new short[] { 403, 1520, 977, 2132, 1733, 3522, 1076, 276, 3335, 1547 }; + Vp8FixedCostsI4[7, 8] = new short[] { 559, 1374, 1101, 615, 673, 2462, 974, 795, 984, 984 }; + Vp8FixedCostsI4[7, 9] = new short[] { 547, 1122, 1062, 812, 1410, 951, 1140, 622, 1268, 651 }; + Vp8FixedCostsI4[8, 0] = new short[] { 165, 982, 1235, 938, 1334, 1366, 1659, 1578, 964, 1612 }; + Vp8FixedCostsI4[8, 1] = new short[] { 592, 422, 925, 847, 1139, 1112, 1387, 2036, 861, 1041 }; + Vp8FixedCostsI4[8, 2] = new short[] { 403, 837, 732, 770, 941, 1658, 1250, 809, 1407, 1407 }; + Vp8FixedCostsI4[8, 3] = new short[] { 896, 874, 1071, 381, 1568, 1722, 1437, 2192, 480, 1035 }; + Vp8FixedCostsI4[8, 4] = new short[] { 640, 1098, 1012, 1032, 684, 1382, 1581, 2106, 416, 865 }; + Vp8FixedCostsI4[8, 5] = new short[] { 559, 1005, 819, 914, 710, 770, 1418, 920, 838, 1435 }; + Vp8FixedCostsI4[8, 6] = new short[] { 415, 1258, 1245, 870, 1278, 3067, 770, 1021, 1287, 522 }; + Vp8FixedCostsI4[8, 7] = new short[] { 406, 990, 601, 1009, 1265, 1265, 1267, 759, 1017, 1277 }; + Vp8FixedCostsI4[8, 8] = new short[] { 968, 1182, 1329, 788, 1032, 1292, 1705, 1714, 203, 1403 }; + Vp8FixedCostsI4[8, 9] = new short[] { 732, 877, 1279, 471, 901, 1161, 1545, 1294, 755, 755 }; + Vp8FixedCostsI4[9, 0] = new short[] { 111, 931, 1378, 1185, 1933, 1648, 1148, 1714, 1873, 1307 }; + Vp8FixedCostsI4[9, 1] = new short[] { 406, 414, 1030, 1023, 1910, 1404, 1313, 1647, 1509, 793 }; + Vp8FixedCostsI4[9, 2] = new short[] { 342, 640, 575, 1088, 1241, 1349, 1161, 1350, 1756, 1502 }; + Vp8FixedCostsI4[9, 3] = new short[] { 559, 766, 1185, 357, 1682, 1428, 1329, 1897, 1219, 802 }; + Vp8FixedCostsI4[9, 4] = new short[] { 473, 909, 1164, 771, 719, 2508, 1427, 1432, 722, 782 }; + Vp8FixedCostsI4[9, 5] = new short[] { 342, 892, 785, 1145, 1150, 794, 1296, 1550, 973, 1057 }; + Vp8FixedCostsI4[9, 6] = new short[] { 208, 1036, 1326, 1343, 1606, 3395, 815, 1455, 1618, 712 }; + Vp8FixedCostsI4[9, 7] = new short[] { 228, 928, 890, 1046, 3499, 1711, 994, 829, 1720, 1318 }; + Vp8FixedCostsI4[9, 8] = new short[] { 768, 724, 1058, 636, 991, 1075, 1319, 1324, 616, 825 }; + Vp8FixedCostsI4[9, 9] = new short[] { 305, 1167, 1358, 899, 1587, 1587, 987, 1988, 1332, 501 }; + } + } +} diff --git a/src/ImageSharp/Formats/Webp/WebpMetadata.cs b/src/ImageSharp/Formats/Webp/WebpMetadata.cs new file mode 100644 index 000000000..f398d3d87 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/WebpMetadata.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +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.FileFormat = other.FileFormat; + + /// + /// Gets or sets the webp file format used. Either lossless or lossy. + /// + public WebpFileFormatType? FileFormat { get; set; } + + /// + 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 000000000..fffdd3410 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/WebpThrowHelper.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// 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); + + /// + /// Cold path optimization for throwing -s + /// + /// The error message for the exception. + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowInvalidImageDimensions(string errorMessage) => throw new InvalidImageContentException(errorMessage); + } +} diff --git a/src/ImageSharp/Formats/Webp/WebpTransparentColorMode.cs b/src/ImageSharp/Formats/Webp/WebpTransparentColorMode.cs new file mode 100644 index 000000000..993033b80 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/WebpTransparentColorMode.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Webp +{ + /// + /// Enum indicating how the transparency should be handled on encoding. + /// + public enum WebpTransparentColorMode + { + /// + /// Discard the transparency information for better compression. + /// + Clear = 0, + + /// + /// The transparency will be kept as is. + /// + Preserve = 1, + } +} diff --git a/src/ImageSharp/Formats/Webp/Webp_Container_Specification.pdf b/src/ImageSharp/Formats/Webp/Webp_Container_Specification.pdf new file mode 100644 index 000000000..e237cb384 Binary files /dev/null and b/src/ImageSharp/Formats/Webp/Webp_Container_Specification.pdf differ diff --git a/tests/ImageSharp.Benchmarks/Codecs/DecodeTiff.cs b/tests/ImageSharp.Benchmarks/Codecs/DecodeTiff.cs index 8deaffd5c..db94fb121 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/DecodeTiff.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/DecodeTiff.cs @@ -7,7 +7,6 @@ using System.IO; using BenchmarkDotNet.Attributes; - using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests; using SDImage = System.Drawing.Image; @@ -24,8 +23,6 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs private byte[] data; - private Configuration configuration; - #if BIG_TESTS private static readonly int BufferSize = 1024 * 68; @@ -59,16 +56,6 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs public string TestImage { get; set; } #endif - [GlobalSetup] - public void Config() - { - if (this.configuration == null) - { - this.configuration = new Configuration(); - this.configuration.StreamProcessingBufferSize = BufferSize; - } - } - [IterationSetup] public void ReadImages() { @@ -93,7 +80,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs public Size TiffCore() { using (var ms = new MemoryStream(this.data)) - using (var image = Image.Load(this.configuration, ms)) + using (var image = Image.Load(ms)) { return image.Size(); } diff --git a/tests/ImageSharp.Benchmarks/Codecs/DecodeWebp.cs b/tests/ImageSharp.Benchmarks/Codecs/DecodeWebp.cs new file mode 100644 index 000000000..407a4ef3b --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/DecodeWebp.cs @@ -0,0 +1,109 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using BenchmarkDotNet.Attributes; + +using ImageMagick; +using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs +{ + [MarkdownExporter] + [HtmlExporter] + [Config(typeof(Config.ShortMultiFramework))] + public class DecodeWebp + { + private Configuration configuration; + + 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.Earth)] + public string TestImageLossy { get; set; } + + [Params(TestImages.Webp.Lossless.Earth)] + public string TestImageLossless { get; set; } + + [GlobalSetup] + public void ReadImages() + { + this.configuration = Configuration.CreateDefaultInstance(); + new WebpConfigurationModule().Configure(this.configuration); + + this.webpLossyBytes ??= File.ReadAllBytes(this.TestImageLossyFullPath); + this.webpLosslessBytes ??= File.ReadAllBytes(this.TestImageLosslessFullPath); + } + + [Benchmark(Description = "Magick Lossy Webp")] + public int WebpLossyMagick() + { + var settings = new MagickReadSettings { Format = MagickFormat.WebP }; + using var memoryStream = new MemoryStream(this.webpLossyBytes); + using var image = new MagickImage(memoryStream, settings); + return image.Width; + } + + [Benchmark(Description = "ImageSharp Lossy Webp")] + public int WebpLossy() + { + using var memoryStream = new MemoryStream(this.webpLossyBytes); + using var image = Image.Load(this.configuration, memoryStream); + return image.Height; + } + + [Benchmark(Description = "Magick Lossless Webp")] + public int WebpLosslessMagick() + { + var settings = new MagickReadSettings { Format = MagickFormat.WebP }; + using var memoryStream = new MemoryStream(this.webpLossyBytes); + using var image = new MagickImage(memoryStream, settings); + return image.Width; + } + + [Benchmark(Description = "ImageSharp Lossless Webp")] + public int WebpLossless() + { + using var memoryStream = new MemoryStream(this.webpLosslessBytes); + using var image = Image.Load(this.configuration, memoryStream); + return image.Height; + } + + /* Results 17.06.2021 + * BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362 + Intel Core i7-6700K CPU 4.00GHz (Skylake), 1 CPU, 8 logical and 4 physical cores + .NET Core SDK=3.1.202 + [Host] : .NET Core 3.1.4 (CoreCLR 4.700.20.20201, CoreFX 4.700.20.22101), X64 RyuJIT + Job-AQFZAV : .NET Framework 4.8 (4.8.4180.0), X64 RyuJIT + Job-YCDAPQ : .NET Core 2.1.18 (CoreCLR 4.6.28801.04, CoreFX 4.6.28802.05), X64 RyuJIT + Job-WMTYOZ : .NET Core 3.1.4 (CoreCLR 4.700.20.20201, CoreFX 4.700.20.22101), X64 RyuJIT + + IterationCount=3 LaunchCount=1 WarmupCount=3 + | Method | Job | Runtime | TestImageLossy | TestImageLossless | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated | + |--------------------------- |----------- |-------------- |---------------------- |------------------------- |-----------:|----------:|---------:|----------:|----------:|------:|------------:| + | 'Magick Lossy Webp' | Job-IERNAB | .NET 4.7.2 | Webp/earth_lossy.webp | Webp/earth_lossless.webp | 105.8 ms | 6.28 ms | 0.34 ms | - | - | - | 17.65 KB | + | 'ImageSharp Lossy Webp' | Job-IERNAB | .NET 4.7.2 | Webp/earth_lossy.webp | Webp/earth_lossless.webp | 1,145.0 ms | 110.82 ms | 6.07 ms | - | - | - | 2779.53 KB | + | 'Magick Lossless Webp' | Job-IERNAB | .NET 4.7.2 | Webp/earth_lossy.webp | Webp/earth_lossless.webp | 145.9 ms | 8.55 ms | 0.47 ms | - | - | - | 18.05 KB | + | 'ImageSharp Lossless Webp' | Job-IERNAB | .NET 4.7.2 | Webp/earth_lossy.webp | Webp/earth_lossless.webp | 1,694.1 ms | 55.09 ms | 3.02 ms | 4000.0000 | 1000.0000 | - | 30556.87 KB | + | 'Magick Lossy Webp' | Job-IMRAGJ | .NET Core 2.1 | Webp/earth_lossy.webp | Webp/earth_lossless.webp | 105.7 ms | 1.89 ms | 0.10 ms | - | - | - | 15.75 KB | + | 'ImageSharp Lossy Webp' | Job-IMRAGJ | .NET Core 2.1 | Webp/earth_lossy.webp | Webp/earth_lossless.webp | 741.6 ms | 21.45 ms | 1.18 ms | - | - | - | 2767.85 KB | + | 'Magick Lossless Webp' | Job-IMRAGJ | .NET Core 2.1 | Webp/earth_lossy.webp | Webp/earth_lossless.webp | 146.1 ms | 9.52 ms | 0.52 ms | - | - | - | 16.54 KB | + | 'ImageSharp Lossless Webp' | Job-IMRAGJ | .NET Core 2.1 | Webp/earth_lossy.webp | Webp/earth_lossless.webp | 522.5 ms | 21.15 ms | 1.16 ms | 4000.0000 | 1000.0000 | - | 22860.02 KB | + | 'Magick Lossy Webp' | Job-NAASQX | .NET Core 3.1 | Webp/earth_lossy.webp | Webp/earth_lossless.webp | 105.9 ms | 5.34 ms | 0.29 ms | - | - | - | 15.45 KB | + | 'ImageSharp Lossy Webp' | Job-NAASQX | .NET Core 3.1 | Webp/earth_lossy.webp | Webp/earth_lossless.webp | 748.8 ms | 290.47 ms | 15.92 ms | - | - | - | 2767.84 KB | + | 'Magick Lossless Webp' | Job-NAASQX | .NET Core 3.1 | Webp/earth_lossy.webp | Webp/earth_lossless.webp | 146.1 ms | 1.14 ms | 0.06 ms | - | - | - | 15.9 KB | + | 'ImageSharp Lossless Webp' | Job-NAASQX | .NET Core 3.1 | Webp/earth_lossy.webp | Webp/earth_lossless.webp | 480.7 ms | 25.25 ms | 1.38 ms | 4000.0000 | 1000.0000 | - | 22859.7 KB | + | 'Magick Lossy Webp' | Job-GLNACU | .NET Core 5.0 | Webp/earth_lossy.webp | Webp/earth_lossless.webp | 105.7 ms | 4.71 ms | 0.26 ms | - | - | - | 15.48 KB | + | 'ImageSharp Lossy Webp' | Job-GLNACU | .NET Core 5.0 | Webp/earth_lossy.webp | Webp/earth_lossless.webp | 645.7 ms | 61.00 ms | 3.34 ms | - | - | - | 2768.13 KB | + | 'Magick Lossless Webp' | Job-GLNACU | .NET Core 5.0 | Webp/earth_lossy.webp | Webp/earth_lossless.webp | 146.5 ms | 18.63 ms | 1.02 ms | - | - | - | 15.8 KB | + | 'ImageSharp Lossless Webp' | Job-GLNACU | .NET Core 5.0 | Webp/earth_lossy.webp | Webp/earth_lossless.webp | 306.7 ms | 32.31 ms | 1.77 ms | 4000.0000 | 1000.0000 | - | 22860.02 KB | + */ + } +} diff --git a/tests/ImageSharp.Benchmarks/Codecs/EncodeTga.cs b/tests/ImageSharp.Benchmarks/Codecs/EncodeTga.cs index 0e320e4a7..e7b102547 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/EncodeTga.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/EncodeTga.cs @@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs public class EncodeTga { private MagickImage tgaMagick; - private Image tgaCore; + private Image tga; private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage); @@ -24,9 +24,9 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs [GlobalSetup] public void ReadImages() { - if (this.tgaCore == null) + if (this.tga == null) { - this.tgaCore = Image.Load(this.TestImageFullPath); + this.tga = Image.Load(this.TestImageFullPath); this.tgaMagick = new MagickImage(this.TestImageFullPath); } } @@ -34,23 +34,23 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs [GlobalCleanup] public void Cleanup() { - this.tgaCore.Dispose(); - this.tgaCore = null; + this.tga.Dispose(); + this.tga = null; this.tgaMagick.Dispose(); } [Benchmark(Baseline = true, Description = "Magick Tga")] - public void TgaMagick() + public void MagickTga() { using var memoryStream = new MemoryStream(); this.tgaMagick.Write(memoryStream, MagickFormat.Tga); } [Benchmark(Description = "ImageSharp Tga")] - public void TgaImageSharp() + public void ImageSharpTga() { using var memoryStream = new MemoryStream(); - this.tgaCore.SaveAsTga(memoryStream); + this.tga.SaveAsTga(memoryStream); } } } diff --git a/tests/ImageSharp.Benchmarks/Codecs/EncodeTiff.cs b/tests/ImageSharp.Benchmarks/Codecs/EncodeTiff.cs index 89094b768..e9c61e729 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/EncodeTiff.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/EncodeTiff.cs @@ -72,12 +72,6 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs { TiffPhotometricInterpretation photometricInterpretation = TiffPhotometricInterpretation.Rgb; - // Workaround for 1-bit bug - if (this.Compression == TiffCompression.CcittGroup3Fax || this.Compression == TiffCompression.Ccitt1D) - { - photometricInterpretation = TiffPhotometricInterpretation.WhiteIsZero; - } - var encoder = new TiffEncoder() { Compression = this.Compression, PhotometricInterpretation = photometricInterpretation }; using var memoryStream = new MemoryStream(); this.core.SaveAsTiff(memoryStream, encoder); diff --git a/tests/ImageSharp.Benchmarks/Codecs/EncodeWebp.cs b/tests/ImageSharp.Benchmarks/Codecs/EncodeWebp.cs new file mode 100644 index 000000000..7d3dfe693 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/EncodeWebp.cs @@ -0,0 +1,112 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using BenchmarkDotNet.Attributes; +using ImageMagick; +using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs +{ + [MarkdownExporter] + [HtmlExporter] + [Config(typeof(Config.ShortMultiFramework))] + public class EncodeWebp + { + private MagickImage webpMagick; + private Image webp; + + private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage); + + [Params(TestImages.Png.Bike)] // The bike image will have all 3 transforms as lossless webp. + public string TestImage { get; set; } + + [GlobalSetup] + public void ReadImages() + { + if (this.webp == null) + { + this.webp = Image.Load(this.TestImageFullPath); + this.webpMagick = new MagickImage(this.TestImageFullPath); + } + } + + [GlobalCleanup] + public void Cleanup() + { + this.webp.Dispose(); + this.webpMagick.Dispose(); + } + + [Benchmark(Description = "Magick Webp Lossy")] + public void MagickWebpLossy() + { + using var memoryStream = new MemoryStream(); + this.webpMagick.Settings.SetDefine(MagickFormat.WebP, "lossless", false); + this.webpMagick.Write(memoryStream, MagickFormat.WebP); + } + + [Benchmark(Description = "ImageSharp Webp Lossy")] + public void ImageSharpWebpLossy() + { + using var memoryStream = new MemoryStream(); + this.webp.Save(memoryStream, new WebpEncoder() + { + FileFormat = WebpFileFormatType.Lossy + }); + } + + [Benchmark(Baseline = true, Description = "Magick Webp Lossless")] + public void MagickWebpLossless() + { + using var memoryStream = new MemoryStream(); + this.webpMagick.Settings.SetDefine(MagickFormat.WebP, "lossless", true); + this.webpMagick.Write(memoryStream, MagickFormat.WebP); + } + + [Benchmark(Description = "ImageSharp Webp Lossless")] + public void ImageSharpWebpLossless() + { + using var memoryStream = new MemoryStream(); + this.webp.Save(memoryStream, new WebpEncoder() + { + FileFormat = WebpFileFormatType.Lossless + }); + } + + /* Results 17.06.2021 + * Summary * + BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.630 (2004/?/20H1) + Intel Core i7-6700K CPU 4.00GHz (Skylake), 1 CPU, 8 logical and 4 physical cores + .NET Core SDK=5.0.100 + [Host] : .NET Core 3.1.9 (CoreCLR 4.700.20.47201, CoreFX 4.700.20.47203), X64 RyuJIT + Job-OUUGWL : .NET Framework 4.8 (4.8.4250.0), X64 RyuJIT + Job-GAIITM : .NET Core 2.1.23 (CoreCLR 4.6.29321.03, CoreFX 4.6.29321.01), X64 RyuJIT + Job-HWOBSO : .NET Core 3.1.9 (CoreCLR 4.700.20.47201, CoreFX 4.700.20.47203), X64 RyuJIT + + | Method | Job | Runtime | TestImage | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |--------------------------- |----------- |-------------- |------------- |----------:|-----------:|----------:|------:|--------:|-----------:|----------:|----------:|-------------:| + | 'Magick Webp Lossy' | Job-RYVNHD | .NET 4.7.2 | Png/Bike.png | 23.30 ms | 0.869 ms | 0.048 ms | 0.14 | 0.00 | - | - | - | 68.19 KB | + | 'ImageSharp Webp Lossy' | Job-RYVNHD | .NET 4.7.2 | Png/Bike.png | 68.22 ms | 16.454 ms | 0.902 ms | 0.42 | 0.01 | 6125.0000 | 125.0000 | - | 26359.49 KB | + | 'Magick Webp Lossless' | Job-RYVNHD | .NET 4.7.2 | Png/Bike.png | 161.96 ms | 9.879 ms | 0.541 ms | 1.00 | 0.00 | - | - | - | 520.28 KB | + | 'ImageSharp Webp Lossless' | Job-RYVNHD | .NET 4.7.2 | Png/Bike.png | 370.88 ms | 58.875 ms | 3.227 ms | 2.29 | 0.02 | 34000.0000 | 5000.0000 | 2000.0000 | 163177.15 KB | + | | | | | | | | | | | | | | + | 'Magick Webp Lossy' | Job-GOZXWU | .NET Core 2.1 | Png/Bike.png | 23.35 ms | 0.428 ms | 0.023 ms | 0.14 | 0.00 | - | - | - | 67.76 KB | + | 'ImageSharp Webp Lossy' | Job-GOZXWU | .NET Core 2.1 | Png/Bike.png | 43.95 ms | 2.850 ms | 0.156 ms | 0.27 | 0.00 | 6250.0000 | 250.0000 | 83.3333 | 26284.72 KB | + | 'Magick Webp Lossless' | Job-GOZXWU | .NET Core 2.1 | Png/Bike.png | 161.44 ms | 3.749 ms | 0.206 ms | 1.00 | 0.00 | - | - | - | 519.26 KB | + | 'ImageSharp Webp Lossless' | Job-GOZXWU | .NET Core 2.1 | Png/Bike.png | 335.78 ms | 78.666 ms | 4.312 ms | 2.08 | 0.03 | 34000.0000 | 5000.0000 | 2000.0000 | 162727.56 KB | + | | | | | | | | | | | | | | + | 'Magick Webp Lossy' | Job-VRDVKW | .NET Core 3.1 | Png/Bike.png | 23.48 ms | 4.325 ms | 0.237 ms | 0.15 | 0.00 | - | - | - | 67.66 KB | + | 'ImageSharp Webp Lossy' | Job-VRDVKW | .NET Core 3.1 | Png/Bike.png | 43.29 ms | 16.503 ms | 0.905 ms | 0.27 | 0.01 | 6272.7273 | 272.7273 | 90.9091 | 26284.86 KB | + | 'Magick Webp Lossless' | Job-VRDVKW | .NET Core 3.1 | Png/Bike.png | 161.81 ms | 10.693 ms | 0.586 ms | 1.00 | 0.00 | - | - | - | 523.25 KB | + | 'ImageSharp Webp Lossless' | Job-VRDVKW | .NET Core 3.1 | Png/Bike.png | 323.97 ms | 235.468 ms | 12.907 ms | 2.00 | 0.08 | 34000.0000 | 5000.0000 | 2000.0000 | 162724.84 KB | + | | | | | | | | | | | | | | + | 'Magick Webp Lossy' | Job-ZJRLRB | .NET Core 5.0 | Png/Bike.png | 23.36 ms | 0.448 ms | 0.025 ms | 0.14 | 0.00 | - | - | - | 67.66 KB | + | 'ImageSharp Webp Lossy' | Job-ZJRLRB | .NET Core 5.0 | Png/Bike.png | 40.11 ms | 2.465 ms | 0.135 ms | 0.25 | 0.00 | 6307.6923 | 230.7692 | 76.9231 | 26284.71 KB | + | 'Magick Webp Lossless' | Job-ZJRLRB | .NET Core 5.0 | Png/Bike.png | 161.55 ms | 6.662 ms | 0.365 ms | 1.00 | 0.00 | - | - | - | 518.84 KB | + | 'ImageSharp Webp Lossless' | Job-ZJRLRB | .NET Core 5.0 | Png/Bike.png | 298.73 ms | 17.953 ms | 0.984 ms | 1.85 | 0.01 | 34000.0000 | 5000.0000 | 2000.0000 | 162725.13 KB | + */ + } +} diff --git a/tests/ImageSharp.Benchmarks/Config.cs b/tests/ImageSharp.Benchmarks/Config.cs index ccc4a7b9f..9221bb7fd 100644 --- a/tests/ImageSharp.Benchmarks/Config.cs +++ b/tests/ImageSharp.Benchmarks/Config.cs @@ -33,14 +33,16 @@ namespace SixLabors.ImageSharp.Benchmarks { public MultiFramework() => this.AddJob( Job.Default.WithRuntime(ClrRuntime.Net472), - Job.Default.WithRuntime(CoreRuntime.Core31)); + Job.Default.WithRuntime(CoreRuntime.Core31), + Job.Default.WithRuntime(CoreRuntime.Core50)); } public class ShortMultiFramework : Config { public ShortMultiFramework() => this.AddJob( Job.Default.WithRuntime(ClrRuntime.Net472).WithLaunchCount(1).WithWarmupCount(3).WithIterationCount(3), - Job.Default.WithRuntime(CoreRuntime.Core31).WithLaunchCount(1).WithWarmupCount(3).WithIterationCount(3)); + Job.Default.WithRuntime(CoreRuntime.Core31).WithLaunchCount(1).WithWarmupCount(3).WithIterationCount(3), + Job.Default.WithRuntime(CoreRuntime.Core50).WithLaunchCount(1).WithWarmupCount(3).WithIterationCount(3)); } public class ShortCore31 : Config diff --git a/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs b/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs index 1f680aa6c..f61e2dc8e 100644 --- a/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs +++ b/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs @@ -19,10 +19,7 @@ namespace SixLabors.ImageSharp.Tests.Common { private ITestOutputHelper Output { get; } - public SimdUtilsTests(ITestOutputHelper output) - { - this.Output = output; - } + public SimdUtilsTests(ITestOutputHelper output) => this.Output = output; private static int R(float f) => (int)Math.Round(f, MidpointRounding.AwayFromZero); @@ -63,7 +60,7 @@ namespace SixLabors.ImageSharp.Tests.Common private static Vector CreateRandomTestVector(int seed, float min, float max) { - var data = new float[Vector.Count]; + float[] data = new float[Vector.Count]; var rnd = new Random(seed); @@ -154,7 +151,7 @@ namespace SixLabors.ImageSharp.Tests.Common float[] source = new Random(seed).GenerateRandomFloatArray(count, 0, 1f); - var dest = new byte[count]; + byte[] dest = new byte[count]; SimdUtils.BasicIntrinsics256.BulkConvertNormalizedFloatToByte(source, dest); @@ -163,25 +160,18 @@ namespace SixLabors.ImageSharp.Tests.Common Assert.Equal(expected, dest); } - public static readonly TheoryData ArraySizesDivisibleBy8 = new TheoryData { 0, 8, 16, 1024 }; - public static readonly TheoryData ArraySizesDivisibleBy4 = new TheoryData { 0, 4, 8, 28, 1020 }; - public static readonly TheoryData ArraySizesDivisibleBy3 = new TheoryData { 0, 3, 9, 36, 957 }; - public static readonly TheoryData ArraySizesDivisibleBy32 = new TheoryData { 0, 32, 512 }; + public static readonly TheoryData ArraySizesDivisibleBy8 = new() { 0, 8, 16, 1024 }; + public static readonly TheoryData ArraySizesDivisibleBy4 = new() { 0, 4, 8, 28, 1020 }; + public static readonly TheoryData ArraySizesDivisibleBy3 = new() { 0, 3, 9, 36, 957 }; + public static readonly TheoryData ArraySizesDivisibleBy32 = new() { 0, 32, 512 }; - public static readonly TheoryData ArbitraryArraySizes = - new TheoryData - { - 0, 1, 2, 3, 4, 7, 8, 9, 15, 16, 17, 63, 64, 255, 511, 512, 513, 514, 515, 516, 517, 518, 519, 520, - }; + public static readonly TheoryData ArbitraryArraySizes = new() { 0, 1, 2, 3, 4, 7, 8, 9, 15, 16, 17, 63, 64, 255, 511, 512, 513, 514, 515, 516, 517, 518, 519, 520 }; [Theory] [MemberData(nameof(ArraySizesDivisibleBy4))] - public void FallbackIntrinsics128_BulkConvertByteToNormalizedFloat(int count) - { - TestImpl_BulkConvertByteToNormalizedFloat( + public void FallbackIntrinsics128_BulkConvertByteToNormalizedFloat(int count) => TestImpl_BulkConvertByteToNormalizedFloat( count, (s, d) => SimdUtils.FallbackIntrinsics128.ByteToNormalizedFloat(s.Span, d.Span)); - } [Theory] [MemberData(nameof(ArraySizesDivisibleBy8))] @@ -199,24 +189,23 @@ namespace SixLabors.ImageSharp.Tests.Common [Theory] [MemberData(nameof(ArraySizesDivisibleBy32))] - public void ExtendedIntrinsics_BulkConvertByteToNormalizedFloat(int count) - { - TestImpl_BulkConvertByteToNormalizedFloat( + public void ExtendedIntrinsics_BulkConvertByteToNormalizedFloat(int count) => TestImpl_BulkConvertByteToNormalizedFloat( count, (s, d) => SimdUtils.ExtendedIntrinsics.ByteToNormalizedFloat(s.Span, d.Span)); - } #if SUPPORTS_RUNTIME_INTRINSICS [Theory] [MemberData(nameof(ArraySizesDivisibleBy32))] public void HwIntrinsics_BulkConvertByteToNormalizedFloat(int count) { - static void RunTest(string serialized) + if (!Sse2.IsSupported) { - TestImpl_BulkConvertByteToNormalizedFloat( + return; + } + + static void RunTest(string serialized) => TestImpl_BulkConvertByteToNormalizedFloat( FeatureTestRunner.Deserialize(serialized), (s, d) => SimdUtils.HwIntrinsics.ByteToNormalizedFloat(s.Span, d.Span)); - } FeatureTestRunner.RunWithHwIntrinsicsFeature( RunTest, @@ -227,20 +216,17 @@ namespace SixLabors.ImageSharp.Tests.Common [Theory] [MemberData(nameof(ArbitraryArraySizes))] - public void BulkConvertByteToNormalizedFloat(int count) - { - TestImpl_BulkConvertByteToNormalizedFloat( + public void BulkConvertByteToNormalizedFloat(int count) => TestImpl_BulkConvertByteToNormalizedFloat( count, (s, d) => SimdUtils.ByteToNormalizedFloat(s.Span, d.Span)); - } private static void TestImpl_BulkConvertByteToNormalizedFloat( int count, Action, Memory> convert) { byte[] source = new Random(count).GenerateRandomByteArray(count); - var result = new float[count]; - float[] expected = source.Select(b => (float)b / 255f).ToArray(); + float[] result = new float[count]; + float[] expected = source.Select(b => b / 255f).ToArray(); convert(source, result); @@ -249,12 +235,9 @@ namespace SixLabors.ImageSharp.Tests.Common [Theory] [MemberData(nameof(ArraySizesDivisibleBy4))] - public void FallbackIntrinsics128_BulkConvertNormalizedFloatToByteClampOverflows(int count) - { - TestImpl_BulkConvertNormalizedFloatToByteClampOverflows( + public void FallbackIntrinsics128_BulkConvertNormalizedFloatToByteClampOverflows(int count) => TestImpl_BulkConvertNormalizedFloatToByteClampOverflows( count, (s, d) => SimdUtils.FallbackIntrinsics128.NormalizedFloatToByteSaturate(s.Span, d.Span)); - } [Theory] [MemberData(nameof(ArraySizesDivisibleBy8))] @@ -270,12 +253,9 @@ namespace SixLabors.ImageSharp.Tests.Common [Theory] [MemberData(nameof(ArraySizesDivisibleBy32))] - public void ExtendedIntrinsics_BulkConvertNormalizedFloatToByteClampOverflows(int count) - { - TestImpl_BulkConvertNormalizedFloatToByteClampOverflows( + public void ExtendedIntrinsics_BulkConvertNormalizedFloatToByteClampOverflows(int count) => TestImpl_BulkConvertNormalizedFloatToByteClampOverflows( count, (s, d) => SimdUtils.ExtendedIntrinsics.NormalizedFloatToByteSaturate(s.Span, d.Span)); - } [Theory] [InlineData(1234)] @@ -304,12 +284,14 @@ namespace SixLabors.ImageSharp.Tests.Common [MemberData(nameof(ArraySizesDivisibleBy32))] public void HwIntrinsics_BulkConvertNormalizedFloatToByteClampOverflows(int count) { - static void RunTest(string serialized) + if (!Sse2.IsSupported) { - TestImpl_BulkConvertNormalizedFloatToByteClampOverflows( + return; + } + + static void RunTest(string serialized) => TestImpl_BulkConvertNormalizedFloatToByteClampOverflows( FeatureTestRunner.Deserialize(serialized), (s, d) => SimdUtils.HwIntrinsics.NormalizedFloatToByteSaturate(s.Span, d.Span)); - } FeatureTestRunner.RunWithHwIntrinsicsFeature( RunTest, @@ -326,7 +308,7 @@ namespace SixLabors.ImageSharp.Tests.Common TestImpl_BulkConvertNormalizedFloatToByteClampOverflows(count, (s, d) => SimdUtils.NormalizedFloatToByteSaturate(s.Span, d.Span)); // For small values, let's stress test the implementation a bit: - if (count > 0 && count < 10) + if (count is > 0 and < 10) { for (int i = 0; i < 20; i++) { @@ -340,23 +322,17 @@ namespace SixLabors.ImageSharp.Tests.Common [Theory] [MemberData(nameof(ArbitraryArraySizes))] - public void PackFromRgbPlanes_Rgb24(int count) - { - TestPackFromRgbPlanes( + public void PackFromRgbPlanes_Rgb24(int count) => TestPackFromRgbPlanes( count, (r, g, b, actual) => SimdUtils.PackFromRgbPlanes(Configuration.Default, r, g, b, actual)); - } [Theory] [MemberData(nameof(ArbitraryArraySizes))] - public void PackFromRgbPlanes_Rgba32(int count) - { - TestPackFromRgbPlanes( + public void PackFromRgbPlanes_Rgba32(int count) => TestPackFromRgbPlanes( count, (r, g, b, actual) => SimdUtils.PackFromRgbPlanes(Configuration.Default, r, g, b, actual)); - } #if SUPPORTS_RUNTIME_INTRINSICS [Fact] @@ -371,7 +347,7 @@ namespace SixLabors.ImageSharp.Tests.Common byte[] g = Enumerable.Range(100, 32).Select(x => (byte)x).ToArray(); byte[] b = Enumerable.Range(200, 32).Select(x => (byte)x).ToArray(); const int padding = 4; - Rgb24[] d = new Rgb24[32 + padding]; + var d = new Rgb24[32 + padding]; ReadOnlySpan rr = r.AsSpan(); ReadOnlySpan gg = g.AsSpan(); @@ -405,7 +381,7 @@ namespace SixLabors.ImageSharp.Tests.Common byte[] g = Enumerable.Range(100, 32).Select(x => (byte)x).ToArray(); byte[] b = Enumerable.Range(200, 32).Select(x => (byte)x).ToArray(); - Rgba32[] d = new Rgba32[32]; + var d = new Rgba32[32]; ReadOnlySpan rr = r.AsSpan(); ReadOnlySpan gg = g.AsSpan(); @@ -432,18 +408,18 @@ namespace SixLabors.ImageSharp.Tests.Common internal static void TestPackFromRgbPlanes(int count, Action packMethod) where TPixel : unmanaged, IPixel { - Random rnd = new Random(42); + var rnd = new Random(42); byte[] r = rnd.GenerateRandomByteArray(count); byte[] g = rnd.GenerateRandomByteArray(count); byte[] b = rnd.GenerateRandomByteArray(count); - TPixel[] expected = new TPixel[count]; + var expected = new TPixel[count]; for (int i = 0; i < count; i++) { expected[i].FromRgb24(new Rgb24(r[i], g[i], b[i])); } - TPixel[] actual = new TPixel[count + 3]; // padding for Rgb24 AVX2 + var actual = new TPixel[count + 3]; // padding for Rgb24 AVX2 packMethod(r, g, b, actual); Assert.True(expected.AsSpan().SequenceEqual(actual.AsSpan().Slice(0, count))); diff --git a/tests/ImageSharp.Tests/ConfigurationTests.cs b/tests/ImageSharp.Tests/ConfigurationTests.cs index 3ad8ef2f8..803babdfa 100644 --- a/tests/ImageSharp.Tests/ConfigurationTests.cs +++ b/tests/ImageSharp.Tests/ConfigurationTests.cs @@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Tests public Configuration DefaultConfiguration { get; } - private readonly int expectedDefaultConfigurationCount = 6; + private readonly int expectedDefaultConfigurationCount = 7; public ConfigurationTests() { diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs index fb371474d..4e6dc36dc 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs @@ -54,7 +54,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp } [Theory] - [WithFileCollection(nameof(MiscBmpFiles), PixelTypes.Rgba32)] + [WithFile(Car, PixelTypes.Rgba32)] + [WithFile(F, PixelTypes.Rgba32)] public void BmpDecoder_CanDecode_MiscellaneousBitmaps_WithLimitedAllocatorBufferCapacity( TestImageProvider provider) { diff --git a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs index c0843a51b..bf13a9097 100644 --- a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs +++ b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs @@ -16,6 +16,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Formats { + [Collection("RunSerial")] public class GeneralFormatTests { /// @@ -152,7 +153,7 @@ namespace SixLabors.ImageSharp.Tests.Formats using (FileStream output = File.OpenWrite(Path.Combine(path, $"{file.FileNameWithoutExtension}.tiff"))) { - image.SaveAsTga(output); + image.SaveAsTiff(output); } } } diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs index 9cb16946b..59f7ebb74 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs @@ -12,6 +12,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Formats.Gif { + [Collection("RunSerial")] [Trait("Format", "Gif")] public class GifMetadataTests { diff --git a/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs b/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs index 1e00bfff8..5cd70b100 100644 --- a/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs +++ b/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs @@ -12,6 +12,7 @@ using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.PixelFormats; using Xunit; @@ -38,6 +39,7 @@ namespace SixLabors.ImageSharp.Tests.Formats Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); + Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType().Count()); @@ -45,6 +47,7 @@ namespace SixLabors.ImageSharp.Tests.Formats Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType().Count()); + Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType().Count()); } [Fact] diff --git a/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs index b4d3769d7..0a49d20cd 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs @@ -121,6 +121,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg if (!Avx.IsSupported) { this.Output.WriteLine("No AVX present, skipping test!"); + return; } Span src = Create8x8RoundedRandomFloatData(-200, 200, seed); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index dd836fd1e..4a13bbe62 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -10,7 +10,6 @@ using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; - using Xunit; using static SixLabors.ImageSharp.Tests.TestImages.Tiff; diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs index 0d05fe341..cdd9616a7 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs @@ -16,6 +16,7 @@ using static SixLabors.ImageSharp.Tests.TestImages.Tiff; namespace SixLabors.ImageSharp.Tests.Formats.Tiff { + [Collection("RunSerial")] [Trait("Format", "Tiff")] public class TiffMetadataTests { diff --git a/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs b/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs new file mode 100644 index 000000000..417b9fed5 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs @@ -0,0 +1,79 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats.Webp.Lossless; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Webp +{ + [Trait("Format", "Webp")] + public class DominantCostRangeTests + { + [Fact] + public void DominantCost_Constructor() + { + var dominantCostRange = new DominantCostRange(); + Assert.Equal(0, dominantCostRange.LiteralMax); + Assert.Equal(double.MaxValue, dominantCostRange.LiteralMin); + Assert.Equal(0, dominantCostRange.RedMax); + Assert.Equal(double.MaxValue, dominantCostRange.RedMin); + Assert.Equal(0, dominantCostRange.BlueMax); + Assert.Equal(double.MaxValue, dominantCostRange.BlueMin); + } + + [Fact] + public void UpdateDominantCostRange_Works() + { + // arrange + var dominantCostRange = new DominantCostRange(); + var histogram = new Vp8LHistogram(10) + { + LiteralCost = 1.0d, + RedCost = 2.0d, + BlueCost = 3.0d + }; + + // act + dominantCostRange.UpdateDominantCostRange(histogram); + + // assert + Assert.Equal(1.0d, dominantCostRange.LiteralMax); + Assert.Equal(1.0d, dominantCostRange.LiteralMin); + Assert.Equal(2.0d, dominantCostRange.RedMax); + Assert.Equal(2.0d, dominantCostRange.RedMin); + Assert.Equal(3.0d, dominantCostRange.BlueMax); + Assert.Equal(3.0d, dominantCostRange.BlueMin); + } + + [Theory] + [InlineData(3, 19)] + [InlineData(4, 34)] + public void GetHistoBinIndex_Works(int partitions, int expectedIndex) + { + // arrange + var dominantCostRange = new DominantCostRange() + { + BlueMax = 253.4625, + BlueMin = 109.0, + LiteralMax = 285.0, + LiteralMin = 133.0, + RedMax = 191.0, + RedMin = 109.0 + }; + var histogram = new Vp8LHistogram(6) + { + LiteralCost = 247.0d, + RedCost = 112.0d, + BlueCost = 202.0d, + BitCost = 733.0d + }; + dominantCostRange.UpdateDominantCostRange(histogram); + + // act + int binIndex = dominantCostRange.GetHistoBinIndex(histogram, partitions); + + // assert + Assert.Equal(expectedIndex, binIndex); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/WebP/ImageExtensionsTests.cs b/tests/ImageSharp.Tests/Formats/WebP/ImageExtensionsTests.cs new file mode 100644 index 000000000..a17248612 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/WebP/ImageExtensionsTests.cs @@ -0,0 +1,156 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Webp +{ + [Trait("Format", "Webp")] + public class ImageExtensionsTests + { + [Fact] + public void SaveAsWebp_Path() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTests)); + string file = Path.Combine(dir, "SaveAsWebp_Path.webp"); + + using (var image = new Image(10, 10)) + { + image.SaveAsWebp(file); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/webp", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsWebpAsync_Path() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTests)); + string file = Path.Combine(dir, "SaveAsWebpAsync_Path.webp"); + + using (var image = new Image(10, 10)) + { + await image.SaveAsWebpAsync(file); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/webp", mime.DefaultMimeType); + } + } + + [Fact] + public void SaveAsWebp_Path_Encoder() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); + string file = Path.Combine(dir, "SaveAsWebp_Path_Encoder.webp"); + + using (var image = new Image(10, 10)) + { + image.SaveAsWebp(file, new WebpEncoder()); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/webp", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsWebpAsync_Path_Encoder() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); + string file = Path.Combine(dir, "SaveAsWebpAsync_Path_Encoder.webp"); + + using (var image = new Image(10, 10)) + { + await image.SaveAsWebpAsync(file, new WebpEncoder()); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/webp", mime.DefaultMimeType); + } + } + + [Fact] + public void SaveAsWebp_Stream() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + image.SaveAsWebp(memoryStream); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/webp", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsWebpAsync_StreamAsync() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + await image.SaveAsWebpAsync(memoryStream); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/webp", mime.DefaultMimeType); + } + } + + [Fact] + public void SaveAsWebp_Stream_Encoder() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + image.SaveAsWebp(memoryStream, new WebpEncoder()); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/webp", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsWebpAsync_Stream_Encoder() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + await image.SaveAsWebpAsync(memoryStream, new WebpEncoder()); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/webp", mime.DefaultMimeType); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/WebP/LosslessUtilsTests.cs b/tests/ImageSharp.Tests/Formats/WebP/LosslessUtilsTests.cs new file mode 100644 index 000000000..be7bc27d3 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/WebP/LosslessUtilsTests.cs @@ -0,0 +1,179 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats.Webp.Lossless; +using SixLabors.ImageSharp.Tests.TestUtilities; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Webp +{ + [Trait("Format", "Webp")] + public class LosslessUtilsTests + { + private static void RunSubtractGreenTest() + { + uint[] pixelData = + { + 4293035898, 4293101432, 4292903793, 4292838511, 4292837995, 4292771950, 4292903791, 4293299316, + 4293563769, 4293629303, 4293363312, 4291913575, 4289282905, 4288692313, 4289349210, 4289809240, + 4289743703, 4289874775, 4289940567, 4289743701, 4290137943, 4290860378, 4291058267, 4291386715, + 4291583836, 4291715937, 4291585379, 4291650657, 4291650143, 4291584863, 4291716451, 4291847521, + 4291913571, 4292044130, 4291978850, 4291847521, 4291847524, 4291847779, 4291913571, 4291848293, + 4291651689, 4291585895, 4291519584, 4291715936, 4291520355, 4291650658, 4291847263, 4291913313, + 4291847777, 4291781731, 4291783015 + }; + + uint[] expectedOutput = + { + 4284188659, 4284254193, 4284318702, 4284187883, 4284318441, 4284383470, 4284318700, 4284124392, + 4283799012, 4283864546, 4284581610, 4285163264, 4284891926, 4284497945, 4284761620, 4284893965, + 4284828428, 4284959500, 4284959755, 4284828426, 4284960520, 4285289733, 4285159937, 4285292030, + 4285358077, 4285228030, 4284966398, 4285097213, 4285227773, 4285096956, 4285097470, 4285228540, + 4285163516, 4285425149, 4285294332, 4285228540, 4285228543, 4285163261, 4285163516, 4285032701, + 4284835841, 4284835584, 4284966140, 4285228029, 4284770300, 4285097214, 4285293819, 4285228795, + 4285163259, 4285228287, 4284901886 + }; + + LosslessUtils.SubtractGreenFromBlueAndRed(pixelData); + + Assert.Equal(expectedOutput, pixelData); + } + + private static void RunAddGreenToBlueAndRedTest() + { + uint[] pixelData = + { + 4284188659, 4284254193, 4284318702, 4284187883, 4284318441, 4284383470, 4284318700, 4284124392, + 4283799012, 4283864546, 4284581610, 4285163264, 4284891926, 4284497945, 4284761620, 4284893965, + 4284828428, 4284959500, 4284959755, 4284828426, 4284960520, 4285289733, 4285159937, 4285292030, + 4285358077, 4285228030, 4284966398, 4285097213, 4285227773, 4285096956, 4285097470, 4285228540, + 4285163516, 4285425149, 4285294332, 4285228540, 4285228543, 4285163261, 4285163516, 4285032701, + 4284835841, 4284835584, 4284966140, 4285228029, 4284770300, 4285097214, 4285293819, 4285228795, + 4285163259, 4285228287, 4284901886 + }; + + uint[] expectedOutput = + { + 4293035898, 4293101432, 4292903793, 4292838511, 4292837995, 4292771950, 4292903791, 4293299316, + 4293563769, 4293629303, 4293363312, 4291913575, 4289282905, 4288692313, 4289349210, 4289809240, + 4289743703, 4289874775, 4289940567, 4289743701, 4290137943, 4290860378, 4291058267, 4291386715, + 4291583836, 4291715937, 4291585379, 4291650657, 4291650143, 4291584863, 4291716451, 4291847521, + 4291913571, 4292044130, 4291978850, 4291847521, 4291847524, 4291847779, 4291913571, 4291848293, + 4291651689, 4291585895, 4291519584, 4291715936, 4291520355, 4291650658, 4291847263, 4291913313, + 4291847777, 4291781731, 4291783015 + }; + + LosslessUtils.AddGreenToBlueAndRed(pixelData); + + Assert.Equal(expectedOutput, pixelData); + } + + private static void RunTransformColorTest() + { + uint[] pixelData = + { + 5998579, 65790, 130301, 16646653, 196350, 130565, 16712702, 16583164, 16452092, 65790, 782600, + 647446, 16571414, 16448771, 263931, 132601, 16711935, 131072, 511, 16711679, 132350, 329469, + 16647676, 132093, 66303, 16647169, 16515584, 196607, 196096, 16646655, 514, 131326, 16712192, + 327169, 16646655, 16776960, 3, 16712190, 511, 16646401, 16580612, 65535, 196092, 327425, 16319743, + 392450, 196861, 16712192, 16711680, 130564, 16451071 + }; + + var m = new Vp8LMultipliers() + { + GreenToBlue = 240, + GreenToRed = 232, + RedToBlue = 0 + }; + + uint[] expectedOutput = + { + 100279, 65790, 16710907, 16712190, 130813, 65028, 131840, 264449, 133377, 65790, 61697, 15917319, + 14801924, 16317698, 591614, 394748, 16711935, 131072, 65792, 16711679, 328704, 656896, 132607, + 328703, 197120, 66563, 16646657, 196607, 130815, 16711936, 131587, 131326, 66049, 261632, 16711936, + 16776960, 3, 511, 65792, 16711938, 16580612, 65535, 65019, 327425, 16516097, 261377, 196861, 66049, + 16711680, 65027, 16712962 + }; + + LosslessUtils.TransformColor(m, pixelData, pixelData.Length); + + Assert.Equal(expectedOutput, pixelData); + } + + private static void RunTransformColorInverseTest() + { + uint[] pixelData = + { + 100279, 65790, 16710907, 16712190, 130813, 65028, 131840, 264449, 133377, 65790, 61697, 15917319, + 14801924, 16317698, 591614, 394748, 16711935, 131072, 65792, 16711679, 328704, 656896, 132607, + 328703, 197120, 66563, 16646657, 196607, 130815, 16711936, 131587, 131326, 66049, 261632, 16711936, + 16776960, 3, 511, 65792, 16711938, 16580612, 65535, 65019, 327425, 16516097, 261377, 196861, 66049, + 16711680, 65027, 16712962 + }; + + var m = new Vp8LMultipliers() + { + GreenToBlue = 240, + GreenToRed = 232, + RedToBlue = 0 + }; + + uint[] expectedOutput = + { + 5998579, 65790, 130301, 16646653, 196350, 130565, 16712702, 16583164, 16452092, 65790, 782600, + 647446, 16571414, 16448771, 263931, 132601, 16711935, 131072, 511, 16711679, 132350, 329469, + 16647676, 132093, 66303, 16647169, 16515584, 196607, 196096, 16646655, 514, 131326, 16712192, + 327169, 16646655, 16776960, 3, 16712190, 511, 16646401, 16580612, 65535, 196092, 327425, 16319743, + 392450, 196861, 16712192, 16711680, 130564, 16451071 + }; + + LosslessUtils.TransformColorInverse(m, pixelData); + + Assert.Equal(expectedOutput, pixelData); + } + + [Fact] + public void SubtractGreen_Works() => RunSubtractGreenTest(); + + [Fact] + public void AddGreenToBlueAndRed_Works() => RunAddGreenToBlueAndRedTest(); + + [Fact] + public void TransformColor_Works() => RunTransformColorTest(); + + [Fact] + public void TransformColorInverse_Works() => RunTransformColorInverseTest(); + +#if SUPPORTS_RUNTIME_INTRINSICS + [Fact] + public void SubtractGreen_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunSubtractGreenTest, HwIntrinsics.AllowAll); + + [Fact] + public void SubtractGreen_WithoutAvx_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunSubtractGreenTest, HwIntrinsics.DisableAVX); + + [Fact] + public void SubtractGreen_WithoutAvxOrSSSE3_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunSubtractGreenTest, HwIntrinsics.DisableAVX | HwIntrinsics.DisableSSSE3); + + [Fact] + public void AddGreenToBlueAndRed_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAddGreenToBlueAndRedTest, HwIntrinsics.AllowAll); + + [Fact] + public void AddGreenToBlueAndRed_WithoutAvx_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAddGreenToBlueAndRedTest, HwIntrinsics.DisableAVX); + + [Fact] + public void AddGreenToBlueAndRed_WithoutAvxOrSSSE3_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAddGreenToBlueAndRedTest, HwIntrinsics.DisableAVX | HwIntrinsics.DisableSSE2 | HwIntrinsics.DisableSSSE3); + + [Fact] + public void TransformColor_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunTransformColorTest, HwIntrinsics.AllowAll); + + [Fact] + public void TransformColor_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunTransformColorTest, HwIntrinsics.DisableSSE2); + + [Fact] + public void TransformColorInverse_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunTransformColorInverseTest, HwIntrinsics.AllowAll); + + [Fact] + public void TransformColorInverse_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunTransformColorInverseTest, HwIntrinsics.DisableSSE2); +#endif + } +} diff --git a/tests/ImageSharp.Tests/Formats/WebP/PredictorEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/PredictorEncoderTests.cs new file mode 100644 index 000000000..b48020198 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/WebP/PredictorEncoderTests.cs @@ -0,0 +1,159 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.Formats.Webp.Lossless; +using SixLabors.ImageSharp.PixelFormats; +#if SUPPORTS_RUNTIME_INTRINSICS +using SixLabors.ImageSharp.Tests.TestUtilities; +#endif +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Webp +{ + [Trait("Format", "Webp")] + public class PredictorEncoderTests + { + [Fact] + public static void ColorSpaceTransform_WithBikeImage_ProducesExpectedData() + => RunColorSpaceTransformTestWithBikeImage(); + + [Fact] + public static void ColorSpaceTransform_WithPeakImage_ProducesExpectedData() + => RunColorSpaceTransformTestWithPeakImage(); + +#if SUPPORTS_RUNTIME_INTRINSICS + [Fact] + public void ColorSpaceTransform_WithPeakImage_WithHardwareIntrinsics_Works() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ColorSpaceTransform_WithPeakImage_ProducesExpectedData, HwIntrinsics.AllowAll); + + [Fact] + public void ColorSpaceTransform_WithPeakImage_WithoutSSE41_Works() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ColorSpaceTransform_WithPeakImage_ProducesExpectedData, HwIntrinsics.DisableSSE41); + + [Fact] + public void ColorSpaceTransform_WithBikeImage_WithHardwareIntrinsics_Works() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ColorSpaceTransform_WithBikeImage_ProducesExpectedData, HwIntrinsics.AllowAll); + + [Fact] + public void ColorSpaceTransform_WithBikeImage_WithoutSSE41_Works() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ColorSpaceTransform_WithBikeImage_ProducesExpectedData, HwIntrinsics.DisableSSE41); +#endif + + private static void RunColorSpaceTransformTestWithPeakImage() + { + // arrange + uint[] expectedData = + { + 4278191104, 4278191104, 4278191104, 4278191104, 4278191104, 4278191104, 4278191104, 4294577152, + 4294707200, 4294707200, 4294707200, 4294707200, 4294837248, 4294837248, 4293926912, 4294316544, + 4278191104, 4278191104, 4294837248, 4294837248, 4280287232, 4280350720, 4294447104, 4294707200, + 4294838272, 4278516736, 4294837248, 4294837248, 4278516736, 4294707200, 4279298048, 4294837248, + 4294837248, 4294837248, 4294837248, 4280287232, 4280287232, 4292670464, 4279633408, 4294838272, + 4294837248, 4278516736, 4278516736, 4278516736, 4278516736, 4278516736, 4278778880, 4278193152, + 4278191104, 4280287232, 4280287232, 4280287232, 4280287232, 4293971968, 4280612864, 4292802560, + 4294837760, 4278516736, 4278516736, 4294837760, 4294707712, 4278516736, 4294837248, 4278193152, + 4280287232, 4278984704, 4280287232, 4278243328, 4280287232, 4278244352, 4280287232, 4280025088, + 4280025088, 4294837760, 4278192128, 4294838784, 4294837760, 4294707712, 4278778880, 4278324224, + 4280287232, 4280287232, 4278202368, 4279115776, 4280287232, 4278243328, 4280287232, 4280287232, + 4280025088, 4280287232, 4278192128, 4294838272, 4294838272, 4294837760, 4278190592, 4278778880, + 4280875008, 4280287232, 4279896576, 4281075712, 4281075712, 4280287232, 4280287232, 4280287232, + 4280287232, 4280287232, 4278190592, 4294709248, 4278516736, 4278516736, 4278584832, 4278909440, + 4280287232, 4280287232, 4294367744, 4294621184, 4279115776, 4280287232, 4280287232, 4280351744, + 4280287232, 4280287232, 4280287232, 4278513664, 4278516736, 4278716416, 4278584832, 4280291328, + 4293062144, 4280287232, 4280287232, 4280287232, 4294456320, 4280291328, 4280287232, 4280287232, + 4280287232, 4280287232, 4280287232, 4280287232, 4278513152, 4278716416, 4278584832, 4280291328, + 4278198272, 4278198272, 4278589952, 4278198272, 4278198272, 4280287232, 4278765568, 4280287232, + 4280287232, 4280287232, 4280287232, 4294712832, 4278513152, 4278716640, 4279300608, 4278584832, + 4280156672, 4279373312, 4278589952, 4279373312, 4278328832, 4278328832, 4278328832, 4279634432, + 4280287232, 4280287232, 4280287232, 4280287232, 4278457344, 4280483328, 4278584832, 4278385664, + 4279634432, 4279373312, 4279634432, 4280287232, 4280287232, 4280156672, 4278589952, 4278328832, + 4278198272, 4280156672, 4280483328, 4294363648, 4280287232, 4278376448, 4280287232, 4278647808, + 4280287232, 4280287232, 4279373312, 4280287232, 4280287232, 4280156672, 4280287232, 4278198272, + 4278198272, 4280156672, 4280287232, 4280287232, 4293669888, 4278765568, 4278765568, 4280287232, + 4280287232, 4280287232, 4279634432, 4279634432, 4280287232, 4280287232, 4280287232, 4280287232, + 4280287232, 4280287232, 4280287232, 4280287232, 4279373312, 4279764992, 4293539328, 4279896576, + 4280287232, 4280287232, 4280287232, 4279634432, 4278198272, 4279634432, 4280287232, 4280287232, + 4280287232, 4280287232, 4280287232, 4280287232, 4280287232, 4279503872, 4279503872, 4280288256, + 4280287232, 4280287232, 4280287232, 4280287232, 4280287232, 4280287232, 4280287232, 4280287232, + 4280287232, 4280287232, 4280287232, 4280287232, 4280287232, 4280287232, 4280287232, 4280287232 + }; + + // Convert image pixels to bgra array. + byte[] imgBytes = File.ReadAllBytes(TestImageFullPath(TestImages.Webp.Peak)); + using var image = Image.Load(imgBytes); + uint[] bgra = ToBgra(image); + + int colorTransformBits = 3; + int transformWidth = LosslessUtils.SubSampleSize(image.Width, colorTransformBits); + int transformHeight = LosslessUtils.SubSampleSize(image.Height, colorTransformBits); + uint[] transformData = new uint[transformWidth * transformHeight]; + + // act + PredictorEncoder.ColorSpaceTransform(image.Width, image.Height, colorTransformBits, 75, bgra, transformData); + + // assert + Assert.Equal(expectedData, transformData); + } + + private static void RunColorSpaceTransformTestWithBikeImage() + { + // arrange + uint[] expectedData = + { + 4278714368, 4278192876, 4278198304, 4278198304, 4278190304, 4278190080, 4278190080, 4278198272, + 4278197760, 4278198816, 4278197794, 4278197774, 4278190080, 4278190080, 4278198816, 4278197281, + 4278197280, 4278197792, 4278200353, 4278191343, 4278190304, 4294713873, 4278198784, 4294844416, + 4278201578, 4278200044, 4278191343, 4278190288, 4294705200, 4294717139, 4278203628, 4278201064, + 4278201586, 4278197792, 4279240909 + }; + + // Convert image pixels to bgra array. + byte[] imgBytes = File.ReadAllBytes(TestImageFullPath(TestImages.Webp.Lossy.BikeSmall)); + using var image = Image.Load(imgBytes, new WebpDecoder()); + uint[] bgra = ToBgra(image); + + int colorTransformBits = 4; + int transformWidth = LosslessUtils.SubSampleSize(image.Width, colorTransformBits); + int transformHeight = LosslessUtils.SubSampleSize(image.Height, colorTransformBits); + uint[] transformData = new uint[transformWidth * transformHeight]; + + // act + PredictorEncoder.ColorSpaceTransform(image.Width, image.Height, colorTransformBits, 75, bgra, transformData); + + // assert + Assert.Equal(expectedData, transformData); + } + + private static uint[] ToBgra(Image image) + where TPixel : unmanaged, IPixel + { + uint[] bgra = new uint[image.Width * image.Height]; + int idx = 0; + for (int y = 0; y < image.Height; y++) + { + Span rowSpan = image.GetPixelRowSpan(y); + for (int x = 0; x < rowSpan.Length; x++) + { + bgra[idx++] = ToBgra32(rowSpan[x]).PackedValue; + } + } + + return bgra; + } + + private static Bgra32 ToBgra32(TPixel color) + where TPixel : unmanaged, IPixel + { + Rgba32 rgba = default; + color.ToRgba32(ref rgba); + var bgra = new Bgra32(rgba.R, rgba.G, rgba.B, rgba.A); + return bgra; + } + + private static string TestImageFullPath(string path) + => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, path); + } +} diff --git a/tests/ImageSharp.Tests/Formats/WebP/Vp8HistogramTests.cs b/tests/ImageSharp.Tests/Formats/WebP/Vp8HistogramTests.cs new file mode 100644 index 000000000..4ff42f4ee --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/WebP/Vp8HistogramTests.cs @@ -0,0 +1,115 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; +using SixLabors.ImageSharp.Formats.Webp.Lossy; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Webp +{ + [Trait("Format", "Webp")] + public class Vp8HistogramTests + { + public static IEnumerable Data + { + get + { + var result = new List(); + result.Add(new object[] + { + new byte[] + { + 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 19, 16, 128, 128, 128, 128, 128, 128, + 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 19, 16, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, + 128, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 19, 16, 128, 128, 128, 128, 128, + 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 19, 16, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, + 128, 128, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 19, 16, 128, 128, 128, 128, + 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 16, 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 19, 16, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, + 128, 128, 128, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 19, 16, 128, 128, 128, + 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 19, 16, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, + 128, 128, 128, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 19, 16, 204, 204, 204, + 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 19, 16, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, + 204, 204, 204, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 19, 16, 204, 204, 204, + 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 19, 16, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, + 204, 204, 204, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 19, 16, 204, 204, 204, + 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 19, 16, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, + 204, 204, 204, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 24, 16, 204, 204, 204, + 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, + 204, 204, 204 + }, + new byte[] + { + 128, 128, 128, 128, 129, 129, 129, 129, 127, 127, 127, 127, 129, 129, 129, 129, 128, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 128, 128, 128, 128, 129, 129, + 129, 129, 127, 127, 127, 127, 129, 129, 129, 129, 129, 128, 127, 127, 128, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 128, 128, 128, 128, 129, 129, 129, 129, 127, 127, 127, 127, + 129, 129, 129, 129, 129, 129, 128, 127, 129, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 128, 128, 128, 128, 129, 129, 129, 129, 127, 127, 127, 127, 129, 129, 129, 129, 129, 129, + 129, 128, 129, 128, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 128, 128, 127, 127, 129, + 129, 129, 129, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, + 204, 204, 204, 204, 204, 204, 204, 204, 129, 129, 128, 128, 129, 129, 129, 129, 204, 204, 204, + 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, + 204, 204, 129, 129, 129, 129, 129, 129, 129, 129, 204, 204, 204, 204, 204, 204, 204, 204, 204, + 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 129, 129, 129, 129, + 129, 129, 129, 129, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, + 204, 204, 204, 204, 204, 204, 204, 204, 204 + } + }); + return result; + } + } + + [Fact] + public void GetAlpha_WithEmptyHistogram_Works() + { + // arrange + var histogram = new Vp8Histogram(); + + // act + int alpha = histogram.GetAlpha(); + + // assert + Assert.Equal(0, alpha); + } + + [Theory] + [MemberData(nameof(Data))] + public void GetAlpha_Works(byte[] reference, byte[] pred) + { + // arrange + var histogram = new Vp8Histogram(); + histogram.CollectHistogram(reference, pred, 0, 1); + + // act + int alpha = histogram.GetAlpha(); + + // assert + Assert.Equal(1054, alpha); + } + + [Theory] + [MemberData(nameof(Data))] + public void Merge_Works(byte[] reference, byte[] pred) + { + // arrange + var histogram1 = new Vp8Histogram(); + histogram1.CollectHistogram(reference, pred, 0, 1); + var histogram2 = new Vp8Histogram(); + histogram1.Merge(histogram2); + + // act + int alpha = histogram2.GetAlpha(); + + // assert + Assert.Equal(1054, alpha); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/WebP/Vp8ModeScoreTests.cs b/tests/ImageSharp.Tests/Formats/WebP/Vp8ModeScoreTests.cs new file mode 100644 index 000000000..d3b11bdb5 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/WebP/Vp8ModeScoreTests.cs @@ -0,0 +1,94 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats.Webp.Lossy; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Webp +{ + [Trait("Format", "Webp")] + public class Vp8ModeScoreTests + { + [Fact] + public void InitScore_Works() + { + var score = new Vp8ModeScore(); + score.InitScore(); + Assert.Equal(0, score.D); + Assert.Equal(0, score.SD); + Assert.Equal(0, score.R); + Assert.Equal(0, score.H); + Assert.Equal(0u, score.Nz); + Assert.Equal(Vp8ModeScore.MaxCost, score.Score); + } + + [Fact] + public void CopyScore_Works() + { + // arrange + var score1 = new Vp8ModeScore + { + Score = 123, + Nz = 1, + D = 2, + H = 3, + ModeI16 = 4, + ModeUv = 5, + R = 6, + SD = 7 + }; + var score2 = new Vp8ModeScore(); + score2.InitScore(); + + // act + score2.CopyScore(score1); + + // assert + Assert.Equal(score1.D, score2.D); + Assert.Equal(score1.SD, score2.SD); + Assert.Equal(score1.R, score2.R); + Assert.Equal(score1.H, score2.H); + Assert.Equal(score1.Nz, score2.Nz); + Assert.Equal(score1.Score, score2.Score); + } + + [Fact] + public void AddScore_Works() + { + // arrange + var score1 = new Vp8ModeScore + { + Score = 123, + Nz = 1, + D = 2, + H = 3, + ModeI16 = 4, + ModeUv = 5, + R = 6, + SD = 7 + }; + var score2 = new Vp8ModeScore + { + Score = 123, + Nz = 1, + D = 2, + H = 3, + ModeI16 = 4, + ModeUv = 5, + R = 6, + SD = 7 + }; + + // act + score2.AddScore(score1); + + // assert + Assert.Equal(4, score2.D); + Assert.Equal(14, score2.SD); + Assert.Equal(12, score2.R); + Assert.Equal(6, score2.H); + Assert.Equal(1u, score2.Nz); + Assert.Equal(246, score2.Score); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpCommonUtilsTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpCommonUtilsTests.cs new file mode 100644 index 000000000..71bd5bf8d --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpCommonUtilsTests.cs @@ -0,0 +1,214 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; +#if SUPPORTS_RUNTIME_INTRINSICS +using SixLabors.ImageSharp.Tests.TestUtilities; +#endif + +namespace SixLabors.ImageSharp.Tests.Formats.Webp +{ + [Trait("Format", "Webp")] + public class WebpCommonUtilsTests + { + [Fact] + public void CheckNonOpaque_WithOpaquePixels_Works() => RunCheckNoneOpaqueWithOpaquePixelsTest(); + + [Fact] + public void CheckNonOpaque_WithNoneOpaquePixels_Works() => RunCheckNoneOpaqueWithNoneOpaquePixelsTest(); + +#if SUPPORTS_RUNTIME_INTRINSICS + [Fact] + public void CheckNonOpaque_WithOpaquePixels_WithHardwareIntrinsics_Works() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCheckNoneOpaqueWithOpaquePixelsTest, HwIntrinsics.AllowAll); + + [Fact] + public void CheckNonOpaque_WithOpaquePixels_WithoutSse2_Works() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCheckNoneOpaqueWithOpaquePixelsTest, HwIntrinsics.DisableSSE2); + + [Fact] + public void CheckNonOpaque_WithOpaquePixels_WithoutAvx2_Works() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCheckNoneOpaqueWithOpaquePixelsTest, HwIntrinsics.DisableAVX2); + + [Fact] + public void CheckNonOpaque_WithNoneOpaquePixels_WithHardwareIntrinsics_Works() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCheckNoneOpaqueWithNoneOpaquePixelsTest, HwIntrinsics.AllowAll); + + [Fact] + public void CheckNonOpaque_WithNoneOpaquePixels_WithoutSse2_Works() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCheckNoneOpaqueWithNoneOpaquePixelsTest, HwIntrinsics.DisableSSE2); + + [Fact] + public void CheckNonOpaque_WithNoneOpaquePixels_WithoutAvx2_Works() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCheckNoneOpaqueWithNoneOpaquePixelsTest, HwIntrinsics.DisableAVX2); +#endif + + private static void RunCheckNoneOpaqueWithNoneOpaquePixelsTest() + { + // arrange + byte[] rowBytes = + { + 122, 120, 101, 255, + 171, 165, 151, 255, + 209, 208, 210, 255, + 174, 183, 189, 255, + 148, 158, 158, 255, + 122, 120, 101, 255, + 171, 165, 151, 255, + 209, 208, 210, 255, + 174, 183, 189, 255, + 148, 158, 158, 255, + 171, 165, 151, 255, + 209, 208, 210, 255, + 174, 183, 189, 255, + 148, 158, 158, 255, + 171, 165, 151, 255, + 209, 208, 210, 255, + 174, 183, 189, 255, + 148, 158, 158, 255, + 171, 165, 151, 255, + 209, 208, 210, 255, + 174, 183, 189, 255, + 148, 158, 158, 255, + 148, 158, 158, 10, + 171, 165, 151, 255, + 209, 208, 210, 255, + 171, 165, 151, 255, + 209, 208, 210, 255, + 174, 183, 189, 255, + 148, 158, 158, 255, + 171, 165, 151, 255, + 209, 208, 210, 255, + 174, 183, 189, 255, + 148, 158, 158, 10, + 171, 165, 151, 255, + 209, 208, 210, 255, + 174, 183, 189, 255, + 148, 158, 158, 255, + 171, 165, 151, 255, + 209, 208, 210, 255, + 174, 183, 189, 255, + 148, 158, 158, 255, + 209, 208, 210, 0, + 174, 183, 189, 255, + 148, 158, 158, 255, + 148, 158, 158, 255, + 171, 165, 151, 255, + 209, 208, 210, 255, + 174, 183, 189, 0, + 148, 158, 158, 255, + 148, 158, 158, 255, + 171, 165, 151, 255, + 209, 208, 210, 255, + 174, 183, 189, 255, + 148, 158, 158, 255, + 148, 158, 158, 100, + 171, 165, 151, 0, + 209, 208, 210, 100, + 174, 183, 189, 255, + 148, 158, 158, 255, + }; + Span row = MemoryMarshal.Cast(rowBytes); + + bool noneOpaque; + for (int length = 8; length < row.Length; length += 8) + { + // act + noneOpaque = WebpCommonUtils.CheckNonOpaque(row); + + // assert + Assert.True(noneOpaque); + } + + // One last test with the complete row. + noneOpaque = WebpCommonUtils.CheckNonOpaque(row); + Assert.True(noneOpaque); + } + + private static void RunCheckNoneOpaqueWithOpaquePixelsTest() + { + // arrange + byte[] rowBytes = + { + 122, 120, 101, 255, + 171, 165, 151, 255, + 209, 208, 210, 255, + 174, 183, 189, 255, + 148, 158, 158, 255, + 122, 120, 101, 255, + 171, 165, 151, 255, + 209, 208, 210, 255, + 174, 183, 189, 255, + 148, 158, 158, 255, + 171, 165, 151, 255, + 209, 208, 210, 255, + 174, 183, 189, 255, + 148, 158, 158, 255, + 171, 165, 151, 255, + 209, 208, 210, 255, + 174, 183, 189, 255, + 148, 158, 158, 255, + 171, 165, 151, 255, + 209, 208, 210, 255, + 174, 183, 189, 255, + 148, 158, 158, 255, + 148, 158, 158, 255, + 171, 165, 151, 255, + 209, 208, 210, 255, + 171, 165, 151, 255, + 209, 208, 210, 255, + 174, 183, 189, 255, + 148, 158, 158, 255, + 171, 165, 151, 255, + 209, 208, 210, 255, + 174, 183, 189, 255, + 148, 158, 158, 255, + 171, 165, 151, 255, + 209, 208, 210, 255, + 174, 183, 189, 255, + 148, 158, 158, 255, + 171, 165, 151, 255, + 209, 208, 210, 255, + 174, 183, 189, 255, + 148, 158, 158, 255, + 209, 208, 210, 255, + 174, 183, 189, 255, + 148, 158, 158, 255, + 148, 158, 158, 255, + 171, 165, 151, 255, + 209, 208, 210, 255, + 174, 183, 189, 255, + 148, 158, 158, 255, + 148, 158, 158, 255, + 171, 165, 151, 255, + 209, 208, 210, 255, + 174, 183, 189, 255, + 148, 158, 158, 255, + 148, 158, 158, 255, + 171, 165, 151, 255, + 209, 208, 210, 255, + 174, 183, 189, 255, + 148, 158, 158, 255, + }; + Span row = MemoryMarshal.Cast(rowBytes); + + bool noneOpaque; + for (int length = 8; length < row.Length; length += 8) + { + // act + noneOpaque = WebpCommonUtils.CheckNonOpaque(row.Slice(0, length)); + + // assert + Assert.False(noneOpaque); + } + + // One last test with the complete row. + noneOpaque = WebpCommonUtils.CheckNonOpaque(row); + Assert.False(noneOpaque); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs new file mode 100644 index 000000000..34fa72c63 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs @@ -0,0 +1,363 @@ +// Copyright (c) Six Labors. +// 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; +using static SixLabors.ImageSharp.Tests.TestImages.Webp; + +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Tests.Formats.Webp +{ + [Collection("RunSerial")] + [Trait("Format", "Webp")] + public class WebpDecoderTests + { + private static WebpDecoder WebpDecoder => new(); + + private static MagickReferenceDecoder ReferenceDecoder => new(); + + [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)] + [WithFile(Lossy.Alpha1, PixelTypes.Rgba32)] + [WithFile(Lossy.Alpha2, PixelTypes.Rgba32)] + [WithFile(Lossy.Alpha3, PixelTypes.Rgba32)] + [WithFile(Lossy.AlphaThinkingSmiley, PixelTypes.Rgba32)] + [WithFile(Lossy.AlphaSticker, 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.Alpha, PixelTypes.Rgba32)] + public void WebpDecoder_CanDecode_Lossless_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.LossLessCorruptImage4, PixelTypes.Rgba32)] + public void WebpDecoder_CanDecode_Lossless_WithIssues(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // Just make sure no exception is thrown. The reference decoder fails to load the image. + using (Image image = provider.GetImage(WebpDecoder)) + { + image.DebugSave(provider); + } + } + + // https://github.com/SixLabors/ImageSharp/issues/1594 + [Theory] + [WithFile(Lossy.Issue1594, PixelTypes.Rgba32)] + public void WebpDecoder_CanDecode_Issue1594(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(WebpDecoder)) + { + image.DebugSave(provider); + image.CompareToOriginal(provider, ReferenceDecoder); + } + } + + [Theory] + [WithFile(Lossless.LossLessCorruptImage3, 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/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs new file mode 100644 index 000000000..70cc487bf --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -0,0 +1,286 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using Xunit; +using static SixLabors.ImageSharp.Tests.TestImages.Webp; + +namespace SixLabors.ImageSharp.Tests.Formats.Webp +{ + [Collection("RunSerial")] + [Trait("Format", "Webp")] + public class WebpEncoderTests + { + [Theory] + [WithFile(Flag, PixelTypes.Rgba32, WebpFileFormatType.Lossless)] // if its not a webp input image, it should default to lossless. + [WithFile(Lossless.NoTransform1, PixelTypes.Rgba32, WebpFileFormatType.Lossless)] + [WithFile(Lossy.Bike, PixelTypes.Rgba32, WebpFileFormatType.Lossy)] + public void Encode_PreserveRatio(TestImageProvider provider, WebpFileFormatType expectedFormat) + where TPixel : unmanaged, IPixel + { + var options = new WebpEncoder(); + using Image input = provider.GetImage(); + using var memoryStream = new MemoryStream(); + input.Save(memoryStream, options); + + memoryStream.Position = 0; + using var output = Image.Load(memoryStream); + + ImageMetadata meta = output.Metadata; + WebpMetadata webpMetaData = meta.GetWebpMetadata(); + Assert.Equal(expectedFormat, webpMetaData.FileFormat); + } + + [Theory] + [WithFile(Flag, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.PalettedTwoColor, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.Paletted256Colors, PixelTypes.Rgba32)] + public void Encode_Lossless_WithPalette_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + var encoder = new WebpEncoder() + { + FileFormat = WebpFileFormatType.Lossless, + Quality = 100, + Method = WebpEncodingMethod.BestQuality + }; + + using Image image = provider.GetImage(); + image.VerifyEncoder(provider, "webp", string.Empty, encoder); + } + + [Theory] + [WithFile(TestImages.Bmp.Car, PixelTypes.Rgba32, 100)] + [WithFile(TestImages.Bmp.Car, PixelTypes.Rgba32, 80)] + [WithFile(TestImages.Bmp.Car, PixelTypes.Rgba32, 20)] + public void Encode_Lossless_WithDifferentQuality_Works(TestImageProvider provider, int quality) + where TPixel : unmanaged, IPixel + { + var encoder = new WebpEncoder() + { + FileFormat = WebpFileFormatType.Lossless, + Quality = quality + }; + + using Image image = provider.GetImage(); + string testOutputDetails = string.Concat("lossless", "_q", quality); + image.VerifyEncoder(provider, "webp", testOutputDetails, encoder); + } + + [Theory] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 0, 75)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 1, 75)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 2, 75)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 3, 75)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 4, 75)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 5, 75)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 6, 75)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 0, 100)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 1, 100)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 2, 100)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 3, 100)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 4, 100)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 5, 100)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 6, 100)] + public void Encode_Lossless_WithDifferentMethodAndQuality_Works(TestImageProvider provider, WebpEncodingMethod method, int quality) + where TPixel : unmanaged, IPixel + { + var encoder = new WebpEncoder() + { + FileFormat = WebpFileFormatType.Lossless, + Method = method, + Quality = quality + }; + + using Image image = provider.GetImage(); + string testOutputDetails = string.Concat("lossless", "_m", method, "_q", quality); + image.VerifyEncoder(provider, "webp", testOutputDetails, encoder); + } + + [Theory] + [WithFile(RgbTestPattern100x100, PixelTypes.Rgba32, 85)] + [WithFile(RgbTestPattern100x100, PixelTypes.Rgba32, 60)] + [WithFile(RgbTestPattern80x80, PixelTypes.Rgba32, 40)] + [WithFile(RgbTestPattern80x80, PixelTypes.Rgba32, 20)] + [WithFile(RgbTestPattern80x80, PixelTypes.Rgba32, 10)] + [WithFile(RgbTestPattern63x63, PixelTypes.Rgba32, 40)] + public void Encode_Lossless_WithNearLosslessFlag_Works(TestImageProvider provider, int nearLosslessQuality) + where TPixel : unmanaged, IPixel + { + var encoder = new WebpEncoder() + { + FileFormat = WebpFileFormatType.Lossless, + NearLossless = true, + NearLosslessQuality = nearLosslessQuality + }; + + using Image image = provider.GetImage(); + string testOutputDetails = string.Concat("nearlossless", "_q", nearLosslessQuality); + image.VerifyEncoder(provider, "webp", testOutputDetails, encoder, customComparer: GetComparer(nearLosslessQuality)); + } + + [Theory] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 0)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 1)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 2)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 3)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 4)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 5)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 6)] + [WithFile(Lossy.Alpha1, PixelTypes.Rgba32, 4)] + public void Encode_Lossless_WithPreserveTransparentColor_Works(TestImageProvider provider, WebpEncodingMethod method) + where TPixel : unmanaged, IPixel + { + var encoder = new WebpEncoder() + { + FileFormat = WebpFileFormatType.Lossless, + Method = method, + TransparentColorMode = WebpTransparentColorMode.Preserve + }; + + using Image image = provider.GetImage(); + string testOutputDetails = string.Concat("lossless", "_m", method); + image.VerifyEncoder(provider, "webp", testOutputDetails, encoder); + } + + [Theory] + [WithFile(TestPatternOpaque, PixelTypes.Rgba32)] + [WithFile(TestPatternOpaqueSmall, PixelTypes.Rgba32)] + public void Encode_Lossless_WorksWithTestPattern(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + var encoder = new WebpEncoder() { FileFormat = WebpFileFormatType.Lossless }; + image.VerifyEncoder(provider, "webp", string.Empty, encoder); + } + + [Fact] + public void Encode_Lossless_OneByOnePixel_Works() + { + // Just make sure, encoding 1 pixel by 1 pixel does not throw an exception. + using var image = new Image(1, 1); + var encoder = new WebpEncoder() { FileFormat = WebpFileFormatType.Lossless }; + using (var memStream = new MemoryStream()) + { + image.SaveAsWebp(memStream, encoder); + } + } + + [Theory] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 100)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 75)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 20)] + public void Encode_Lossy_WithDifferentQuality_Works(TestImageProvider provider, int quality) + where TPixel : unmanaged, IPixel + { + var encoder = new WebpEncoder() + { + FileFormat = WebpFileFormatType.Lossy, + Quality = quality + }; + + using Image image = provider.GetImage(); + string testOutputDetails = string.Concat("lossy", "_q", quality); + image.VerifyEncoder(provider, "webp", testOutputDetails, encoder, customComparer: GetComparer(quality)); + } + + [Theory] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 100)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 80)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 50)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 30)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 10)] + public void Encode_Lossy_WithDifferentFilterStrength_Works(TestImageProvider provider, int filterStrength) + where TPixel : unmanaged, IPixel + { + var encoder = new WebpEncoder() + { + FileFormat = WebpFileFormatType.Lossy, + FilterStrength = filterStrength + }; + + using Image image = provider.GetImage(); + string testOutputDetails = string.Concat("lossy", "_f", filterStrength); + image.VerifyEncoder(provider, "webp", testOutputDetails, encoder, customComparer: GetComparer(75)); + } + + [Theory] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 100)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 80)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 50)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 30)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 10)] + public void Encode_Lossy_WithDifferentSpatialNoiseShapingStrength_Works(TestImageProvider provider, int snsStrength) + where TPixel : unmanaged, IPixel + { + var encoder = new WebpEncoder() + { + FileFormat = WebpFileFormatType.Lossy, + SpatialNoiseShaping = snsStrength + }; + + using Image image = provider.GetImage(); + string testOutputDetails = string.Concat("lossy", "_sns", snsStrength); + image.VerifyEncoder(provider, "webp", testOutputDetails, encoder, customComparer: GetComparer(75)); + } + + [Theory] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 0, 75)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 1, 75)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 2, 75)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 3, 75)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 4, 75)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 5, 75)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 6, 75)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 0, 100)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 1, 100)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 2, 100)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 3, 100)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 4, 100)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 5, 100)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 6, 100)] + public void Encode_Lossy_WithDifferentMethodsAndQuality_Works(TestImageProvider provider, WebpEncodingMethod method, int quality) + where TPixel : unmanaged, IPixel + { + var encoder = new WebpEncoder() + { + FileFormat = WebpFileFormatType.Lossy, + Method = method, + Quality = quality + }; + + using Image image = provider.GetImage(); + string testOutputDetails = string.Concat("lossy", "_m", method, "_q", quality); + image.VerifyEncoder(provider, "webp", testOutputDetails, encoder, customComparer: GetComparer(quality)); + } + + [Theory] + [WithFile(TestPatternOpaque, PixelTypes.Rgba32)] + [WithFile(TestPatternOpaqueSmall, PixelTypes.Rgba32)] + public void Encode_Lossy_WorksWithTestPattern(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + var encoder = new WebpEncoder() { FileFormat = WebpFileFormatType.Lossy }; + image.VerifyEncoder(provider, "webp", string.Empty, encoder, ImageComparer.Tolerant(0.04f)); + } + + private static ImageComparer GetComparer(int quality) + { + float tolerance = 0.01f; // ~1.0% + + if (quality < 30) + { + tolerance = 0.02f; // ~2.0% + } + + return ImageComparer.Tolerant(tolerance); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs new file mode 100644 index 000000000..81067a41f --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs @@ -0,0 +1,108 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Tests.Formats.Webp +{ + [Trait("Format", "Webp")] + public class WebpMetaDataTests + { + private static WebpDecoder WebpDecoder => new() { IgnoreMetadata = false }; + + [Theory] + [WithFile(TestImages.Webp.Lossy.WithExif, PixelTypes.Rgba32, false)] + [WithFile(TestImages.Webp.Lossy.WithExif, PixelTypes.Rgba32, true)] + [WithFile(TestImages.Webp.Lossless.WithExif, PixelTypes.Rgba32, false)] + [WithFile(TestImages.Webp.Lossless.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 + { + ExifProfile exifProfile = image.Metadata.ExifProfile; + Assert.NotNull(exifProfile); + Assert.NotEmpty(exifProfile.Values); + Assert.Contains(exifProfile.Values, m => m.Tag.Equals(ExifTag.Make) && m.GetValue().Equals("Canon")); + Assert.Contains(exifProfile.Values, m => m.Tag.Equals(ExifTag.Model) && m.GetValue().Equals("Canon PowerShot S40")); + Assert.Contains(exifProfile.Values, m => m.Tag.Equals(ExifTag.Software) && m.GetValue().Equals("GIMP 2.10.2")); + } + } + + [Theory] + [WithFile(TestImages.Webp.Lossy.WithIccp, PixelTypes.Rgba32, false)] + [WithFile(TestImages.Webp.Lossy.WithIccp, PixelTypes.Rgba32, true)] + [WithFile(TestImages.Webp.Lossless.WithIccp, PixelTypes.Rgba32, false)] + [WithFile(TestImages.Webp.Lossless.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); + } + } + + [Theory] + [WithFile(TestImages.Webp.Lossy.WithExif, PixelTypes.Rgba32)] + public void EncodeLossyWebp_PreservesExif(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // arrange + using Image input = provider.GetImage(WebpDecoder); + using var memoryStream = new MemoryStream(); + ExifProfile expectedExif = input.Metadata.ExifProfile; + + // act + input.Save(memoryStream, new WebpEncoder() { FileFormat = WebpFileFormatType.Lossy }); + memoryStream.Position = 0; + + // assert + using var image = Image.Load(memoryStream); + ExifProfile actualExif = image.Metadata.ExifProfile; + Assert.NotNull(actualExif); + Assert.Equal(expectedExif.Values.Count, actualExif.Values.Count); + } + + [Theory] + [WithFile(TestImages.Webp.Lossless.WithExif, PixelTypes.Rgba32)] + public void EncodeLosslessWebp_PreservesExif(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // arrange + using Image input = provider.GetImage(WebpDecoder); + using var memoryStream = new MemoryStream(); + ExifProfile expectedExif = input.Metadata.ExifProfile; + + // act + input.Save(memoryStream, new WebpEncoder() { FileFormat = WebpFileFormatType.Lossless }); + memoryStream.Position = 0; + + // assert + using var image = Image.Load(memoryStream); + ExifProfile actualExif = image.Metadata.ExifProfile; + Assert.NotNull(actualExif); + Assert.Equal(expectedExif.Values.Count, actualExif.Values.Count); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs b/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs new file mode 100644 index 000000000..65b4b987e --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs @@ -0,0 +1,238 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Formats.Webp.Lossy; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Webp +{ + [Trait("Format", "Webp")] + public class YuvConversionTests + { + [Theory] + [WithFile(TestImages.Webp.Yuv, PixelTypes.Rgba32)] + public void ConvertRgbToYuv_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // arrange + using Image image = provider.GetImage(); + Configuration config = image.GetConfiguration(); + MemoryAllocator memoryAllocator = config.MemoryAllocator; + int pixels = image.Width * image.Height; + int uvWidth = (image.Width + 1) >> 1; + using System.Buffers.IMemoryOwner yBuffer = memoryAllocator.Allocate(pixels, AllocationOptions.Clean); + using System.Buffers.IMemoryOwner uBuffer = memoryAllocator.Allocate(uvWidth * image.Height, AllocationOptions.Clean); + using System.Buffers.IMemoryOwner vBuffer = memoryAllocator.Allocate(uvWidth * image.Height, AllocationOptions.Clean); + Span y = yBuffer.GetSpan(); + Span u = uBuffer.GetSpan(); + Span v = vBuffer.GetSpan(); + byte[] expectedY = + { + 82, 82, 82, 82, 128, 135, 134, 129, 167, 179, 176, 172, 192, 201, 200, 204, 188, 172, 175, 177, 168, + 151, 154, 154, 153, 152, 151, 151, 152, 160, 160, 160, 160, 82, 82, 82, 82, 140, 137, 135, 116, 174, + 183, 176, 162, 196, 199, 199, 210, 188, 166, 176, 181, 170, 145, 155, 154, 153, 154, 151, 151, 150, + 162, 159, 160, 160, 82, 82, 83, 82, 142, 139, 137, 117, 176, 184, 177, 164, 195, 199, 198, 210, 188, + 165, 175, 180, 169, 145, 155, 154, 153, 154, 152, 151, 150, 163, 160, 160, 160, 82, 82, 82, 82, 124, + 122, 120, 101, 161, 171, 165, 151, 197, 209, 208, 210, 197, 174, 183, 189, 175, 148, 158, 158, 155, + 151, 148, 148, 147, 159, 156, 156, 159, 128, 140, 142, 124, 189, 185, 183, 167, 201, 199, 198, 209, + 179, 165, 171, 179, 160, 145, 151, 152, 151, 154, 152, 151, 153, 164, 160, 160, 160, 170, 170, 170, + 169, 135, 137, 139, 122, 185, 182, 180, 165, 201, 200, 199, 210, 180, 166, 173, 180, 162, 145, 153, + 153, 151, 154, 151, 150, 152, 164, 160, 159, 159, 170, 170, 170, 170, 134, 135, 137, 120, 184, 180, + 177, 164, 200, 198, 196, 210, 181, 167, 174, 181, 163, 146, 155, 155, 153, 154, 152, 150, 152, 163, + 160, 159, 159, 167, 167, 167, 168, 129, 116, 117, 101, 167, 166, 164, 149, 205, 210, 209, 210, 191, + 177, 184, 191, 170, 149, 158, 159, 153, 151, 148, 146, 148, 159, 155, 155, 155, 170, 169, 170, 170, + 167, 174, 175, 161, 201, 201, 200, 204, 178, 173, 174, 185, 159, 148, 155, 158, 152, 152, 151, 150, + 153, 162, 159, 158, 160, 170, 169, 169, 168, 109, 122, 120, 129, 179, 183, 184, 171, 199, 200, 198, + 210, 172, 166, 170, 179, 155, 145, 150, 152, 149, 155, 152, 150, 155, 164, 161, 159, 162, 170, 170, + 170, 170, 92, 111, 109, 115, 176, 176, 177, 165, 198, 198, 196, 209, 174, 170, 173, 183, 159, 148, + 155, 156, 152, 154, 152, 150, 154, 163, 160, 158, 159, 166, 166, 168, 169, 98, 117, 116, 117, 172, + 162, 164, 152, 209, 210, 210, 210, 184, 179, 183, 192, 164, 151, 157, 159, 150, 150, 148, 146, 150, + 159, 155, 154, 157, 170, 169, 170, 170, 117, 136, 134, 123, 192, 196, 196, 197, 179, 180, 180, 191, + 159, 155, 159, 164, 153, 151, 152, 150, 154, 160, 157, 155, 160, 170, 166, 167, 165, 120, 134, 135, + 139, 69, 87, 86, 90, 201, 199, 199, 208, 165, 166, 167, 177, 148, 145, 148, 151, 150, 155, 153, 150, + 157, 165, 162, 159, 165, 170, 169, 170, 166, 84, 107, 108, 111, 49, 66, 64, 71, 200, 199, 198, 208, + 171, 173, 174, 184, 155, 150, 155, 157, 152, 153, 153, 149, 156, 163, 160, 157, 162, 167, 165, 169, + 167, 97, 121, 121, 125, 60, 77, 75, 76, 204, 210, 210, 210, 179, 180, 181, 191, 158, 152, 156, 159, + 150, 150, 149, 146, 152, 159, 156, 153, 160, 170, 169, 170, 170, 112, 135, 136, 138, 71, 88, 86, 79, + 188, 188, 188, 197, 160, 162, 163, 170, 152, 150, 152, 151, 154, 157, 156, 152, 160, 167, 164, 164, + 161, 135, 146, 150, 143, 77, 98, 99, 103, 51, 62, 60, 62, 172, 166, 165, 174, 145, 145, 145, 150, + 152, 155, 154, 150, 160, 165, 163, 159, 168, 170, 168, 170, 151, 80, 104, 109, 101, 44, 63, 63, 66, + 55, 52, 53, 51, 175, 176, 175, 183, 151, 153, 155, 158, 151, 152, 152, 148, 157, 161, 160, 156, 164, + 168, 165, 169, 156, 100, 122, 126, 118, 60, 79, 79, 81, 54, 52, 52, 51, 177, 181, 180, 188, 153, + 153, 155, 159, 149, 150, 150, 146, 155, 159, 157, 153, 164, 170, 169, 170, 170, 109, 131, 136, 127, + 66, 86, 86, 87, 46, 43, 43, 47, 168, 170, 169, 175, 151, 151, 152, 153, 153, 155, 154, 150, 160, + 164, 162, 160, 161, 151, 157, 165, 144, 88, 109, 114, 105, 55, 69, 68, 67, 62, 56, 56, 59, 151, 145, + 145, 148, 154, 154, 154, 150, 162, 164, 163, 159, 170, 170, 167, 170, 135, 80, 100, 110, 89, 41, 61, + 64, 59, 56, 53, 50, 50, 94, 85, 86, 79, 154, 155, 155, 158, 152, 152, 152, 148, 159, 161, 160, 155, + 166, 169, 165, 169, 146, 104, 122, 131, 110, 61, 80, 83, 75, 53, 53, 48, 47, 84, 74, 75, 75, 154, + 154, 154, 158, 151, 150, 150, 146, 158, 159, 158, 154, 167, 170, 169, 170, 153, 108, 127, 136, 113, + 63, 83, 87, 78, 48, 46, 43, 41, 81, 71, 72, 74, 153, 153, 153, 155, 153, 152, 152, 148, 160, 161, + 159, 157, 165, 165, 166, 170, 143, 101, 118, 127, 104, 60, 75, 78, 70, 56, 51, 48, 46, 85, 76, 77, + 81, 152, 154, 154, 151, 164, 164, 163, 159, 170, 170, 167, 170, 121, 84, 98, 114, 78, 44, 60, 68, + 52, 56, 53, 48, 56, 96, 85, 85, 83, 107, 105, 106, 100, 151, 151, 152, 148, 160, 160, 160, 155, 169, + 170, 166, 169, 134, 108, 121, 135, 98, 63, 79, 87, 69, 53, 53, 46, 50, 85, 73, 73, 71, 104, 95, 96, + 97, 151, 151, 151, 148, 160, 159, 159, 155, 169, 170, 170, 170, 137, 108, 121, 136, 99, 63, 78, 87, + 67, 51, 48, 43, 48, 85, 73, 72, 71, 105, 96, 97, 98, 152, 150, 150, 147, 160, 159, 159, 155, 169, + 170, 169, 170, 140, 111, 125, 139, 102, 67, 81, 87, 67, 50, 47, 41, 46, 83, 71, 71, 70, 103, 96, 96, + 98, 160, 162, 163, 159, 170, 170, 167, 170, 109, 91, 98, 117, 70, 49, 60, 72, 49, 55, 54, 46, 62, + 95, 84, 81, 85, 107, 104, 105, 103, 96, 98, 97, 100, 160, 159, 160, 156, 170, 170, 167, 169, 122, + 111, 118, 136, 87, 66, 77, 88, 62, 52, 53, 43, 56, 85, 74, 71, 76, 105, 95, 96, 96, 98, 100, 100, + 100, 160, 160, 160, 156, 170, 170, 167, 170, 120, 109, 116, 134, 86, 64, 75, 86, 60, 53, 51, 43, 56, + 86, 75, 72, 77, 106, 96, 97, 96, 97, 100, 100, 100, 160, 160, 160, 159, 169, 170, 168, 170, 129, + 115, 117, 123, 90, 71, 76, 79, 62, 51, 51, 47, 59, 79, 75, 74, 81, 100, 97, 98, 98, 100, 100, 100, + 100 + }; + byte[] expectedU = + { + 90, 90, 59, 63, 36, 38, 23, 20, 34, 35, 47, 48, 70, 82, 104, 121, 121, 90, 90, 61, 69, 37, 42, 22, + 18, 33, 32, 47, 47, 67, 75, 97, 113, 120, 59, 61, 30, 37, 22, 20, 38, 36, 50, 50, 78, 83, 113, 122, + 142, 166, 164, 63, 69, 37, 43, 20, 18, 34, 32, 48, 47, 70, 73, 102, 110, 136, 166, 166, 36, 37, 22, + 20, 38, 35, 50, 49, 80, 80, 116, 119, 145, 165, 185, 197, 193, 38, 42, 20, 18, 35, 32, 48, 47, 72, + 72, 106, 108, 142, 165, 184, 191, 194, 23, 22, 38, 34, 50, 48, 81, 77, 117, 115, 150, 160, 184, 194, + 212, 220, 217, 20, 18, 36, 32, 49, 47, 76, 71, 111, 108, 148, 164, 185, 190, 208, 217, 219, 34, 33, + 50, 48, 80, 73, 116, 111, 150, 154, 184, 190, 213, 217, 226, 232, 232, 35, 32, 49, 47, 80, 72, 115, + 107, 154, 164, 187, 189, 211, 216, 228, 237, 235, 47, 46, 77, 70, 115, 106, 149, 148, 184, 187, 213, + 214, 226, 230, 227, 223, 224, 48, 47, 83, 73, 119, 108, 159, 164, 190, 189, 214, 216, 229, 236, 229, + 222, 220, 70, 67, 113, 101, 145, 142, 184, 185, 213, 211, 226, 229, 226, 226, 218, 211, 212, 82, 75, + 122, 110, 165, 165, 193, 190, 217, 216, 231, 236, 226, 222, 214, 208, 207, 104, 97, 142, 136, 186, + 184, 212, 208, 227, 228, 227, 229, 218, 214, 196, 185, 188, 121, 113, 166, 166, 197, 191, 220, 217, + 232, 237, 223, 222, 211, 208, 185, 173, 172, 121, 120, 164, 166, 193, 194, 217, 219, 232, 235, 224, + 220, 212, 207, 188, 172, 172 + }; + byte[] expectedV = + { + 240, 240, 201, 206, 172, 174, 136, 136, 92, 90, 55, 50, 37, 30, 26, 23, 23, 240, 240, 204, 213, 173, + 179, 141, 141, 96, 98, 56, 54, 38, 31, 27, 25, 23, 201, 204, 164, 172, 129, 135, 82, 87, 46, 47, 33, + 29, 25, 23, 20, 16, 16, 206, 213, 172, 180, 137, 141, 93, 99, 54, 54, 36, 31, 26, 25, 21, 17, 16, + 172, 173, 129, 138, 81, 89, 45, 49, 32, 30, 24, 24, 19, 16, 42, 55, 51, 174, 179, 136, 141, 89, 99, + 51, 55, 35, 31, 26, 25, 21, 17, 39, 48, 52, 136, 141, 82, 92, 45, 51, 31, 32, 24, 24, 19, 17, 43, + 51, 74, 85, 81, 136, 141, 87, 99, 49, 55, 32, 32, 25, 25, 20, 17, 41, 46, 69, 81, 83, 92, 96, 46, + 53, 32, 34, 24, 25, 18, 18, 44, 47, 76, 81, 103, 117, 116, 90, 97, 48, 54, 30, 31, 24, 25, 18, 17, + 43, 46, 74, 80, 103, 118, 122, 55, 57, 33, 36, 24, 26, 19, 20, 44, 43, 76, 77, 102, 111, 138, 159, + 157, 50, 54, 30, 31, 24, 25, 17, 17, 47, 46, 77, 79, 106, 118, 143, 164, 168, 37, 38, 25, 26, 19, + 21, 43, 41, 75, 73, 103, 106, 138, 152, 174, 195, 194, 30, 31, 23, 25, 16, 17, 51, 46, 81, 79, 111, + 118, 151, 164, 188, 205, 206, 26, 27, 20, 21, 43, 39, 74, 69, 103, 102, 138, 143, 174, 188, 204, + 216, 218, 23, 25, 16, 17, 55, 48, 85, 81, 117, 118, 159, 164, 195, 205, 216, 227, 227, 23, 23, 16, + 16, 51, 52, 81, 83, 116, 122, 157, 168, 194, 206, 218, 227, 227 + }; + + // act + YuvConversion.ConvertRgbToYuv(image, config, memoryAllocator, y, u, v); + + // assert + Assert.True(expectedY.AsSpan().SequenceEqual(y)); + Assert.True(expectedU.AsSpan().SequenceEqual(u.Slice(0, expectedU.Length))); + Assert.True(expectedV.AsSpan().SequenceEqual(v.Slice(0, expectedV.Length))); + } + + [Theory] + [WithFile(TestImages.Png.TestPattern31x31HalfTransparent, PixelTypes.Rgba32)] + public void ConvertRgbToYuv_WithAlpha_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // arrange + using Image image = provider.GetImage(); + Configuration config = image.GetConfiguration(); + MemoryAllocator memoryAllocator = config.MemoryAllocator; + int pixels = image.Width * image.Height; + int uvWidth = (image.Width + 1) >> 1; + using System.Buffers.IMemoryOwner yBuffer = memoryAllocator.Allocate(pixels, AllocationOptions.Clean); + using System.Buffers.IMemoryOwner uBuffer = memoryAllocator.Allocate(uvWidth * image.Height, AllocationOptions.Clean); + using System.Buffers.IMemoryOwner vBuffer = memoryAllocator.Allocate(uvWidth * image.Height, AllocationOptions.Clean); + + Span y = yBuffer.GetSpan(); + Span u = uBuffer.GetSpan(); + Span v = vBuffer.GetSpan(); + byte[] expectedY = + { + 16, 16, 16, 16, 16, 235, 235, 235, 235, 235, 16, 16, 16, 16, 16, 152, 41, 41, 152, 152, 41, 41, 152, + 152, 41, 41, 152, 152, 41, 41, 152, 16, 16, 16, 16, 16, 235, 235, 235, 235, 235, 16, 16, 16, 16, 16, + 152, 41, 41, 152, 152, 41, 41, 152, 152, 41, 41, 152, 152, 41, 41, 152, 16, 16, 16, 16, 16, 235, + 235, 235, 235, 235, 16, 16, 16, 16, 16, 152, 41, 41, 152, 152, 41, 41, 152, 152, 41, 41, 152, 152, + 41, 41, 152, 16, 16, 16, 16, 16, 235, 235, 235, 235, 235, 16, 16, 16, 16, 16, 152, 41, 41, 152, 152, + 41, 41, 152, 152, 41, 41, 152, 152, 41, 41, 152, 16, 16, 16, 16, 16, 235, 235, 235, 235, 235, 16, + 16, 16, 16, 16, 152, 41, 41, 152, 152, 41, 41, 152, 152, 41, 41, 152, 152, 41, 41, 152, 235, 235, + 235, 235, 235, 16, 16, 16, 16, 16, 235, 235, 235, 235, 235, 152, 41, 41, 152, 152, 41, 41, 152, 152, + 41, 41, 152, 152, 41, 41, 152, 235, 235, 235, 235, 235, 16, 16, 16, 16, 16, 235, 235, 235, 235, 235, + 152, 41, 41, 152, 152, 41, 41, 152, 152, 41, 41, 152, 152, 41, 41, 152, 235, 235, 235, 235, 235, 16, + 16, 16, 16, 16, 235, 235, 235, 235, 235, 152, 41, 41, 152, 152, 41, 41, 152, 152, 41, 41, 152, 152, + 41, 41, 152, 235, 235, 235, 235, 235, 16, 16, 16, 16, 16, 235, 235, 235, 235, 235, 152, 41, 41, 152, + 152, 41, 41, 152, 152, 41, 41, 152, 152, 41, 41, 152, 235, 235, 235, 235, 235, 16, 16, 16, 16, 16, + 235, 235, 235, 235, 235, 152, 41, 41, 152, 152, 41, 41, 152, 152, 41, 41, 152, 152, 41, 41, 152, 16, + 16, 16, 16, 16, 235, 235, 235, 235, 235, 16, 16, 16, 16, 16, 152, 41, 41, 152, 152, 41, 41, 152, + 152, 41, 41, 152, 152, 41, 41, 152, 16, 16, 16, 16, 16, 235, 235, 235, 235, 235, 16, 16, 16, 16, 16, + 152, 41, 41, 152, 152, 41, 41, 152, 152, 41, 41, 152, 152, 41, 41, 152, 16, 16, 16, 16, 16, 235, + 235, 235, 235, 235, 16, 16, 16, 16, 16, 152, 41, 41, 152, 152, 41, 41, 152, 152, 41, 41, 152, 152, + 41, 41, 152, 16, 16, 16, 16, 16, 235, 235, 235, 235, 235, 16, 16, 16, 16, 16, 152, 41, 41, 152, 152, + 41, 41, 152, 152, 41, 41, 152, 152, 41, 41, 152, 16, 16, 16, 16, 16, 235, 235, 235, 235, 235, 16, + 16, 16, 16, 16, 152, 41, 41, 152, 152, 41, 41, 152, 152, 41, 41, 152, 152, 41, 41, 152, 82, 82, 82, + 82, 82, 82, 82, 82, 82, 82, 82, 82, 82, 82, 82, 81, 158, 170, 118, 130, 182, 65, 142, 220, 103, 155, + 167, 115, 127, 204, 127, 82, 82, 82, 82, 82, 82, 82, 82, 82, 82, 82, 82, 82, 82, 82, 145, 157, 106, + 118, 170, 118, 130, 207, 90, 142, 154, 103, 114, 192, 115, 127, 82, 82, 82, 82, 82, 82, 82, 82, 82, + 82, 82, 82, 82, 82, 82, 145, 93, 105, 157, 105, 117, 195, 78, 130, 142, 90, 102, 179, 102, 114, 192, + 82, 82, 82, 82, 82, 82, 82, 82, 82, 82, 82, 82, 82, 82, 82, 80, 92, 170, 93, 105, 182, 65, 142, 129, + 77, 155, 167, 90, 102, 179, 62, 82, 82, 82, 82, 82, 82, 82, 82, 82, 82, 82, 82, 82, 82, 82, 80, 157, + 80, 92, 170, 52, 130, 117, 65, 142, 154, 102, 89, 166, 49, 127, 82, 82, 82, 82, 82, 82, 82, 82, 82, + 82, 82, 82, 82, 82, 82, 145, 197, 80, 157, 169, 117, 104, 181, 130, 142, 90, 77, 154, 37, 114, 191, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 209, 67, 144, 156, 105, 117, 169, 117, + 129, 206, 64, 141, 153, 102, 179, 166, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, + 55, 132, 144, 92, 169, 156, 104, 116, 194, 77, 129, 141, 89, 166, 178, 101, 81, 81, 81, 81, 81, 81, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 119, 131, 80, 157, 144, 92, 104, 181, 64, 116, 193, 76, 154, + 166, 89, 101, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 119, 67, 144, 156, 79, 91, + 169, 52, 104, 181, 64, 141, 153, 76, 88, 165, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, + 81, 81, 183, 132, 144, 196, 79, 156, 39, 116, 168, 51, 129, 141, 89, 76, 153, 101, 81, 81, 81, 81, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 119, 131, 183, 66, 143, 155, 104, 156, 168, 116, 128, + 205, 63, 140, 218, 101, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 119, 196, 54, + 131, 208, 91, 143, 155, 103, 115, 193, 51, 128, 205, 88, 165, 41, 41, 41, 41, 41, 41, 41, 41, 41, + 41, 41, 41, 41, 41, 41, 183, 41, 118, 196, 79, 156, 143, 91, 103, 180, 63, 115, 193, 75, 153, 165, + 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 29, 106, 183, 66, 143, 130, 78, 90, 168, + 116, 103, 180, 63, 140, 152, 75, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 93, + 171, 53, 131, 118, 66, 78, 155, 103, 90, 167, 50, 128, 140, 63, 75 + }; + byte[] expectedU = + { + 128, 128, 128, 128, 128, 128, 128, 139, 240, 139, 240, 139, 240, 139, 240, 139, 128, 128, 128, 128, + 128, 128, 128, 139, 240, 139, 240, 139, 240, 139, 240, 139, 128, 128, 128, 128, 128, 128, 128, 139, + 240, 139, 240, 139, 240, 139, 240, 139, 128, 128, 128, 128, 128, 128, 128, 139, 240, 139, 240, 139, + 240, 139, 240, 139, 128, 128, 128, 128, 128, 128, 128, 139, 240, 139, 240, 139, 240, 139, 240, 139, + 128, 128, 128, 128, 128, 128, 128, 139, 240, 139, 240, 139, 240, 139, 240, 139, 128, 128, 128, 128, + 128, 128, 128, 139, 240, 139, 240, 139, 240, 139, 240, 139, 112, 112, 108, 106, 106, 112, 112, 139, + 229, 146, 204, 132, 199, 131, 204, 135, 90, 90, 90, 90, 90, 90, 90, 100, 161, 92, 116, 141, 99, 155, + 113, 97, 90, 90, 90, 90, 90, 90, 90, 173, 145, 114, 173, 122, 133, 127, 96, 170, 96, 96, 96, 96, 96, + 96, 96, 148, 98, 134, 122, 113, 139, 93, 169, 85, 91, 91, 91, 91, 91, 91, 91, 108, 134, 130, 112, + 149, 105, 139, 146, 110, 91, 91, 91, 91, 91, 91, 91, 107, 164, 117, 149, 127, 128, 166, 107, 129, + 159, 159, 159, 159, 159, 159, 159, 161, 112, 113, 138, 87, 143, 112, 88, 161, 240, 240, 240, 240, + 240, 240, 240, 137, 110, 162, 110, 140, 158, 104, 159, 137, 240, 240, 240, 240, 240, 240, 240, 109, + 150, 108, 140, 161, 80, 157, 162, 128 + }; + byte[] expectedV = + { + 128, 128, 128, 128, 128, 128, 128, 189, 110, 189, 110, 189, 110, 189, 110, 189, 128, 128, 128, 128, + 128, 128, 128, 189, 110, 189, 110, 189, 110, 189, 110, 189, 128, 128, 128, 128, 128, 128, 128, 189, + 110, 189, 110, 189, 110, 189, 110, 189, 128, 128, 128, 128, 128, 128, 128, 189, 110, 189, 110, 189, + 110, 189, 110, 189, 128, 128, 128, 128, 128, 128, 128, 189, 110, 189, 110, 189, 110, 189, 110, 189, + 128, 128, 128, 128, 128, 128, 128, 189, 110, 189, 110, 189, 110, 189, 110, 189, 128, 128, 128, 128, + 128, 128, 128, 189, 110, 189, 110, 189, 110, 189, 110, 189, 175, 175, 186, 193, 193, 175, 175, 188, + 109, 172, 108, 164, 119, 152, 92, 189, 240, 240, 240, 240, 240, 240, 240, 104, 121, 131, 142, 135, + 109, 92, 146, 115, 240, 240, 240, 240, 240, 240, 240, 122, 136, 148, 137, 113, 157, 155, 121, 130, + 155, 155, 155, 155, 155, 155, 155, 109, 135, 96, 88, 142, 136, 105, 138, 116, 81, 81, 81, 81, 81, + 81, 81, 143, 104, 148, 150, 111, 138, 128, 116, 141, 81, 81, 81, 81, 81, 81, 81, 63, 147, 133, 119, + 141, 165, 126, 147, 173, 101, 101, 101, 101, 101, 101, 101, 135, 109, 129, 122, 124, 107, 108, 128, + 138, 110, 110, 110, 110, 110, 110, 110, 117, 137, 151, 127, 114, 131, 139, 142, 120, 110, 110, 110, + 110, 110, 110, 110, 142, 156, 119, 137, 167, 141, 151, 66, 85 + }; + + // act + YuvConversion.ConvertRgbToYuv(image, config, memoryAllocator, y, u, v); + + // assert + Assert.True(expectedY.AsSpan().SequenceEqual(y)); + Assert.True(expectedU.AsSpan().SequenceEqual(u.Slice(0, expectedU.Length))); + Assert.True(expectedV.AsSpan().SequenceEqual(v.Slice(0, expectedV.Length))); + } + } +} diff --git a/tests/ImageSharp.Tests/Metadata/ImageMetadataTests.cs b/tests/ImageSharp.Tests/Metadata/ImageMetadataTests.cs index a82ea7017..2456246b6 100644 --- a/tests/ImageSharp.Tests/Metadata/ImageMetadataTests.cs +++ b/tests/ImageSharp.Tests/Metadata/ImageMetadataTests.cs @@ -98,8 +98,8 @@ namespace SixLabors.ImageSharp.Tests.Metadata image.Metadata.SyncProfiles(); - Assert.Equal(400, ((Rational)image.Metadata.ExifProfile.GetValue(ExifTag.XResolution).Value).ToDouble()); - Assert.Equal(500, ((Rational)image.Metadata.ExifProfile.GetValue(ExifTag.YResolution).Value).ToDouble()); + Assert.Equal(400, image.Metadata.ExifProfile.GetValue(ExifTag.XResolution).Value.ToDouble()); + Assert.Equal(500, image.Metadata.ExifProfile.GetValue(ExifTag.YResolution).Value.ToDouble()); } } } diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs index 1f23838ab..ebc096852 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; @@ -27,7 +28,17 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif /// /// Writes a png file. /// - Png + Png, + + /// + /// Writes a lossless webp file. + /// + WebpLossless, + + /// + /// Writes a lossy webp file. + /// + WebpLossy } private static readonly Dictionary TestProfileValues = new Dictionary @@ -44,6 +55,8 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif [Theory] [InlineData(TestImageWriteFormat.Jpeg)] [InlineData(TestImageWriteFormat.Png)] + [InlineData(TestImageWriteFormat.WebpLossless)] + [InlineData(TestImageWriteFormat.WebpLossy)] public void Constructor(TestImageWriteFormat imageFormat) { Image image = TestFile.Create(TestImages.Jpeg.Baseline.Calliphora).CreateRgba32Image(); @@ -92,6 +105,8 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif [Theory] [InlineData(TestImageWriteFormat.Jpeg)] [InlineData(TestImageWriteFormat.Png)] + [InlineData(TestImageWriteFormat.WebpLossless)] + [InlineData(TestImageWriteFormat.WebpLossy)] public void WriteFraction(TestImageWriteFormat imageFormat) { using (var memStream = new MemoryStream()) @@ -135,6 +150,8 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif [Theory] [InlineData(TestImageWriteFormat.Jpeg)] [InlineData(TestImageWriteFormat.Png)] + [InlineData(TestImageWriteFormat.WebpLossless)] + [InlineData(TestImageWriteFormat.WebpLossy)] public void ReadWriteInfinity(TestImageWriteFormat imageFormat) { Image image = TestFile.Create(TestImages.Jpeg.Baseline.Floorplan).CreateRgba32Image(); @@ -170,6 +187,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif https://exiftool.org/TagNames/EXIF.html */ [InlineData(TestImageWriteFormat.Jpeg, 16)] [InlineData(TestImageWriteFormat.Png, 16)] + [InlineData(TestImageWriteFormat.WebpLossless, 16)] public void SetValue(TestImageWriteFormat imageFormat, int expectedProfileValueCount) { Image image = TestFile.Create(TestImages.Jpeg.Baseline.Floorplan).CreateRgba32Image(); @@ -241,6 +259,8 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif [Theory] [InlineData(TestImageWriteFormat.Jpeg)] [InlineData(TestImageWriteFormat.Png)] + [InlineData(TestImageWriteFormat.WebpLossless)] + [InlineData(TestImageWriteFormat.WebpLossy)] public void WriteOnlyExifTags_Works(TestImageWriteFormat imageFormat) { // Arrange @@ -327,7 +347,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif [Fact] public void ReadWriteLargeProfileJpg() { - ExifTag[] tags = new[] { ExifTag.Software, ExifTag.Copyright, ExifTag.Model, ExifTag.ImageDescription }; + ExifTag[] tags = { ExifTag.Software, ExifTag.Copyright, ExifTag.Model, ExifTag.ImageDescription }; foreach (ExifTag tag in tags) { // Arrange @@ -344,7 +364,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif image.Metadata.ExifProfile = expectedProfile; // Act - using Image reloadedImage = WriteAndRead(image, TestImageWriteFormat.Jpeg); + Image reloadedImage = WriteAndRead(image, TestImageWriteFormat.Jpeg); // Assert ExifProfile actualProfile = reloadedImage.Metadata.ExifProfile; @@ -366,7 +386,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif [Fact] public void ExifTypeUndefined() { - // This image contains an 802 byte EXIF profile + // This image contains an 802 byte EXIF profile. // It has a tag with an index offset of 18,481,152 bytes (overrunning the data) using Image image = TestFile.Create(TestImages.Jpeg.Progressive.Bad.ExifUndefType).CreateRgba32Image(); Assert.NotNull(image); @@ -409,6 +429,8 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif [Theory] [InlineData(TestImageWriteFormat.Jpeg)] [InlineData(TestImageWriteFormat.Png)] + [InlineData(TestImageWriteFormat.WebpLossless)] + [InlineData(TestImageWriteFormat.WebpLossy)] public void WritingImagePreservesExifProfile(TestImageWriteFormat imageFormat) { // Arrange @@ -483,6 +505,10 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif return WriteAndReadJpeg(image); case TestImageWriteFormat.Png: return WriteAndReadPng(image); + case TestImageWriteFormat.WebpLossless: + return WriteAndReadWebp(image, WebpFileFormatType.Lossless); + case TestImageWriteFormat.WebpLossy: + return WriteAndReadWebp(image, WebpFileFormatType.Lossy); default: throw new ArgumentException("Unexpected test image format, only Jpeg and Png are allowed"); } @@ -512,6 +538,18 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif } } + private static Image WriteAndReadWebp(Image image, WebpFileFormatType fileFormat) + { + using (var memStream = new MemoryStream()) + { + image.SaveAsWebp(memStream, new WebpEncoder() { FileFormat = fileFormat }); + image.Dispose(); + + memStream.Position = 0; + return Image.Load(memStream, new WebpDecoder()); + } + } + private static void TestProfile(ExifProfile profile) { Assert.NotNull(profile); diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index b0a219711..116c5adc3 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -44,6 +44,7 @@ namespace SixLabors.ImageSharp.Tests public const string CalliphoraPartial = "Png/CalliphoraPartial.png"; public const string CalliphoraPartialGrayscale = "Png/CalliphoraPartialGrayscale.png"; public const string Bike = "Png/Bike.png"; + public const string BikeSmall = "Png/bike-small.png"; public const string BikeGrayscale = "Png/BikeGrayscale.png"; public const string SnakeGame = "Png/SnakeGame.png"; public const string Icon = "Png/icon.png"; @@ -58,6 +59,8 @@ namespace SixLabors.ImageSharp.Tests public const string PngWithMetadata = "Png/PngWithMetaData.png"; public const string InvalidTextData = "Png/InvalidTextData.png"; public const string David = "Png/david.png"; + public const string TestPattern31x31 = "Png/testpattern31x31.png"; + public const string TestPattern31x31HalfTransparent = "Png/testpattern31x31-halftransparent.png"; // Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html public const string Filter0 = "Png/filter0.png"; @@ -514,6 +517,193 @@ namespace SixLabors.ImageSharp.Tests public const string NoAlphaBits32BitRle = "Tga/32bit_rle_no_alphabits.tga"; } + public static class Webp + { + // Reference image as png + public const string Peak = "Webp/peak.png"; + + // Test pattern images for testing the encoder. + public const string TestPatternOpaque = "Webp/testpattern_opaque.png"; + public const string TestPatternOpaqueSmall = "Webp/testpattern_opaque_small.png"; + public const string RgbTestPattern100x100 = "Webp/rgb_pattern_100x100.png"; + public const string RgbTestPattern80x80 = "Webp/rgb_pattern_80x80.png"; + public const string RgbTestPattern63x63 = "Webp/rgb_pattern_63x63.png"; + + // Test image for encoding image with a palette. + public const string Flag = "Webp/flag_of_germany.png"; + + // Test images for converting rgb data to yuv. + public const string Yuv = "Webp/yuv_test.png"; + + 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 Earth = "Webp/earth_lossless.webp"; + public const string Alpha = "Webp/lossless_alpha_small.webp"; + public const string WithExif = "Webp/exif_lossless.webp"; + public const string WithIccp = "Webp/lossless_with_iccp.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 + + // substract_green, predictor, cross_color + public const string ThreeTransforms1 = "Webp/color_cache_bits_11.webp"; + + // color_indexing, predictor, cross_color + public const string ThreeTransforms2 = "Webp/lossless_vec_1_11.webp"; + + // substract_green, predictor, cross_color + public const string ThreeTransforms3 = "Webp/lossless_vec_1_14.webp"; + + // color_indexing, 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"; + + // substract_green, predictor, cross_color + public const string ThreeTransforms6 = "Webp/lossless_vec_2_14.webp"; + + // color_indexing, predictor, cross_color + public const string ThreeTransforms7 = "Webp/lossless_vec_2_15.webp"; + + // substract_green, predictor, cross_color + public const string BikeThreeTransforms = "Webp/bike_lossless.webp"; + + // 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 Earth = "Webp/earth_lossy.webp"; + public const string WithExif = "Webp/exif_lossy.webp"; + public const string WithIccp = "Webp/lossy_with_iccp.webp"; + public const string BikeSmall = "Webp/bike_lossless_small.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 = WithIccp; + 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 a alpha channel. + public const string Alpha1 = "Webp/lossy_alpha1.webp"; + public const string Alpha2 = "Webp/lossy_alpha2.webp"; + public const string Alpha3 = "Webp/alpha_color_cache.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"; + public const string AlphaThinkingSmiley = "Webp/1602311202.webp"; + public const string AlphaSticker = "Webp/sticker.webp"; + + // Issues + public const string Issue1594 = "Webp/issues/Issue1594.webp"; + } + } + public static class Tiff { public const string Benchmark_Path = "Tiff/Benchmarks/"; diff --git a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs index f68fb4d95..4b374b21f 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs @@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; namespace SixLabors.ImageSharp.Tests @@ -57,6 +58,7 @@ namespace SixLabors.ImageSharp.Tests new JpegConfigurationModule(), new GifConfigurationModule(), new TgaConfigurationModule(), + new WebpConfigurationModule(), new TiffConfigurationModule()); IImageEncoder pngEncoder = IsWindows ? (IImageEncoder)SystemDrawingReferenceEncoder.Png : new ImageSharpPngEncoderWithDefaultConfiguration(); diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/TestEnvironmentTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/TestEnvironmentTests.cs index 05f4f032b..782c80ea8 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/TestEnvironmentTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/TestEnvironmentTests.cs @@ -4,12 +4,13 @@ using System; using System.IO; using Microsoft.DotNet.RemoteExecutor; + using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; - using Xunit; using Xunit.Abstractions; @@ -56,6 +57,7 @@ namespace SixLabors.ImageSharp.Tests [InlineData("lol/Rofl.bmp", typeof(SystemDrawingReferenceEncoder))] [InlineData("lol/Baz.JPG", typeof(JpegEncoder))] [InlineData("lol/Baz.gif", typeof(GifEncoder))] + [InlineData("lol/foobar.webp", typeof(WebpEncoder))] public void GetReferenceEncoder_ReturnsCorrectEncoders_Windows(string fileName, Type expectedEncoderType) { if (!TestEnvironment.IsWindows) @@ -72,6 +74,7 @@ namespace SixLabors.ImageSharp.Tests [InlineData("lol/Rofl.bmp", typeof(SystemDrawingReferenceDecoder))] [InlineData("lol/Baz.JPG", typeof(JpegDecoder))] [InlineData("lol/Baz.gif", typeof(GifDecoder))] + [InlineData("lol/foobar.webp", typeof(WebpDecoder))] public void GetReferenceDecoder_ReturnsCorrectDecoders_Windows(string fileName, Type expectedDecoderType) { if (!TestEnvironment.IsWindows) @@ -88,6 +91,7 @@ namespace SixLabors.ImageSharp.Tests [InlineData("lol/Rofl.bmp", typeof(BmpEncoder))] [InlineData("lol/Baz.JPG", typeof(JpegEncoder))] [InlineData("lol/Baz.gif", typeof(GifEncoder))] + [InlineData("lol/foobar.webp", typeof(WebpEncoder))] public void GetReferenceEncoder_ReturnsCorrectEncoders_Linux(string fileName, Type expectedEncoderType) { if (!TestEnvironment.IsLinux) @@ -104,6 +108,7 @@ namespace SixLabors.ImageSharp.Tests [InlineData("lol/Rofl.bmp", typeof(MagickReferenceDecoder))] [InlineData("lol/Baz.JPG", typeof(JpegDecoder))] [InlineData("lol/Baz.gif", typeof(GifDecoder))] + [InlineData("lol/foobar.webp", typeof(WebpDecoder))] public void GetReferenceDecoder_ReturnsCorrectDecoders_Linux(string fileName, Type expectedDecoderType) { if (!TestEnvironment.IsLinux) diff --git a/tests/Images/Input/Png/testpattern31x31-halftransparent.png b/tests/Images/Input/Png/testpattern31x31-halftransparent.png new file mode 100644 index 000000000..56b8a16b2 --- /dev/null +++ b/tests/Images/Input/Png/testpattern31x31-halftransparent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:415483fde21637bdf919244c0625665f4914f7678eb4e047507b9bcd2262ec50 +size 472 diff --git a/tests/Images/Input/Png/testpattern31x31.png b/tests/Images/Input/Png/testpattern31x31.png new file mode 100644 index 000000000..f7abd7959 --- /dev/null +++ b/tests/Images/Input/Png/testpattern31x31.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:233dc410d723204dfd63c296ee3ca165f4a3641475627eb82f5515f31d25afe5 +size 484 diff --git a/tests/Images/Input/Webp/1602311202.webp b/tests/Images/Input/Webp/1602311202.webp new file mode 100644 index 000000000..4dfd0184f --- /dev/null +++ b/tests/Images/Input/Webp/1602311202.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4dac76bec0e5b23a988b0f2221e9b20e63dc207ef48f33e49a4336a874e2a915 +size 18406 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 000000000..ec5d7540e --- /dev/null +++ b/tests/Images/Input/Webp/alpha_color_cache.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4b9e2459858e6f6a1d919c2adeb73d7e2ad251d6bfbfb99303a6b4508ca757a +size 1838 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 000000000..d85e800dc --- /dev/null +++ b/tests/Images/Input/Webp/alpha_filter_0_method_0.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:150ff79f16e254281153d7e75e5968663c7f83ae58217b36c12d11088045eb07 +size 22038 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 000000000..56318eca9 --- /dev/null +++ b/tests/Images/Input/Webp/alpha_filter_0_method_1.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96f514bce6faa65330eba17c06eb1cf120ba8c133288ab2633afa63d7c6c66ad +size 12162 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 000000000..216f2eef6 --- /dev/null +++ b/tests/Images/Input/Webp/alpha_filter_1.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53ad8a7038fed7bdfa90db1c1f987782a9f46903aaccd5ad04dd78d067632fba +size 114 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 000000000..94a605e13 --- /dev/null +++ b/tests/Images/Input/Webp/alpha_filter_1_method_0.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4dc2d060c723aa0a855bc696191d220a60a36bad014cda9764ee93516fa9f073 +size 22038 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 000000000..a3f0cd93e --- /dev/null +++ b/tests/Images/Input/Webp/alpha_filter_1_method_1.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79000c9c553f28e4ea589e772dd46a47a604f961050ca191a84a03d92d212eb8 +size 15592 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 000000000..d38845444 --- /dev/null +++ b/tests/Images/Input/Webp/alpha_filter_2.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a1eba20a9ba6a09735c2424d31e26c9282be64a0d7795dd689a42c4921d436b +size 114 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 000000000..e5429119f --- /dev/null +++ b/tests/Images/Input/Webp/alpha_filter_2_method_0.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d169bfee11e65f1b5870142531d1e35539e2686640d19fa196b36a5b7b33a45 +size 22038 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 000000000..e7bffc1db --- /dev/null +++ b/tests/Images/Input/Webp/alpha_filter_2_method_1.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16ce80b417c8d5d95d895e3a50be00247262a3ab5700e2a22a408f5042884042 +size 15604 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 000000000..b75c44759 --- /dev/null +++ b/tests/Images/Input/Webp/alpha_filter_3.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4052952ea401afa934b2d262f2852f615160c7cb82c4406c47622d26b343e95e +size 118 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 000000000..ca0baef06 --- /dev/null +++ b/tests/Images/Input/Webp/alpha_filter_3_method_0.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79c7f73faec6a9b0f7ec5d274a3dd10a7eb002ebab114122a49f9794c5b8541a +size 22038 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 000000000..414723d96 --- /dev/null +++ b/tests/Images/Input/Webp/alpha_filter_3_method_1.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e11b944fd8aa2e5f90c7bea05527a94e919492bef0bc464bac1493e00724ae01 +size 18266 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 000000000..a7d058e89 --- /dev/null +++ b/tests/Images/Input/Webp/alpha_no_compression.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:864de77c2209a8346004f8fe5595ffb35cfaacb71f385cc8487236689056df7d +size 336 diff --git a/tests/Images/Input/Webp/animated-webp.webp b/tests/Images/Input/Webp/animated-webp.webp new file mode 100644 index 000000000..d221bc0ca --- /dev/null +++ b/tests/Images/Input/Webp/animated-webp.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf633cfad0fba9b53ef84f0319db15537868bbe75c7b3cd0f31add9c0d25addf +size 37341 diff --git a/tests/Images/Input/Webp/animated2.webp b/tests/Images/Input/Webp/animated2.webp new file mode 100644 index 000000000..aa08cae87 --- /dev/null +++ b/tests/Images/Input/Webp/animated2.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b17cfa1c0f484f1fc03f16d07684831585125817e5c7fb2c12cfed3d6ad863a8 +size 11840 diff --git a/tests/Images/Input/Webp/animated3.webp b/tests/Images/Input/Webp/animated3.webp new file mode 100644 index 000000000..98d4c4114 --- /dev/null +++ b/tests/Images/Input/Webp/animated3.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68ba327459ac40a7a054dc5d8b237d3ce0154524854a4f2334e3b839524d13a9 +size 41063 diff --git a/tests/Images/Input/Webp/animated_lossy.webp b/tests/Images/Input/Webp/animated_lossy.webp new file mode 100644 index 000000000..654c2d03f --- /dev/null +++ b/tests/Images/Input/Webp/animated_lossy.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54957c3daa3ab0bf258c00b170fcfc0578d909acd5dfc870b752688b9b64e406 +size 73772 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 000000000..dd8e7fd3f --- /dev/null +++ b/tests/Images/Input/Webp/bad_palette_index.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8540997edf54f030201f7354f52438dd04bf248fb21a72b71a93095fc681fb5e +size 9682 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 000000000..ae0c85b42 --- /dev/null +++ b/tests/Images/Input/Webp/big_endian_bug_393.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f2359f5425d78dbe16665d9e15cb56b84559eff527ab316c509a5dd1708c126 +size 16313 diff --git a/tests/Images/Input/Webp/bike_lossless.webp b/tests/Images/Input/Webp/bike_lossless.webp new file mode 100644 index 000000000..a311c5af1 --- /dev/null +++ b/tests/Images/Input/Webp/bike_lossless.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a552b43d45c77ece0ab4331f054fb183725420748656d47a49c5b672e42f4f9 +size 61782 diff --git a/tests/Images/Input/Webp/bike_lossless_small.webp b/tests/Images/Input/Webp/bike_lossless_small.webp new file mode 100644 index 000000000..661129451 --- /dev/null +++ b/tests/Images/Input/Webp/bike_lossless_small.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0cff46a5bbc4903e8e372ee79e988942c101a6cc6642658cb92a9f377443dca +size 2598 diff --git a/tests/Images/Input/Webp/bike_lossy.webp b/tests/Images/Input/Webp/bike_lossy.webp new file mode 100644 index 000000000..a9e2fc6a8 --- /dev/null +++ b/tests/Images/Input/Webp/bike_lossy.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f93883b8ba4ebc9c048c598b9294736baddfa756c4884e85f0d3b8e7f9d996c +size 39244 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 000000000..73eabf363 --- /dev/null +++ b/tests/Images/Input/Webp/bike_lossy_complex_filter.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21612476f2d7668f773ce286af1a2a4c33da8718352c5a5c1dd839a4643de823 +size 9396 diff --git a/tests/Images/Input/Webp/bryce.webp b/tests/Images/Input/Webp/bryce.webp new file mode 100644 index 000000000..763ac2428 --- /dev/null +++ b/tests/Images/Input/Webp/bryce.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6ba1142638a7ae9df901bb7fc8e3a9e6afbe0ec8320fd28e35308a57a2e3e4f +size 3533772 diff --git a/tests/Images/Input/Webp/bug3.webp b/tests/Images/Input/Webp/bug3.webp new file mode 100644 index 000000000..97ae77e91 --- /dev/null +++ b/tests/Images/Input/Webp/bug3.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37d98a0b3e2132f7b5bbc03935a88a8659735c61268d8ce6acdfccfa574f4166 +size 954 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 000000000..29a7f190f --- /dev/null +++ b/tests/Images/Input/Webp/color_cache_bits_11.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a19dde0c51ce4c83d9bc05ea3b8f3cfed9cfac7ca19dcb23d85c56e465242350 +size 15822 diff --git a/tests/Images/Input/Webp/earth_lossless.webp b/tests/Images/Input/Webp/earth_lossless.webp new file mode 100644 index 000000000..1abcb8668 --- /dev/null +++ b/tests/Images/Input/Webp/earth_lossless.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35e61613388342baac7f39a4a3c3ae32587a065505269115a134592eee9563b8 +size 7813062 diff --git a/tests/Images/Input/Webp/earth_lossy.webp b/tests/Images/Input/Webp/earth_lossy.webp new file mode 100644 index 000000000..790a194de --- /dev/null +++ b/tests/Images/Input/Webp/earth_lossy.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c45c068709fa3f878564d399e539636b9e42926291dde683adb7bb5d98c2c680 +size 467258 diff --git a/tests/Images/Input/Webp/exif_lossless.webp b/tests/Images/Input/Webp/exif_lossless.webp new file mode 100644 index 000000000..a3eeae555 --- /dev/null +++ b/tests/Images/Input/Webp/exif_lossless.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21de077dd545c182a36584955918a70643ae2b972b208234f548d95ef8535a3e +size 183286 diff --git a/tests/Images/Input/Webp/exif_lossy.webp b/tests/Images/Input/Webp/exif_lossy.webp new file mode 100644 index 000000000..35e454b96 --- /dev/null +++ b/tests/Images/Input/Webp/exif_lossy.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fdf4e9b20af4168f4177d33f7f502906343bbaaae2af9b90e1531bd4452b317b +size 40765 diff --git a/tests/Images/Input/Webp/flag_of_germany.png b/tests/Images/Input/Webp/flag_of_germany.png new file mode 100644 index 000000000..f6a4438fb --- /dev/null +++ b/tests/Images/Input/Webp/flag_of_germany.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26bf39cea75210c9132eec4567f1f63c870b1eec3b541cfc25da7b5095902f41 +size 72315 diff --git a/tests/Images/Input/Webp/issues/Issue1594.webp b/tests/Images/Input/Webp/issues/Issue1594.webp new file mode 100644 index 000000000..664db4e2f --- /dev/null +++ b/tests/Images/Input/Webp/issues/Issue1594.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37413b1a89ba7d42cdfe98196775c2ddc2f8f4d143f6fc65218dc288423b7177 +size 62 diff --git a/tests/Images/Input/Webp/lossless1.webp b/tests/Images/Input/Webp/lossless1.webp new file mode 100644 index 000000000..1d561f9ad --- /dev/null +++ b/tests/Images/Input/Webp/lossless1.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5eaf3d3e7f7a38487afa8d3f91062167eb061cd6a5dfa455d24a9a2004860311 +size 15368 diff --git a/tests/Images/Input/Webp/lossless2.webp b/tests/Images/Input/Webp/lossless2.webp new file mode 100644 index 000000000..1c975384f --- /dev/null +++ b/tests/Images/Input/Webp/lossless2.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5131b5d7c0ba6bd7d6e6f74a325e0ffa2d388197b5132ed46a5c36ea8453cb22 +size 15898 diff --git a/tests/Images/Input/Webp/lossless3.webp b/tests/Images/Input/Webp/lossless3.webp new file mode 100644 index 000000000..34bc7919f --- /dev/null +++ b/tests/Images/Input/Webp/lossless3.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2bcca2ea2a1a43d19c839528e9b831519e0a6875e4c9a2ce8bb9c34bb85ece3a +size 15734 diff --git a/tests/Images/Input/Webp/lossless4.webp b/tests/Images/Input/Webp/lossless4.webp new file mode 100644 index 000000000..5c46787d1 --- /dev/null +++ b/tests/Images/Input/Webp/lossless4.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a96cc5243569ada325efadb3a6c78816b4a015a73283a400c5cc94893584901f +size 4332 diff --git a/tests/Images/Input/Webp/lossless_alpha_small.webp b/tests/Images/Input/Webp/lossless_alpha_small.webp new file mode 100644 index 000000000..304080f93 --- /dev/null +++ b/tests/Images/Input/Webp/lossless_alpha_small.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d078eb784835863f12ef25d9c1c135e79c2495532cec08da6f19c2e27c0cacee +size 1638 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 000000000..a2baaf1a3 --- /dev/null +++ b/tests/Images/Input/Webp/lossless_big_random_alpha.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:40928dc5a6ca61e7008d212e66b24f5e62f43d5fe55f23add9843414168cbaa6 +size 13968249 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 000000000..89276eae4 --- /dev/null +++ b/tests/Images/Input/Webp/lossless_color_transform.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b9557b7f3798bb9b511f2edad5dad330d7346f5f13440a70627488f9a53ec81 +size 163807 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 000000000..ea5faa2d2 --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_1_0.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:011089057caf7e11c9a59d3ec2b3448ea56d83545622e313f8584a22c322bc90 +size 50 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 000000000..6cdad61d0 --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_1_1.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:482c1304367ede7a4b2e43e14aefced318c075e82e466473720d3bdabc0526fc +size 106 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 000000000..39475bf46 --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_1_10.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f91a575ba29729357a612eb511a9ebab725c2d34a6a6eaaf6b6a16cee3ba25a2 +size 80 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 000000000..d516737cd --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_1_11.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e598e8d2aef6a562a80c3fc9cb7cc6fdd979e210c26ed3a4defbdf895ae1c1cc +size 132 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 000000000..6f8ed9551 --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_1_12.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45fa843b9d374e1949f58e9d0d2a2ecf97d4a9cc2af55dfa3ef488d846ea3c80 +size 56 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 000000000..2e2bb6dcd --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_1_13.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04a9d1c2e8b43224f5d12623e4a085be1e1c5dda716bfd108202c64b1d796179 +size 114 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 000000000..55b0f3b10 --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_1_14.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8b8130c71e6958fd7eab9539f431125a35075cf0c38fc7ebb1316aa0a4d1946 +size 78 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 000000000..13f3ff7b2 --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_1_15.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f06eaa2e3fdc11235d9b9c14485e6e5a83f4f4de10caf05a70c0fdfc253c7a67 +size 130 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 000000000..8971121c0 --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_1_2.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eed151dabacbad9b99aa5ad47787240f5344d8cd653f2c3842ccc0f95d6ce798 +size 76 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 000000000..5060ae091 --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_1_3.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45a32abfcc449acff80249885078ffe56d244d85db7120fdb30f58ae2bf89ac9 +size 132 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 000000000..b346c4216 --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_1_4.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6021a817ad3e17053de4b170b9ff2c7646ee2ef365b23a5e75bea3159d83023a +size 50 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 000000000..f2a2aa0d3 --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_1_5.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:673161af330a911bd6a3bc3f0ab266a34eafba139a174372dd20727b8831e7e1 +size 106 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 000000000..248bcf6ba --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_1_6.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b90db591321b6235cfbf2c4a9083f29459185b84953c7ca02b47da16f82df149 +size 76 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 000000000..788e7a33a --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_1_7.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89aaf7749eefb289403ca6bdac93c0b80ac47da498f5064ea9f064994479045e +size 122 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 000000000..d55c10e10 --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_1_8.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c255d6803fb2d9fa549322e9eb1ac5641ee091c32a24810310464d207bb73cc +size 56 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 000000000..07f0cdb54 --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_1_9.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99e579d9b23ac41a1c6162b6c0585d7cd9a797058f7c89e5a5a245bd159cd1b0 +size 112 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 000000000..f338a8642 --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_2_0.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b90cb047e529364197e0435122e96be3e105c7e4b21688a56be9532af9a08609 +size 12822 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 000000000..007695445 --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_2_1.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ee2154490d6342aff5bbebae8a29aa00ba2aa4630b5c071fe7f45c327e1e56b +size 10672 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 000000000..7c5ee058c --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_2_10.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ef60485d13cd7c19d7974707d3c0b00bb8518f653669b992e1de9aea4fdd305 +size 20362 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 000000000..e029941fd --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_2_11.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:418e017cafcf4bbb09ed95c7ec7d3a08935b3e266b2de2a3b392eb7c0db7e408 +size 10980 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 000000000..59d05f33e --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_2_12.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0923abbeaf98db4d50d5a857ec4a61ca2cdf90cb9f7819e07e101c4fda574af0 +size 14280 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 000000000..5ba8186a4 --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_2_13.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13abcefd4630e562f132040956aa71a66df244e18ea4454bcb58c390aba0e3a7 +size 9818 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 000000000..e2ec8c74c --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_2_14.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f73aea1e60ae1d702aefd5df65c64920c7a1f7c547ecee1189864c9ecd118c00 +size 20704 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 000000000..f3c130168 --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_2_15.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b5cb1bf92525785231986c48f9d668b22166d2ff78b1a1f3fdcae6548c5e24b +size 11438 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 000000000..694201b29 --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_2_2.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:880f63d6da0647bc1468551a5117b225f84f0e9c6df0cbb7e9cffbebcec159da +size 21444 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 000000000..8bb0a902e --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_2_3.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50bbb2e1c3e8fb8ee86beb034a0d0673b6238fa16f83479b38dec90aba4a9019 +size 11432 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 000000000..53eb696ff --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_2_4.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ec19bfa2bd9852cc735320dde4fa7047b14aca8281f3fbc1ad5fa3ad8215d6b +size 12491 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 000000000..e6f83941f --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_2_5.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09b72c5e236ef7c27a1cc80677e2c63f2b8effd004009de1c62fad88d4ad6559 +size 10294 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 000000000..bc17d4ee3 --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_2_6.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6f63102a86ec168f1eeb4ad3bd80fc7c1db0d48b767736591108320b5bed9f8 +size 21922 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 000000000..81871bebc --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_2_7.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99353d740c62b60ddc594648f5885380aeb2ebe2acb0feb15a115539c6eebdc1 +size 11211 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 000000000..9656571eb --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_2_8.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:215ace49899cf63ade891f4ec802ecb9657001c51fbd1a8c2f0880bc4fb2760a +size 12640 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 000000000..831be6c32 --- /dev/null +++ b/tests/Images/Input/Webp/lossless_vec_2_9.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:033e7d1034513392a7b527176eeb7fab22568af5c2365dd1f65fdc3ad4c0f270 +size 10304 diff --git a/tests/Images/Input/Webp/lossless_with_iccp.webp b/tests/Images/Input/Webp/lossless_with_iccp.webp new file mode 100644 index 000000000..56897125a --- /dev/null +++ b/tests/Images/Input/Webp/lossless_with_iccp.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:863db3c8970769ec4fc6ab729abbd172a14e3fbb22bc3530d0288761506d751e +size 75858 diff --git a/tests/Images/Input/Webp/lossy_alpha1.webp b/tests/Images/Input/Webp/lossy_alpha1.webp new file mode 100644 index 000000000..9f1e3c2be --- /dev/null +++ b/tests/Images/Input/Webp/lossy_alpha1.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:403dff2d4cffc78607bcd6088fade38ed4a0b26e83b2927b0b1f28c0a826ef1c +size 19478 diff --git a/tests/Images/Input/Webp/lossy_alpha2.webp b/tests/Images/Input/Webp/lossy_alpha2.webp new file mode 100644 index 000000000..a3cbe5c23 --- /dev/null +++ b/tests/Images/Input/Webp/lossy_alpha2.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f2cd2585d5254903227bd86f367b400861cde62db9337fb74dd98d6123ce06c +size 13566 diff --git a/tests/Images/Input/Webp/lossy_alpha3.webp b/tests/Images/Input/Webp/lossy_alpha3.webp new file mode 100644 index 000000000..f87deec5a --- /dev/null +++ b/tests/Images/Input/Webp/lossy_alpha3.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca1d20c440c56eb8b1507e2abafe3447a4f4e11f3d4976a0dc1e93df68881126 +size 9960 diff --git a/tests/Images/Input/Webp/lossy_alpha4.webp b/tests/Images/Input/Webp/lossy_alpha4.webp new file mode 100644 index 000000000..82193f4b8 --- /dev/null +++ b/tests/Images/Input/Webp/lossy_alpha4.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2feb221aee944cb273b11cf02c268601d657f6a8def745e4a6b24031650cd701 +size 4262 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 000000000..94110f8fe --- /dev/null +++ b/tests/Images/Input/Webp/lossy_extreme_probabilities.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4bad65a42ed076a8684494c8a11eb8be02da328195228aa635276f90b4523f27 +size 468740 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 000000000..c10e07c2c --- /dev/null +++ b/tests/Images/Input/Webp/lossy_q0_f100.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf480a1328f5f68b541f80e8af1bf82545f948874dd05aacd355adee2b7ca935 +size 270 diff --git a/tests/Images/Input/Webp/lossy_with_iccp.webp b/tests/Images/Input/Webp/lossy_with_iccp.webp new file mode 100644 index 000000000..2f50e7673 --- /dev/null +++ b/tests/Images/Input/Webp/lossy_with_iccp.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:434cfe308cfcaef8d79f030906fe783df70e568d66df2e906dd98f2ffd5bcc1b +size 63036 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 000000000..86c426aa5 --- /dev/null +++ b/tests/Images/Input/Webp/near_lossless_75.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12e6b033cb2e636224bd787843bc528cfe42f33fd7c1f3814b1f77269b1ec2ab +size 45274 diff --git a/tests/Images/Input/Webp/peak.png b/tests/Images/Input/Webp/peak.png new file mode 100644 index 000000000..5a417b9c0 --- /dev/null +++ b/tests/Images/Input/Webp/peak.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9b56ed5c1278664222c77f9a452b824b4f9215c819502b3f6b0e0d44270e7e7 +size 26456 diff --git a/tests/Images/Input/Webp/rgb_pattern_100x100.png b/tests/Images/Input/Webp/rgb_pattern_100x100.png new file mode 100644 index 000000000..789424dcb --- /dev/null +++ b/tests/Images/Input/Webp/rgb_pattern_100x100.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12f39b990367eb09ffbe69eb11bf970b5386e75a02a820e4740e66a079dda527 +size 30225 diff --git a/tests/Images/Input/Webp/rgb_pattern_63x63.png b/tests/Images/Input/Webp/rgb_pattern_63x63.png new file mode 100644 index 000000000..37a6e8812 --- /dev/null +++ b/tests/Images/Input/Webp/rgb_pattern_63x63.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a7826312b4dabc2d8a89bf84e501ddb0bcc09932c54d2dedb0c96909da94da8 +size 12071 diff --git a/tests/Images/Input/Webp/rgb_pattern_80x80.png b/tests/Images/Input/Webp/rgb_pattern_80x80.png new file mode 100644 index 000000000..d4722cfc1 --- /dev/null +++ b/tests/Images/Input/Webp/rgb_pattern_80x80.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4e27705d23ff33dbac5bdfe8e7e75a6eeda359ff343594fb07feb29abbc2fb5 +size 19393 diff --git a/tests/Images/Input/Webp/segment01.webp b/tests/Images/Input/Webp/segment01.webp new file mode 100644 index 000000000..0f1da8f91 --- /dev/null +++ b/tests/Images/Input/Webp/segment01.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4236341aa2f02c44ea0e6caa9a6b0c059ac87ca1d490821ce81dbb565732c5d0 +size 7658 diff --git a/tests/Images/Input/Webp/segment02.webp b/tests/Images/Input/Webp/segment02.webp new file mode 100644 index 000000000..94cc9b077 --- /dev/null +++ b/tests/Images/Input/Webp/segment02.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99ecbb7b2b42128050242e67eb0ae424e09f2a886f9cd862d1cf176fcdf1542b +size 7112 diff --git a/tests/Images/Input/Webp/segment03.webp b/tests/Images/Input/Webp/segment03.webp new file mode 100644 index 000000000..c15e40f8e --- /dev/null +++ b/tests/Images/Input/Webp/segment03.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbe0d3c0b2d180ea1ed92ea6321667071dea2831991741f9769745947c37ff42 +size 5470 diff --git a/tests/Images/Input/Webp/small_13x1.webp b/tests/Images/Input/Webp/small_13x1.webp new file mode 100644 index 000000000..5707e7e32 --- /dev/null +++ b/tests/Images/Input/Webp/small_13x1.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29121ecb41cb11a2e0301f20aaa10b971bcf93d711e402fc7e331d01d86b7cf1 +size 106 diff --git a/tests/Images/Input/Webp/small_1x1.webp b/tests/Images/Input/Webp/small_1x1.webp new file mode 100644 index 000000000..77ff63d2b --- /dev/null +++ b/tests/Images/Input/Webp/small_1x1.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f34799482dd5349b549d113fdaa188714d9737fe414e71541b752627bedbde3 +size 94 diff --git a/tests/Images/Input/Webp/small_1x13.webp b/tests/Images/Input/Webp/small_1x13.webp new file mode 100644 index 000000000..f361421c3 --- /dev/null +++ b/tests/Images/Input/Webp/small_1x13.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15acf5b0193273afc3cfa4207718b50acd79ffea20cd6a6beab01717d080887a +size 106 diff --git a/tests/Images/Input/Webp/small_31x13.webp b/tests/Images/Input/Webp/small_31x13.webp new file mode 100644 index 000000000..dbb81c189 --- /dev/null +++ b/tests/Images/Input/Webp/small_31x13.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9649f11b7ff9ffde0119b57b7728d837a3f04854e92ef03f2f06d79fbf63748b +size 262 diff --git a/tests/Images/Input/Webp/sticker.webp b/tests/Images/Input/Webp/sticker.webp new file mode 100644 index 000000000..ae781c2d0 --- /dev/null +++ b/tests/Images/Input/Webp/sticker.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49795fc80522dae2ca687b345c21e9b0848f307d3cc3e39fbdcda730772d338c +size 27734 diff --git a/tests/Images/Input/Webp/test-nostrong.webp b/tests/Images/Input/Webp/test-nostrong.webp new file mode 100644 index 000000000..222a0c0f9 --- /dev/null +++ b/tests/Images/Input/Webp/test-nostrong.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:85e7ca4f1361394a29f4f0c9d4e5a31f20c2cc6b8816f991bad80b523941e2f9 +size 1968 diff --git a/tests/Images/Input/Webp/test.webp b/tests/Images/Input/Webp/test.webp new file mode 100644 index 000000000..b403414b9 --- /dev/null +++ b/tests/Images/Input/Webp/test.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3665d7a6fd48ff60137ae29ac6792207aed3f768c2c05ef324f64c78352d5a5 +size 4928 diff --git a/tests/Images/Input/Webp/testpattern_opaque.png b/tests/Images/Input/Webp/testpattern_opaque.png new file mode 100644 index 000000000..4f1f3ea09 --- /dev/null +++ b/tests/Images/Input/Webp/testpattern_opaque.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b89449ae398c5b54a120b6b1e6b394e6d5cd58f0a55e5fb86f759fa12dcd325f +size 1983 diff --git a/tests/Images/Input/Webp/testpattern_opaque_small.png b/tests/Images/Input/Webp/testpattern_opaque_small.png new file mode 100644 index 000000000..62cdcf141 --- /dev/null +++ b/tests/Images/Input/Webp/testpattern_opaque_small.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:81ef8da0aae89095da92ac82830e0f3de935d62248954e577bf4d573158ffd5f +size 35660 diff --git a/tests/Images/Input/Webp/very_short.webp b/tests/Images/Input/Webp/very_short.webp new file mode 100644 index 000000000..f1297cfc3 --- /dev/null +++ b/tests/Images/Input/Webp/very_short.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50c3d70b2fd3caad1fbe01b7a0a6b0c9152525b2ed4dde7a50fbba6c1ea6a0d6 +size 86 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 000000000..410e0a090 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-00-comprehensive-001.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44e1ddf3593d26148a03fb95379e03bb21fb397e3c9a26d64b47433e521d91c6 +size 754 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 000000000..d16d3e20d --- /dev/null +++ b/tests/Images/Input/Webp/vp80-00-comprehensive-002.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cbd5f5c38f1d2692b1e21b9981d50f71522faff68d57929674899c810cb5ed88 +size 4448 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 000000000..ca443b856 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-00-comprehensive-003.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:526dbfc452a5c72edc672a2bed229196a67232dcc852b0b5c806d82e9bc108f2 +size 4500 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 000000000..b956bf8db --- /dev/null +++ b/tests/Images/Input/Webp/vp80-00-comprehensive-004.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9fb9183e44744997a9afec7242120c2e92c1dd9a9351ca5312abbe5ee0606c39 +size 754 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 000000000..48574db18 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-00-comprehensive-005.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:788a937a4287e41d8116fc8f79673fea47a90090e68bdc966af9a14943eff11c +size 4444 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 000000000..e74cf8997 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-00-comprehensive-006.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9642771e93f4d43fa0cde4116a447dc108cada765c94c0d46cdd1750d3712db8 +size 8528 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 000000000..d727a3586 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-00-comprehensive-007.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b44d063894291fb1b5309206bf476a2daa3263373db4e9f32a5f9a525b3d8b59 +size 346 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 000000000..1e139b39d --- /dev/null +++ b/tests/Images/Input/Webp/vp80-00-comprehensive-008.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c3e47eaa974319611baa3c12faeb9ce9ffbdd73e088c0f9dde164384b6b866c +size 45636 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 000000000..80bd6f70c --- /dev/null +++ b/tests/Images/Input/Webp/vp80-00-comprehensive-009.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc9d71fffce845fc46717bf7fc89647a711afd4398a3c16668727aa38585f5c3 +size 8502 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 000000000..7fcff7b58 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-00-comprehensive-010.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de3cef909dc6cf5f0211351401ab1b303efe2358b9654a8d6ac01e3a4f29178b +size 16050 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 000000000..8dcacc6ef --- /dev/null +++ b/tests/Images/Input/Webp/vp80-00-comprehensive-011.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a856ab961966aad8dde337c71303f145e24e3d8a066eeb7a08d323d90c84221e +size 758 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 000000000..b660134df --- /dev/null +++ b/tests/Images/Input/Webp/vp80-00-comprehensive-012.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ffe43365b937f075ee508159ae166fec7bc0671358ff5c2bdc8f5689b20f860 +size 1044 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 000000000..51b2d184a --- /dev/null +++ b/tests/Images/Input/Webp/vp80-00-comprehensive-013.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d5301de57b7fd3cfdf55e463020742a88e0d3b522e602090acc2cf1f7a264ef +size 758 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 000000000..1d537103b --- /dev/null +++ b/tests/Images/Input/Webp/vp80-00-comprehensive-014.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:957cf5dfd41e46ee332791082fdb2e42ca63881a0b76865ced7a09c4fbab6138 +size 11982 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 000000000..ca82dcaa1 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-00-comprehensive-015.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d65d42a905b9cdccc38b968573f9b7d86a296dbb3972ec07ae56caae84ee40f1 +size 7412 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 000000000..eda3b185c --- /dev/null +++ b/tests/Images/Input/Webp/vp80-00-comprehensive-016.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77d17a74d586ccd2abea002e7b966d2b887bab68be2aa1c3866d5ea2f3587e76 +size 188 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 000000000..abedc9556 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-00-comprehensive-017.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0797cd3ff4e12e1de9a2330ccd78fb675e6d2d7422803350a00df5c1125faaf8 +size 188 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 000000000..3f53c34e5 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-01-intra-1400.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:385b683228d78120162c4be79287010bba55175ed06c5ad0d2fe82f837704641 +size 15294 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 000000000..89436b3cf --- /dev/null +++ b/tests/Images/Input/Webp/vp80-01-intra-1411.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9330d735d1cdda007810e76a9cd836f07a6e3954363a0f82b1aca52adf346b4 +size 11963 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 000000000..f1171b9cc --- /dev/null +++ b/tests/Images/Input/Webp/vp80-01-intra-1416.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d3e253211155418d55618ac0a70ed1118a96917ce63b129bc49914d09f3620cf +size 11227 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 000000000..23e8c8fc6 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-01-intra-1417.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d28624b166b4bcff6494f53f14e3335d5a762faa8c8e7fbfb0045f2b04123724 +size 11364 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 000000000..6853283e1 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-02-inter-1402.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ea8dcf7462d978ce3f5a38f505caebe09b3d9170a03fdb6479a8111c6bf54c2 +size 15294 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 000000000..0af4ef532 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-02-inter-1412.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2286cf0613e7a910b44494338157ef73504fefd88cd9427b4aecfbed7a034ae +size 11963 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 000000000..8d825257b --- /dev/null +++ b/tests/Images/Input/Webp/vp80-02-inter-1418.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7a9da63fdec6ca0c42447867f6a7c7d165b0c3fcbf9313cacd6fc8eeb79a6fa +size 17680 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 000000000..934cae5bf --- /dev/null +++ b/tests/Images/Input/Webp/vp80-02-inter-1424.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da8ce9c3078e56a9281620cace12abb39aebdfc0ab25a6586f676e3cf2981704 +size 5254 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 000000000..c4f23515a --- /dev/null +++ b/tests/Images/Input/Webp/vp80-03-segmentation-1401.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a243611a69fef3a8306dd3c48d645d7d4295f60781428b39e1f32bea5c5df46c +size 15296 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 000000000..a0322ce69 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-03-segmentation-1403.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:007c78b248d71d135638637417a458d0a89ba3a46850df4503d10b576a3433c6 +size 15296 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 000000000..4075c52a3 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-03-segmentation-1407.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7fdcc6f20e730074602fbf4fbfbb76f614f13f7bdb7ce038ba32dc691fdfd09 +size 26388 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 000000000..737b281b3 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-03-segmentation-1408.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83c9a6874afc10ef08b853b7c990947fe78206b6a9ca8ad092fda3941d78d2b4 +size 26392 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 000000000..0af47a0a7 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-03-segmentation-1409.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52c0f64e79056e8927ec9625e3fcfe3b0665afe30a12270f3c61665e80e5f4ed +size 26402 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 000000000..10cbce996 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-03-segmentation-1410.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28dd9a50e95436ca29fd8b191d6073a6a7c049de732e512bb127b925eeba9102 +size 26420 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 000000000..6087cae87 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-03-segmentation-1413.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a757b7f0b539d51d9c450bce6f40c99d00109a8b61ea327a07867ebce23c397 +size 11998 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 000000000..d4ac35db2 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-03-segmentation-1414.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f9fe429dbef950dbd9b096b22566f4df4e036af0d95a4c05c954da44490e4af +size 19884 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 000000000..52ee59a12 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-03-segmentation-1415.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:feac273c9e5152e6f9ccdffa65f0a9ce863abbbd446625a32ad42922452429e3 +size 19877 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 000000000..d3e3ff1de --- /dev/null +++ b/tests/Images/Input/Webp/vp80-03-segmentation-1425.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65b704903e9e11c43132aef1d9353011928954c9d5fdd5312477de40ddb26fb9 +size 3632 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 000000000..1a444068a --- /dev/null +++ b/tests/Images/Input/Webp/vp80-03-segmentation-1426.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11bc8b1f337cbe0dd1760eee1483d82243147917c5588b9463353a71c0b03271 +size 18524 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 000000000..95d6289d9 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-03-segmentation-1427.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4a1ee18d804b4f83bf5ef7ff1d3d849a136a7480dd074c721c41e788b51868c +size 32982 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 000000000..44257b641 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-03-segmentation-1432.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa1d8b9426f595db1c92733470c60bddfcf021cb95a6c27da829598180e0a6d0 +size 20094 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 000000000..281b63983 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-03-segmentation-1435.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb8129ea589ab5cab6368be88861125bcc55a492ba2eb20c086aa99c7c9410d2 +size 12080 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 000000000..39c8b7191 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-03-segmentation-1436.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78a5fd9afa2edb44d73235a0d1a160abf34efd8ee9495121d3405aa89a8f8b63 +size 14512 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 000000000..0c094e8c4 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-03-segmentation-1437.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:299ac1152847f4bded20bdd114c9f1f5a12ceee767a924cb347db7508d784375 +size 27132 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 000000000..d13f619af --- /dev/null +++ b/tests/Images/Input/Webp/vp80-03-segmentation-1441.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70d3bc371f42f3a7d8f59eb89c66a3d1ef10476baa86e66487f158370403b595 +size 4606 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 000000000..047bf1572 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-03-segmentation-1442.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ef18fbc941074c644d01db06fcf06b9e29628e6bedf23db29c239aa1795cc9b +size 14804 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 000000000..2d29d86fd --- /dev/null +++ b/tests/Images/Input/Webp/vp80-04-partitions-1404.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25cd4540f189f61ab0119f8f26e3dc28ba1a7840843b205389948dc3019eee6d +size 15298 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 000000000..f8704e166 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-04-partitions-1405.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d2daf0d7c7e902208621342450bd4009a7bfe3b6aaf36b7d43d232066cd9037 +size 15308 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 000000000..dcf7a73a5 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-04-partitions-1406.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af294ab9f4de0ca82f9df0a29d60f00b1bc20099d337ffaac63e6e1e5c4a14e6 +size 15324 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 000000000..727ec0e10 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-05-sharpness-1428.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a13d9081c8d55dacd6819704712a64a7d25971e59c0ba7e5e5ae4c86ca522b7b +size 8864 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 000000000..d1f36de81 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-05-sharpness-1429.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99e3f0a30900c65f5af22e41bc60c4fc7209e2c8f93d2edf5d5ff09db6beb900 +size 14518 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 000000000..01399b5e2 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-05-sharpness-1430.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acd9c9fb1876fd2035405b9b72aa5a985922ec1aab2055f6c32a21e02fdd9dbd +size 290 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 000000000..b924e43c4 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-05-sharpness-1431.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9aa0b809dc9aac340acab0f7f4953f497e4b6cefc9dda14f823ab3053a11d5cd +size 6666 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 000000000..340c4a448 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-05-sharpness-1433.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:005811b6b16550a1da22a1df767311c1b85f1cc7c2409d7917fd594d0b48d4c1 +size 14508 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 000000000..c06ea3fb9 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-05-sharpness-1434.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da5bef25e427bb9a8be2889c76a65f9506cdfc4bab455b3285b6b627e5880285 +size 18224 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 000000000..618f5e358 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-05-sharpness-1438.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd0ea301c7446b6fd6d002b9ab48b383501ce05c3953d589be48ede5cf293f9d +size 9981 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 000000000..e3ac596a2 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-05-sharpness-1439.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36bf4fef87be45f49411f93102433f117d54356a7aebd294ae1b68799938ce1c +size 20068 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 000000000..809a2fd9d --- /dev/null +++ b/tests/Images/Input/Webp/vp80-05-sharpness-1440.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f66a1bc109f04baa07a530fb79267d931899591c10798c4dc95f59eb03c5ac44 +size 14508 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 000000000..851dfb6b6 --- /dev/null +++ b/tests/Images/Input/Webp/vp80-05-sharpness-1443.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bcedf4d253801cf2461bd01675558dadc3895395a6432d0ba8f5cb9734b4040c +size 6188 diff --git a/tests/Images/Input/Webp/yuv_test.png b/tests/Images/Input/Webp/yuv_test.png new file mode 100644 index 000000000..5606b783e --- /dev/null +++ b/tests/Images/Input/Webp/yuv_test.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96b86c39cad831c97c6ef9633d4d2d04ea0382547514dda5b1f639e10d7207fa +size 3389