From 3d280757efdd85fb294a9f04ebb5f0da5f5ca606 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 6 Dec 2020 16:44:46 +0100 Subject: [PATCH] Add SaveAsWebP methods and tests --- .../Formats/ImageExtensions.Save.cs | 107 +++++++++++- .../Formats/ImageExtensions.Save.tt | 28 +-- .../Formats/WebP/Lossless/Vp8LEncoder.cs | 6 +- .../WebP/Lossless/WebPLosslessDecoder.cs | 10 +- .../Formats/WebP/ImageExtensionsTest.cs | 164 ++++++++++++++++++ .../Formats/WebP/WebPDecoderTests.cs | 1 + .../Formats/WebP/WebPEncoderTests.cs | 1 + .../Formats/WebP/WebPMetaDataTests.cs | 1 + 8 files changed, 301 insertions(+), 17 deletions(-) create mode 100644 tests/ImageSharp.Tests/Formats/WebP/ImageExtensionsTest.cs diff --git a/src/ImageSharp/Formats/ImageExtensions.Save.cs b/src/ImageSharp/Formats/ImageExtensions.Save.cs index 075c708b6..f7900ec73 100644 --- a/src/ImageSharp/Formats/ImageExtensions.Save.cs +++ b/src/ImageSharp/Formats/ImageExtensions.Save.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. // @@ -6,6 +6,8 @@ using System.IO; using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; +// using SixLabors.ImageSharp.Formats.Experimental.Tiff; +using SixLabors.ImageSharp.Formats.Experimental.WebP; using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Gif; @@ -535,5 +537,108 @@ namespace SixLabors.ImageSharp encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance), cancellationToken); + /// + /// EXPERIMENTAL! 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); + + /// + /// EXPERIMENTAL! 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); + + /// + /// EXPERIMENTAL! 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); + + /// + /// EXPERIMENTAL! 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)); + + /// + /// EXPERIMENTAL! 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); + + /// + /// EXPERIMENTAL! 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); + + /// + /// EXPERIMENTAL! 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); + + /// + /// EXPERIMENTAL! 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)); + + /// + /// EXPERIMENTAL! 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); + } } diff --git a/src/ImageSharp/Formats/ImageExtensions.Save.tt b/src/ImageSharp/Formats/ImageExtensions.Save.tt index 63b404cc4..c8979da0d 100644 --- a/src/ImageSharp/Formats/ImageExtensions.Save.tt +++ b/src/ImageSharp/Formats/ImageExtensions.Save.tt @@ -1,4 +1,4 @@ -<#@ template language="C#" #> +<#@ template language="C#" #> <#@ import namespace="System.Text" #> <#@ import namespace="System.Collections.Generic" #> // Copyright (c) Six Labors. @@ -9,6 +9,8 @@ using System.IO; using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; +// using SixLabors.ImageSharp.Formats.Experimental.Tiff; +using SixLabors.ImageSharp.Formats.Experimental.WebP; <# var formats = new []{ @@ -17,10 +19,15 @@ using SixLabors.ImageSharp.Advanced; "Jpeg", "Png", "Tga", + "WebP" }; foreach (string fmt in formats) { + if (fmt == "Tiff" || fmt == "WebP") + { + continue; + } #> using SixLabors.ImageSharp.Formats.<#= fmt #>; <# @@ -38,9 +45,10 @@ namespace SixLabors.ImageSharp <# foreach (string fmt in formats) { + string experimentalString = fmt == "Tiff" || fmt == "WebP" ? @"EXPERIMENTAL! " : ""; #> /// - /// Saves the image to the given stream with the <#= fmt #> format. + /// <#= experimentalString #>Saves the image to the given stream with the <#= fmt #> format. /// /// The image this method extends. /// The file path to save the image to. @@ -48,7 +56,7 @@ namespace SixLabors.ImageSharp public static void SaveAs<#= fmt #>(this Image source, string path) => SaveAs<#= fmt #>(source, path, null); /// - /// Saves the image to the given stream with the <#= fmt #> format. + /// <#= experimentalString #>Saves the image to the given stream with the <#= fmt #> format. /// /// The image this method extends. /// The file path to save the image to. @@ -57,7 +65,7 @@ namespace SixLabors.ImageSharp public static Task SaveAs<#= fmt #>Async(this Image source, string path) => SaveAs<#= fmt #>Async(source, path, null); /// - /// Saves the image to the given stream with the <#= fmt #> format. + /// <#= experimentalString #>Saves the image to the given stream with the <#= fmt #> format. /// /// The image this method extends. /// The file path to save the image to. @@ -68,7 +76,7 @@ namespace SixLabors.ImageSharp => SaveAs<#= fmt #>Async(source, path, null, cancellationToken); /// - /// Saves the image to the given stream with the <#= fmt #> format. + /// <#= experimentalString #>Saves the image to the given stream with the <#= fmt #> format. /// /// The image this method extends. /// The file path to save the image to. @@ -80,7 +88,7 @@ namespace SixLabors.ImageSharp encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(<#= fmt #>Format.Instance)); /// - /// Saves the image to the given stream with the <#= fmt #> format. + /// <#= experimentalString #>Saves the image to the given stream with the <#= fmt #> format. /// /// The image this method extends. /// The file path to save the image to. @@ -95,7 +103,7 @@ namespace SixLabors.ImageSharp cancellationToken); /// - /// Saves the image to the given stream with the <#= fmt #> format. + /// <#= experimentalString #>Saves the image to the given stream with the <#= fmt #> format. /// /// The image this method extends. /// The stream to save the image to. @@ -104,7 +112,7 @@ namespace SixLabors.ImageSharp => SaveAs<#= fmt #>(source, stream, null); /// - /// Saves the image to the given stream with the <#= fmt #> format. + /// <#= experimentalString #>Saves the image to the given stream with the <#= fmt #> format. /// /// The image this method extends. /// The stream to save the image to. @@ -115,7 +123,7 @@ namespace SixLabors.ImageSharp => SaveAs<#= fmt #>Async(source, stream, null, cancellationToken); /// - /// Saves the image to the given stream with the <#= fmt #> format. + /// <#= experimentalString #>Saves the image to the given stream with the <#= fmt #> format. /// /// The image this method extends. /// The stream to save the image to. @@ -128,7 +136,7 @@ namespace SixLabors.ImageSharp encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(<#= fmt #>Format.Instance)); /// - /// Saves the image to the given stream with the <#= fmt #> format. + /// <#= experimentalString #>Saves the image to the given stream with the <#= fmt #> format. /// /// The image this method extends. /// The stream to save the image to. diff --git a/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs index 3fac4eb45..9782d8ab7 100644 --- a/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs @@ -423,6 +423,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.WebP.Lossless 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( @@ -525,11 +526,13 @@ namespace SixLabors.ImageSharp.Formats.Experimental.WebP.Lossless this.StoreImageToBitMask(width, histogramBits, refsBest, histogramSymbols, huffmanCodes); // Keep track of the smallest image so far. - if (bitWriterBest != null && this.bitWriter.NumBytes() < bitWriterBest.NumBytes()) + if (isFirstIteration || (bitWriterBest != null && this.bitWriter.NumBytes() < bitWriterBest.NumBytes())) { // TODO: This was done in the reference by swapping references, this will be slower bitWriterBest = this.bitWriter.Clone(); } + + isFirstIteration = false; } this.bitWriter = bitWriterBest; @@ -1156,6 +1159,7 @@ namespace SixLabors.ImageSharp.Formats.Experimental.WebP.Lossless xBits = (paletteSize <= 16) ? 1 : 0; } + this.CurrentWidth = LosslessUtils.SubSampleSize(width, xBits); this.ApplyPalette(src, srcStride, dst, this.CurrentWidth, palette, paletteSize, width, height, xBits); } diff --git a/src/ImageSharp/Formats/WebP/Lossless/WebPLosslessDecoder.cs b/src/ImageSharp/Formats/WebP/Lossless/WebPLosslessDecoder.cs index 0ff3cccbc..8a6c36595 100644 --- a/src/ImageSharp/Formats/WebP/Lossless/WebPLosslessDecoder.cs +++ b/src/ImageSharp/Formats/WebP/Lossless/WebPLosslessDecoder.cs @@ -196,15 +196,15 @@ namespace SixLabors.ImageSharp.Formats.Experimental.WebP.Lossless ApplyInverseTransforms(decoder, pixelData, this.memoryAllocator); Span pixelDataAsBytes = MemoryMarshal.Cast(pixelData); - int widthMul4 = width * 4; + int bytesPerRow = width * 4; for (int y = 0; y < decoder.Height; y++) { - Span row = pixelDataAsBytes.Slice(y * widthMul4, widthMul4); - Span pixelSpan = pixels.GetRowSpan(y); + Span rowAsBytes = pixelDataAsBytes.Slice(y * bytesPerRow, bytesPerRow); + Span pixelRow = pixels.GetRowSpan(y); PixelOperations.Instance.FromBgra32Bytes( this.configuration, - row, - pixelSpan, + rowAsBytes.Slice(0, bytesPerRow), + pixelRow.Slice(0, width), width); } } diff --git a/tests/ImageSharp.Tests/Formats/WebP/ImageExtensionsTest.cs b/tests/ImageSharp.Tests/Formats/WebP/ImageExtensionsTest.cs new file mode 100644 index 000000000..b20376b47 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/WebP/ImageExtensionsTest.cs @@ -0,0 +1,164 @@ +// 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.Experimental.WebP; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Webp +{ + [Trait("Format", "Webp")] + public class ImageExtensionsTest + { + public ImageExtensionsTest() + { + Configuration.Default.ImageFormatsManager.AddImageFormat(WebPFormat.Instance); + Configuration.Default.ImageFormatsManager.AddImageFormatDetector(new WebPImageFormatDetector()); + Configuration.Default.ImageFormatsManager.SetDecoder(WebPFormat.Instance, new WebPDecoder()); + Configuration.Default.ImageFormatsManager.SetEncoder(WebPFormat.Instance, new WebPEncoder()); + } + + [Fact] + public void SaveAsWebp_Path() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); + 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(ImageExtensionsTest)); + 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/WebPDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebPDecoderTests.cs index 938ba2e7d..562befa21 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebPDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebPDecoderTests.cs @@ -14,6 +14,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.WebP { using static SixLabors.ImageSharp.Tests.TestImages.WebP; + [Trait("Format", "Webp")] public class WebPDecoderTests { private static WebPDecoder WebpDecoder => new WebPDecoder(); diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebPEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebPEncoderTests.cs index 3e4b0799b..8143503a1 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebPEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebPEncoderTests.cs @@ -10,6 +10,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.WebP { using static TestImages.WebP; + [Trait("Format", "Webp")] public class WebPEncoderTests { [Theory] diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebPMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebPMetaDataTests.cs index 4eff21572..375770117 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebPMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebPMetaDataTests.cs @@ -8,6 +8,7 @@ using Xunit; // ReSharper disable InconsistentNaming namespace SixLabors.ImageSharp.Tests.Formats.WebP { + [Trait("Format", "Webp")] public class WebPMetadataTests { [Theory]