diff --git a/Packages.props b/Packages.props index 0a5f2c6e..e726840d 100644 --- a/Packages.props +++ b/Packages.props @@ -7,14 +7,20 @@ - - - - + + + + + + + + + - + + diff --git a/src/Mobile.BuildTools/Drawing/ColorUtils.cs b/src/Mobile.BuildTools/Drawing/ColorUtils.cs index f732d872..958d0450 100644 --- a/src/Mobile.BuildTools/Drawing/ColorUtils.cs +++ b/src/Mobile.BuildTools/Drawing/ColorUtils.cs @@ -1,21 +1,21 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; +using SkiaSharp; namespace Mobile.BuildTools.Drawing { internal static class ColorUtils { - private static readonly Dictionary namedColors = typeof(Color).GetFields(BindingFlags.Public | BindingFlags.Static) - .Where(x => x.FieldType == typeof(Color)) - .ToDictionary(x => x.Name, x => (Color)x.GetValue(null)); + private static readonly Dictionary namedColors = typeof(SKColors).GetFields(BindingFlags.Public | BindingFlags.Static) + .Where(x => x.FieldType == typeof(SKColor)) + .ToDictionary(x => x.Name, x => (SKColor)x.GetValue(null), StringComparer.OrdinalIgnoreCase); - public static bool TryParse(string input, out Color result) + public static bool TryParse(string input, out SKColor result) { result = default; - + if (string.IsNullOrWhiteSpace(input)) { return false; @@ -26,15 +26,7 @@ public static bool TryParse(string input, out Color result) return true; } - try - { - result = Rgba32.ParseHex(input); - return true; - } - catch - { - return false; - } + return SKColor.TryParse(input, out result); } } } diff --git a/src/Mobile.BuildTools/Drawing/Context.cs b/src/Mobile.BuildTools/Drawing/Context.cs new file mode 100644 index 00000000..e7eed8ff --- /dev/null +++ b/src/Mobile.BuildTools/Drawing/Context.cs @@ -0,0 +1,36 @@ +using System.Drawing; +using Mobile.BuildTools.Logging; +using SkiaSharp; + +namespace Mobile.BuildTools.Drawing +{ + internal class Context + { + public SKColor BackgroundColor { get; } + + public ILog Log { get; } + + public double Opacity { get; } = 1.0; + + public PointF Scale { get; } + + public Size Size { get; } + + public Context(SKColor backgroundColor, ILog log, double opacity, int width, int height, float uniformScale) : this(backgroundColor, log, opacity, width, height, uniformScale, uniformScale) + { + } + + public Context(SKColor backgroundColor, ILog log, double opacity, int width, int height, float xScale, float yScale) : this(backgroundColor, log, opacity, new Size(width, height), new PointF(xScale, yScale)) + { + } + + private Context(SKColor backgroundColor, ILog log, double opacity, Size size, PointF scale) + { + BackgroundColor = backgroundColor; + Opacity = opacity; + Log = log; + Size = size; + Scale = scale; + } + } +} diff --git a/src/Mobile.BuildTools/Drawing/FloatExtensions.cs b/src/Mobile.BuildTools/Drawing/FloatExtensions.cs new file mode 100644 index 00000000..568f7c3a --- /dev/null +++ b/src/Mobile.BuildTools/Drawing/FloatExtensions.cs @@ -0,0 +1,9 @@ +using System; + +namespace Mobile.BuildTools.Drawing +{ + public static class FloatExtensions + { + public static bool IsEqualTo(this float value, float comparisonValue) => Math.Abs(value - comparisonValue) <= 0.0001; + } +} diff --git a/src/Mobile.BuildTools/Drawing/IconUtils.cs b/src/Mobile.BuildTools/Drawing/IconUtils.cs deleted file mode 100644 index 74791c0b..00000000 --- a/src/Mobile.BuildTools/Drawing/IconUtils.cs +++ /dev/null @@ -1,47 +0,0 @@ -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace Mobile.BuildTools.Drawing -{ - public static class IconUtils - { - public static bool HasTransparentBackground(this Image image) - { - using var clone = image.CloneAs(); - for (var y = 0; y < image.Height; ++y) - { - var pixelRowSpan = clone.GetPixelRowSpan(y); - for (var x = 0; x < image.Width; ++x) - { - if (pixelRowSpan[x].A == 0) - { - return true; - } - } - } - - return false; - } - - public static void ApplyBackground(this IImageProcessingContext context, string hexColor) - { - var color = ColorUtils.TryParse(hexColor, out var x) ? x : Color.ParseHex(Constants.DefaultBackgroundColor); - - context.BackgroundColor(color); - } - - public static void ResizeCanvas(this IImageProcessingContext context, double? pad, string padColorNameOrHexString) - { - if (pad is null || pad.Value < 1) return; - - var scale = (double)pad; - var color = ColorUtils.TryParse(padColorNameOrHexString, out var result) ? result : Color.Transparent; - - var size = context.GetCurrentSize(); - var height = (int)(size.Height * scale); - var width = (int)(size.Width * scale); - context.Pad(width, height, color); - } - } -} diff --git a/src/Mobile.BuildTools/Drawing/Image.cs b/src/Mobile.BuildTools/Drawing/Image.cs new file mode 100644 index 00000000..1effb74b --- /dev/null +++ b/src/Mobile.BuildTools/Drawing/Image.cs @@ -0,0 +1,62 @@ +using System.Drawing; +using SkiaSharp; + +namespace Mobile.BuildTools.Drawing +{ + internal class Image : ImageBase + { + private SKBitmap bitmap; + + public Image(string filename) : base(filename) + { + bitmap = SKBitmap.Decode(filename); + } + + public override bool HasTransparentBackground + { + get + { + var imageWidth = GetOriginalSize().Width; + var imageHeight = GetOriginalSize().Height; + + for (var x = 0; x < imageWidth; x++) + { + for (var y = 0; y < imageHeight; y++) + { + if (bitmap.GetPixel(x, y).Alpha == 0) + { + return true; + } + } + } + + return false; + } + } + + public override Size GetOriginalSize() => + new Size(bitmap.Info.Width, bitmap.Info.Height); + + public override void Draw(SKCanvas canvas, Context context) + { + SKPaint paint = null; + if (context.Opacity != 1d) + { + paint = new SKPaint + { + Color = SKColors.Transparent.WithAlpha((byte)(0xFF * context.Opacity)) + }; + } + + canvas.DrawBitmap(bitmap, 0, 0, paint); + + paint?.Dispose(); + } + + public override void Dispose() + { + bitmap?.Dispose(); + bitmap = null; + } + } +} diff --git a/src/Mobile.BuildTools/Drawing/ImageBase.cs b/src/Mobile.BuildTools/Drawing/ImageBase.cs new file mode 100644 index 00000000..1a1bd6df --- /dev/null +++ b/src/Mobile.BuildTools/Drawing/ImageBase.cs @@ -0,0 +1,40 @@ +using System; +using System.Drawing; +using System.IO; +using SkiaSharp; + +namespace Mobile.BuildTools.Drawing +{ + internal abstract class ImageBase : IDisposable + { + public ImageBase(string filename) + { + Filename = filename; + } + + public string Filename { get; } + + public abstract bool HasTransparentBackground { get; } + + public int Height => GetOriginalSize().Height; + + public int Width => GetOriginalSize().Width; + + public abstract Size GetOriginalSize(); + + public abstract void Draw(SKCanvas canvas, Context context); + + public static ImageBase Load(string filename) + => IsVector(filename) + ? new VectorImage(filename) + : new Image(filename); + + private static bool IsVector(string filename) => + Path.GetExtension(filename)?.Equals(".svg", StringComparison.OrdinalIgnoreCase) ?? false; + + public virtual void Dispose() + { + + } + } +} diff --git a/src/Mobile.BuildTools/Drawing/VectorImage.cs b/src/Mobile.BuildTools/Drawing/VectorImage.cs new file mode 100644 index 00000000..c1d7c402 --- /dev/null +++ b/src/Mobile.BuildTools/Drawing/VectorImage.cs @@ -0,0 +1,75 @@ +using System.Drawing; +using SkiaSharp; +using Svg.Skia; + +namespace Mobile.BuildTools.Drawing +{ + internal class VectorImage : ImageBase + { + private SKSvg svg; + + public VectorImage(string filename) : base(filename) + { + svg = new SKSvg(); + svg.Load(filename); + } + + public override bool HasTransparentBackground => false; + + public override Size GetOriginalSize() => + new Size((int)svg.Picture.CullRect.Size.Width, (int)svg.Picture.CullRect.Size.Height); + + public override void Draw(SKCanvas canvas, Context context) + { + SKPaint opacityPaint = null; + if (context.Opacity != 1d) + { + opacityPaint = new SKPaint + { + Color = SKColors.Transparent.WithAlpha((byte)(0xFF * context.Opacity)) + }; + } + + if (context.Scale.X >= 1) + { + // draw using default scaling + canvas.DrawPicture(svg.Picture, opacityPaint); + } + else + { + // draw using raster downscaling + var size = GetOriginalSize(); + + // TODO: Try and apply a sensible default of 1024x1024 if no sizing information exists. + // Log warning out if reverting to defaults. + + // vector scaling has rounding issues, so first draw as intended + var info = new SKImageInfo(size.Width, size.Height); + using var bmp = new SKBitmap(info); + using var cvn = new SKCanvas(bmp); + + // draw to a larger canvas first + cvn.Clear(context.BackgroundColor); + cvn.DrawPicture(svg.Picture, opacityPaint); + + // set the paint to be the highest quality it can find + var paint = new SKPaint + { + IsAntialias = true, + FilterQuality = SKFilterQuality.High + }; + + // draw to the main canvas using the correct quality settings + canvas.DrawBitmap(bmp, 0, 0, paint); + } + + opacityPaint?.Dispose(); + } + + public override void Dispose() + { + svg?.Dispose(); + svg = null; + } + } +} diff --git a/src/Mobile.BuildTools/Drawing/Watermark.cs b/src/Mobile.BuildTools/Drawing/Watermark.cs index 68b1c35e..51a4f440 100644 --- a/src/Mobile.BuildTools/Drawing/Watermark.cs +++ b/src/Mobile.BuildTools/Drawing/Watermark.cs @@ -1,166 +1,34 @@ -using System; -using System.IO; -using System.Linq; -using System.Reflection; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; +using System.Drawing; using Mobile.BuildTools.Models.AppIcons; -using SixLabors.Fonts; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing; +using SkiaSharp; namespace Mobile.BuildTools.Drawing -{ - public static class Watermark - { - public static IImageProcessingContext ApplyWatermark(this IImageProcessingContext processingContext, WatermarkConfiguration config) - { - if (config is null) - { - return processingContext; - } - else if (!string.IsNullOrEmpty(config.SourceFile)) - { - return processingContext.ApplyWatermark(config.SourceFile, config.Opacity); - } - - var settings = WatermarkSettings.FromConfig(config); - var size = processingContext.GetCurrentSize(); - // Create a new image for the watermark layer. - using var watermark = new Image(size.Width, size.Height); - - var xOffeset = 0; - var yOffeset = 0; - - watermark.Mutate(ctx => - { - // Draw the watermark. - ctx.DrawWatermark(settings); - - var angle = 0.0f; - if ((settings.Position == WatermarkPosition.BottomLeft) || (settings.Position == WatermarkPosition.TopRight)) - { - angle = 45.0f; - // Calculate the x/y offsets for later when we draw the watermark on top of the source image. - xOffeset = -(int)((Math.Sin(angle) * (size.Width / 2)) / 2); - yOffeset = -(int)((Math.Sin(angle) * (size.Height / 2)) / 2); - } - else if ((settings.Position == WatermarkPosition.BottomRight) || (settings.Position == WatermarkPosition.TopLeft)) - { - angle = -45.0f; - // Calculate the x/y offsets for later when we draw the watermark on top of the source image. - xOffeset = (int)((Math.Sin(angle) * (size.Width / 2)) / 2); - yOffeset = (int)((Math.Sin(angle) * (size.Height / 2)) / 2); - } - if (angle != 0) - { - ctx.Rotate(angle); - } - - }); - - // Draw the watermark layer on top of the source image. - processingContext.DrawImage(watermark, new Point(xOffeset, yOffeset), 1); - - return processingContext; - } - - public static IImageProcessingContext ApplyWatermark(this IImageProcessingContext processingContext, string watermarkFile, double? opacity) - { - if (File.Exists(watermarkFile)) - { - using var watermarkImage = Image.Load(watermarkFile); - var size = processingContext.GetCurrentSize(); - watermarkImage.Mutate(x => x.Resize(size)); - processingContext.DrawImage(watermarkImage, PixelColorBlendingMode.Normal, (float)(opacity ?? 0.95)); - } - - return processingContext; - } - // Colors: Red,Blue,Purple,#006666 - private static IImageProcessingContext DrawWatermark(this IImageProcessingContext context, WatermarkSettings settings) - { - var imgSize = context.GetCurrentSize(); - - // measure the text size - var size = TextMeasurer.Measure(settings.Text, new RendererOptions(settings.TextFont)); - - //find out how much we need to scale the text to fill the space (up or down) - var scalingFactor = Math.Min(imgSize.Width / size.Width, imgSize.Height / size.Height) / 3; - - //create a new settings.TextFont - var scaledFont = new Font(settings.TextFont, scalingFactor * settings.TextFont.Size); - var center = settings.Position switch - { - WatermarkPosition.Top => new PointF(imgSize.Width / 2, (imgSize.Height / 9)), - WatermarkPosition.TopLeft => new PointF(imgSize.Width / 2, (imgSize.Height / 9)), - WatermarkPosition.TopRight => new PointF(imgSize.Width / 2, (imgSize.Height / 9)), - _ => new PointF(imgSize.Width / 2, imgSize.Height - (imgSize.Height / 9)), +{ + internal static class Watermark + { + public static ImageBase Create(WatermarkConfiguration configuration, PointF originalScale) + => configuration switch + { + null => new EmptyWatermark(), + _ => !string.IsNullOrEmpty(configuration.SourceFile) + ? new WatermarkImage(configuration.SourceFile) + : new WatermarkTextBanner(configuration, originalScale) }; + } - var textGraphicOptions = new TextGraphicsOptions - { - GraphicsOptions = new GraphicsOptions - { - Antialias = true - }, - TextOptions = new TextOptions - { - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - } - }; - - // Apply Banner - context.DrawBanner(settings) - .DrawText(textGraphicOptions, settings.Text, scaledFont, settings.TextColor, center); - - return context; - } - - private static IImageProcessingContext DrawBanner(this IImageProcessingContext context, WatermarkSettings settings) - { - var imgSize = context.GetCurrentSize(); - var options = new ShapeGraphicsOptions - { - GraphicsOptions = new GraphicsOptions - { - Antialias = true, - ColorBlendingMode = PixelColorBlendingMode.Normal, - BlendPercentage = 1, - AlphaCompositionMode = PixelAlphaCompositionMode.SrcOver - }, - ShapeOptions = new ShapeOptions - { - IntersectionRule = IntersectionRule.Nonzero - } - }; - - var points = new[] { new PointF(0, imgSize.Height), new PointF(imgSize.Width, imgSize.Height) }; - - var stop = 0; - - IBrush brush = new LinearGradientBrush( - points[0], - points[1], - GradientRepetitionMode.Repeat, - settings.Colors.Select(x => new ColorStop(stop++, x)).ToArray()); - - var thickness = imgSize.Height / 4.5; - - var center = imgSize.Height - thickness; - - if ((settings.Position == WatermarkPosition.Top) || (settings.Position == WatermarkPosition.TopLeft) || (settings.Position == WatermarkPosition.TopRight)) - { - points = new[] { new PointF(0, 0), new PointF(imgSize.Width, 0) }; - } - - var fullThickness = (float)(thickness * 2); - var pen = new Pen(brush, fullThickness); - var polygon = new Polygon(new LinearLineSegment(points)); + internal class EmptyWatermark : ImageBase + { + public EmptyWatermark() : base(string.Empty) + { + } + + public override bool HasTransparentBackground => false; + + public override void Draw(SKCanvas canvas, Context context) + { + + } - return context.Draw(options, pen, polygon); - } - } + public override Size GetOriginalSize() => Size.Empty; + } } diff --git a/src/Mobile.BuildTools/Drawing/WatermarkImage.cs b/src/Mobile.BuildTools/Drawing/WatermarkImage.cs new file mode 100644 index 00000000..33407c1a --- /dev/null +++ b/src/Mobile.BuildTools/Drawing/WatermarkImage.cs @@ -0,0 +1,33 @@ +using System.Drawing; +using SkiaSharp; + +namespace Mobile.BuildTools.Drawing +{ + internal class WatermarkImage : ImageBase + { + private readonly ImageBase sourceImage; + + public WatermarkImage(string filename) : base(filename) + { + sourceImage = ImageBase.Load(filename); + } + + public override bool HasTransparentBackground => sourceImage.HasTransparentBackground; + + public override void Draw(SKCanvas canvas, Context context) + { + if (!context.Scale.X.IsEqualTo(1f) || + !context.Scale.Y.IsEqualTo(1f)) + { + context.Log.LogWarning("Watermark image has been scaled to meet output dimensions."); + } + + canvas.Scale(context.Scale.X, context.Scale.Y); + sourceImage.Draw(canvas, context); + } + + public override Size GetOriginalSize() => sourceImage?.GetOriginalSize() ?? Size.Empty; + + public override void Dispose() => sourceImage?.Dispose(); + } +} diff --git a/src/Mobile.BuildTools/Drawing/WatermarkSettings.cs b/src/Mobile.BuildTools/Drawing/WatermarkSettings.cs index 66772e89..b15cb001 100644 --- a/src/Mobile.BuildTools/Drawing/WatermarkSettings.cs +++ b/src/Mobile.BuildTools/Drawing/WatermarkSettings.cs @@ -1,20 +1,19 @@ using System; using System.Collections.Generic; using System.Linq; -using SixLabors.ImageSharp; using Mobile.BuildTools.Models.AppIcons; -using SixLabors.Fonts; - +using SkiaSharp; + namespace Mobile.BuildTools.Drawing { public class WatermarkSettings { - public IEnumerable Colors { get; set; } + public IEnumerable Colors { get; set; } public WatermarkPosition Position { get; set; } public string Text { get; set; } - public Color TextColor { get; set; } - public Font TextFont { get; set; } + public SKColor TextColor { get; set; } + public SKTypeface Typeface { get; set; } public static WatermarkSettings FromConfig(WatermarkConfiguration config) { @@ -24,39 +23,36 @@ public static WatermarkSettings FromConfig(WatermarkConfiguration config) Text = config.Text }; - if(config.Colors is null || config.Colors.Count() == 0) + if (config.Colors is null || config.Colors.Count() == 0) { - settings.Colors = new[] { Color.Red, Color.Purple }; + settings.Colors = new[] { SKColors.Red, SKColors.Purple }; } else { settings.Colors = config.Colors.Select(x => ColorUtils.TryParse(x, out var color) ? color : throw new Exception($"Cannot parse the color with value '{x}'.")); } - if(string.IsNullOrEmpty(config.TextColor)) + if (string.IsNullOrEmpty(config.TextColor)) { - settings.TextColor = Color.White; + settings.TextColor = SKColors.White; } else { settings.TextColor = ColorUtils.TryParse(config.TextColor, out var color) ? color : throw new Exception($"Cannot parse the text color with value '{config.TextColor}'."); } - if(!string.IsNullOrEmpty(config.FontFile)) + if (!string.IsNullOrEmpty(config.FontFile)) { - // TODO: Support remote http and local files - var localFile = config.FontFile; - var fontCollection = new FontCollection(); - FontFamily fontFamily = fontCollection.Install(localFile); - settings.TextFont = new Font(fontFamily, 10); + // TODO: Support remote http and local files + settings.Typeface = SKTypeface.FromFile(config.FontFile); } - else if(!string.IsNullOrEmpty(config.FontFamily)) - { - settings.TextFont = SystemFonts.CreateFont(config.FontFamily, 10); + else if (!string.IsNullOrEmpty(config.FontFamily)) + { + settings.Typeface = SKTypeface.FromFamilyName(config.FontFamily); } else - { - settings.TextFont = SystemFonts.CreateFont("Arial", 10); + { + settings.Typeface = SKTypeface.Default; } return settings; diff --git a/src/Mobile.BuildTools/Drawing/WatermarkTextBanner.cs b/src/Mobile.BuildTools/Drawing/WatermarkTextBanner.cs new file mode 100644 index 00000000..6f1c3619 --- /dev/null +++ b/src/Mobile.BuildTools/Drawing/WatermarkTextBanner.cs @@ -0,0 +1,105 @@ +using System.Drawing; +using System.Linq; +using Mobile.BuildTools.Models.AppIcons; +using SkiaSharp; + +namespace Mobile.BuildTools.Drawing +{ + internal class WatermarkTextBanner : ImageBase + { + private readonly string[] commonDescenders = new[] { "g", "j", "p", "q", "y" }; + private readonly WatermarkConfiguration configuration; + private readonly PointF originalScale; + + public WatermarkTextBanner(WatermarkConfiguration configuration, PointF originalScale) : base(string.Empty) + { + this.configuration = configuration; + this.originalScale = originalScale; + } + + public override bool HasTransparentBackground => true; + + public override void Draw(SKCanvas canvas, Context context) + { + DrawTextBanner(canvas, context); + } + + public override Size GetOriginalSize() => Size.Empty; + + private void DrawTextBanner(SKCanvas canvas, Context context) + { + // Undo the original scaling factor to simplify the rendering. + canvas.Scale(originalScale.X, originalScale.Y); + + var settings = WatermarkSettings.FromConfig(configuration); + var bannerHeight = (float)(context.Size.Width / 4.5); + var (start, end) = GetBannerLocations(settings.Position, context.Size, (float)bannerHeight); + + var shader = SKShader.CreateLinearGradient( + start, + end, + settings.Colors.Select(c => c.WithAlpha((byte)(0xFF * context.Opacity))).ToArray(), + null, + SKShaderTileMode.Clamp); + using var linePaint = new SKPaint + { + Shader = shader, + IsStroke = true, + StrokeWidth = bannerHeight, + Style = SKPaintStyle.Stroke + }; + var path = new SKPath(); + path.MoveTo(start); + path.LineTo(end); + + canvas.DrawPath(path, linePaint); + using var textPaint = new SKPaint + { + Color = settings.TextColor.WithAlpha((byte)(0xFF * context.Opacity)), + TextAlign = SKTextAlign.Center, + Typeface = settings.Typeface + }; + + // Adjust TextSize property so text is 45% of the resulting image size. + var textWidth = textPaint.MeasureText(settings.Text); + textPaint.TextSize = 0.45f * context.Size.Width * textPaint.TextSize / textWidth; + + // Find the text bounds + var textBounds = new SKRect(); + // It may well be a Skia bug but it appears the measuring doesn't include the height of the descender characters in the measured height. + textPaint.MeasureText(StripDescenders(settings.Text), ref textBounds); + + // Text drawn on the centre of the line so we need to bump it down to align the centre of the text. + canvas.DrawTextOnPath(settings.Text, path, 0, textBounds.Height / 2, textPaint); + } + + private string StripDescenders(string text) + { + // TODO: Surely there must be a better way? + var cleansedText = text; + foreach (var descender in commonDescenders) + { + cleansedText = cleansedText.Replace(descender, ""); + } + + if (cleansedText.Length == 0) + { + cleansedText = "dev"; + } + + return cleansedText; + } + + private static (SKPoint, SKPoint) GetBannerLocations(WatermarkPosition watermarkPosition, Size size, float bannerHeight) + => watermarkPosition switch + { + WatermarkPosition.Top => (new SKPoint(0, 0), new SKPoint(size.Width, 0)), + WatermarkPosition.Bottom => (new SKPoint(0, size.Height), new SKPoint(size.Width, size.Height)), + WatermarkPosition.TopLeft => (new SKPoint(-bannerHeight, size.Height / 2 + bannerHeight), new SKPoint(size.Width / 2 + bannerHeight, -bannerHeight)), + WatermarkPosition.TopRight => (new SKPoint(size.Width / 2 - bannerHeight, -bannerHeight), new SKPoint(size.Width + bannerHeight, size.Height / 2 + bannerHeight)), + WatermarkPosition.BottomLeft => (new SKPoint(-bannerHeight, size.Height / 2 - bannerHeight), new SKPoint(size.Width / 2 + bannerHeight, size.Height + bannerHeight)), + WatermarkPosition.BottomRight => (new SKPoint(size.Width / 2 - bannerHeight, size.Height + bannerHeight), new SKPoint(size.Width + bannerHeight, size.Height / 2 - bannerHeight)), + _ => (new SKPoint(0, 0), new SKPoint(0, 0)), + }; + } +} diff --git a/src/Mobile.BuildTools/Generators/Images/ImageResizeGenerator.cs b/src/Mobile.BuildTools/Generators/Images/ImageResizeGenerator.cs index 7537583d..2687bd64 100644 --- a/src/Mobile.BuildTools/Generators/Images/ImageResizeGenerator.cs +++ b/src/Mobile.BuildTools/Generators/Images/ImageResizeGenerator.cs @@ -1,12 +1,13 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Drawing; using System.IO; using Mobile.BuildTools.Build; using Mobile.BuildTools.Drawing; +using Mobile.BuildTools.Logging; using Mobile.BuildTools.Models.AppIcons; using Newtonsoft.Json; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Processing; +using SkiaSharp; namespace Mobile.BuildTools.Generators.Images { @@ -22,7 +23,7 @@ public ImageResizeGenerator(IBuildConfiguration buildConfiguration) protected override void ExecuteInternal() { Outputs = new List(); - foreach(var outputImage in OutputImages) + foreach (var outputImage in OutputImages) { ProcessImage(outputImage); Outputs.Add(outputImage); @@ -34,12 +35,40 @@ internal void ProcessImage(OutputImage outputImage) Log.LogMessage($"Generating file '{outputImage.OutputFile}"); var fi = new FileInfo(outputImage.OutputFile); Directory.CreateDirectory(Path.Combine(Build.IntermediateOutputPath, fi.DirectoryName)); - using var image = Image.Load(outputImage.InputFile); - var requiresBackground = outputImage.RequiresBackgroundColor && image.HasTransparentBackground(); + + using var image = ImageBase.Load(outputImage.InputFile); try { - image.Mutate(x => MutateImage(x, outputImage, requiresBackground)); + var context = CreateContext( + GetBackgroundColor(outputImage.BackgroundColor, outputImage.RequiresBackgroundColor, image.HasTransparentBackground), + Log, + 1.0, + outputImage.Scale, + outputImage.Width, + outputImage.Height, + image.GetOriginalSize()); + + if (!context.Scale.X.IsEqualTo(context.Scale.Y)) + { + Log.LogWarning("Image aspect ratio is not being maintained."); + } + + using var tempBitmap = new SKBitmap(context.Size.Width, context.Size.Height); + using var canvas = new SKCanvas(tempBitmap); + + canvas.Clear(context.BackgroundColor); + canvas.Save(); + canvas.Scale(context.Scale.X, context.Scale.Y); + + image.Draw(canvas, context); + + ApplyWatermark(outputImage, context, Log, canvas); + + using var outputBitmap = ApplyPadding(outputImage, image, context, tempBitmap); + + using var stream = File.Create(outputImage.OutputFile); + outputBitmap.Encode(stream, SKEncodedImageFormat.Png, 100); } catch (System.Exception ex) { @@ -51,34 +80,84 @@ internal void ProcessImage(OutputImage outputImage) {JsonConvert.SerializeObject(outputImage)}"); throw; } + } - using var outputStream = new FileStream(outputImage.OutputFile, FileMode.OpenOrCreate); - image.SaveAsPng(outputStream); + private SKBitmap ApplyPadding(OutputImage outputImage, ImageBase image, Context context, SKBitmap outputBitmap) + { + if (outputImage.PaddingFactor is null) + { + return outputBitmap; + } + + var paddedBitmap = new SKBitmap(context.Size.Width, context.Size.Height); + using var paddedCanvas = new SKCanvas(paddedBitmap); + + var paddedContext = CreateContext( + GetBackgroundColor(outputImage.PaddingColor, outputImage.RequiresBackgroundColor, image.HasTransparentBackground), + Log, + 1.0, + outputImage.PaddingFactor.Value, + 0, + 0, + context.Size); + + paddedCanvas.Clear(paddedContext.BackgroundColor); + paddedCanvas.Save(); + + using var resizedBitmap = outputBitmap.Resize(new SKImageInfo(paddedContext.Size.Width, paddedContext.Size.Height), SKFilterQuality.High); + + paddedCanvas.DrawBitmap( + resizedBitmap, + (context.Size.Width - paddedContext.Size.Width) / 2, + (context.Size.Height - paddedContext.Size.Height) / 2); + + return paddedBitmap; + } + + private static void ApplyWatermark(OutputImage outputImage, Context context, ILog log, SKCanvas canvas) + { + using var watermark = Watermark.Create(outputImage.Watermark, new PointF(1 / context.Scale.X, 1 / context.Scale.Y)); + var watermarkContext = CreateContext(SKColors.Transparent, log, outputImage.Watermark?.Opacity ?? 1.0, 0, context.Size.Width, context.Size.Height, watermark.GetOriginalSize()); + watermark.Draw(canvas, watermarkContext); } - private static IImageProcessingContext MutateImage(IImageProcessingContext ctx, OutputImage outputImage, bool requiresBackground) + private static SKColor GetBackgroundColor(string colorText, bool requiresBackgroundColor, bool hasTransparentBackground) { - var currentSize = ctx.GetCurrentSize(); - ctx.Resize(GetUpdatedSize(outputImage, currentSize)); - ctx.ApplyWatermark(outputImage.Watermark); + var requiresBackground = requiresBackgroundColor && hasTransparentBackground; + var isExpectedBackgroundDefined = !string.IsNullOrWhiteSpace(colorText); - if (!string.IsNullOrWhiteSpace(outputImage.BackgroundColor) || requiresBackground) + if (isExpectedBackgroundDefined || requiresBackground) { - ctx.ApplyBackground(outputImage.BackgroundColor); + if (ColorUtils.TryParse(isExpectedBackgroundDefined ? colorText : Constants.DefaultBackgroundColor, out var color)) + { + return color; + } } - ctx.ResizeCanvas(outputImage.PaddingFactor, outputImage.PaddingColor); - return ctx; + return SKColors.Transparent; } - private static Size GetUpdatedSize(OutputImage output, Size currentSize) + private static Context CreateContext(SKColor backgroundColor, Logging.ILog log, double opacity, double scale, int width, int height, Size currentSize) { - if (output.Scale > 0) + if (scale > 0) { - return new Size((int)(currentSize.Width * output.Scale), (int)(currentSize.Height * output.Scale)); + return new Context( + backgroundColor, + log, + opacity, + (int)(currentSize.Width * scale), + (int)(currentSize.Height * scale), + (float)scale); } - return new Size(output.Width, output.Height); + return new Context( + backgroundColor, + log, + opacity, + width, + height, + (float)width / currentSize.Width, + (float)height / currentSize.Height); } } } diff --git a/src/Mobile.BuildTools/Mobile.BuildTools.csproj b/src/Mobile.BuildTools/Mobile.BuildTools.csproj index 7bececa7..19e097ad 100644 --- a/src/Mobile.BuildTools/Mobile.BuildTools.csproj +++ b/src/Mobile.BuildTools/Mobile.BuildTools.csproj @@ -21,26 +21,32 @@ - + - + + - - - + + + + + + + + - + \ No newline at end of file diff --git a/src/Mobile.BuildTools/Models/AppIcons/OutputImage.cs b/src/Mobile.BuildTools/Models/AppIcons/OutputImage.cs index c6ff6260..d36a218e 100644 --- a/src/Mobile.BuildTools/Models/AppIcons/OutputImage.cs +++ b/src/Mobile.BuildTools/Models/AppIcons/OutputImage.cs @@ -24,7 +24,9 @@ public partial class OutputImage : IEqualityComparer public string BackgroundColor { get; set; } - public double? PaddingFactor { get; set; } + public double? PaddingFactor { get; set; } + + // Possibly add background image. public string PaddingColor { get; set; } diff --git a/src/Mobile.BuildTools/ScssToCss.targets b/src/Mobile.BuildTools/ScssToCss.targets index f8f2fe30..07cab468 100644 --- a/src/Mobile.BuildTools/ScssToCss.targets +++ b/src/Mobile.BuildTools/ScssToCss.targets @@ -7,16 +7,19 @@ _CollectScss; + _EnsureScssRuntimeIsAvailable; ProcessScss; $(CssGDependsOn); _CollectScss; + _EnsureScssRuntimeIsAvailable; ProcessScss; $(PrepareResourcesDependsOn) _CollectScss; + _EnsureScssRuntimeIsAvailable; @@ -28,6 +31,28 @@ + + + <_NativeRuntime>win-x86 + <_NativeRuntimeFile>libsass.dll + + <_NativeRuntime Condition=" '$(OS)' != 'Windows_NT'">osx-x64 + <_NativeRuntimeFile Condition=" '$(OS)' != 'Windows_NT'">libsass.dylib + $([System.IO.Path]::Combine($(MSBuildThisFileDirectory), 'runtimes', $(_NativeRuntime), $(_NativeRuntimeFile))) + $([System.IO.Path]::Combine($(MSBuildThisFileDirectory), $(_NativeRuntimeFile))) + + + + + + + + success = ColorUtils.TryParse(input, out result)); Assert.Null(ex); Assert.True(success); @@ -32,10 +28,13 @@ public void GeneratesExpectedColor(string input, Color color) public static IEnumerable Data => new List { - new object[] { "Red", Color.Red }, - new object[] { "Blue", Color.Blue }, - new object[] { "OrangeRed", Color.OrangeRed }, - new object[] { "#FF88DD", Color.ParseHex("#FF88DD") }, + new object[] { "Red", SKColors.Red }, + new object[] { "red", SKColors.Red }, + new object[] { "Blue", SKColors.Blue }, + new object[] { "blue", SKColors.Blue }, + new object[] { "OrangeRed", SKColors.OrangeRed }, + new object[] { "orangered", SKColors.OrangeRed }, + new object[] { "#FF88DD", SKColor.Parse("#FF88DD") }, }; } } diff --git a/tests/Mobile.BuildTools.Tests/Fixtures/Generators/ImageResizerGeneratorFixture.cs b/tests/Mobile.BuildTools.Tests/Fixtures/Generators/ImageResizerGeneratorFixture.cs index 6cd9b18f..c8cb7c57 100644 --- a/tests/Mobile.BuildTools.Tests/Fixtures/Generators/ImageResizerGeneratorFixture.cs +++ b/tests/Mobile.BuildTools.Tests/Fixtures/Generators/ImageResizerGeneratorFixture.cs @@ -1,23 +1,18 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.IO; -using System.Text; +using System.Text; using Mobile.BuildTools.Generators.Images; +using Mobile.BuildTools.Models.AppIcons; +using SkiaSharp; using Xunit; using Xunit.Abstractions; -using Mobile.BuildTools.Models.AppIcons; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Advanced; - -namespace Mobile.BuildTools.Tests.Fixtures.Generators -{ - public class ImageResizer { } - - [CollectionDefinition(nameof(ImageResizer), DisableParallelization = true)] - public class ImageResizerCollection : ICollectionFixture { } +namespace Mobile.BuildTools.Tests.Fixtures.Generators +{ + public class ImageResizer { } + + [CollectionDefinition(nameof(ImageResizer), DisableParallelization = true)] + public class ImageResizerCollection : ICollectionFixture { } + [Collection(nameof(ImageResizer))] public class ImageResizerGeneratorFixture : FixtureBase { @@ -27,22 +22,25 @@ public ImageResizerGeneratorFixture(ITestOutputHelper testOutputHelper) } [Theory] - [InlineData("xxxhdpi", 1, 300)] - [InlineData("xxhdpi", .75, 225)] - [InlineData("xhdpi", .5, 150)] - public void GeneratesImage(string resourcePath, double scale, int expectedOutput) + [InlineData("dotnetbot.png", "xxxhdpi", 1)] + [InlineData("dotnetbot.png", "xxhdpi", .75)] + [InlineData("dotnetbot.png", "xhdpi", .5)] + [InlineData("dotnetbot.svg", "xxxhdpi", 1)] + [InlineData("dotnetbot.svg", "xxhdpi", .75)] + [InlineData("dotnetbot.svg", "xhdpi", .5)] + public void GeneratesImage(string inputFile, string resourcePath, double scale) { - var config = GetConfiguration(); - config.IntermediateOutputPath += resourcePath; + var config = GetConfiguration(); + config.IntermediateOutputPath += GetOutputDirectorySuffix((nameof(inputFile), inputFile), (nameof(scale), scale)); var generator = new ImageResizeGenerator(config); var image = new OutputImage { Height = 0, Width = 0, - InputFile = Path.Combine(TestConstants.ImageDirectory, "dotnetbot.png"), + InputFile = Path.Combine(TestConstants.ImageDirectory, inputFile), OutputFile = Path.Combine(config.IntermediateOutputPath, "dotnetbot.png"), - OutputLink = Path.Combine("Resources", "drawable-xxxhdpi", "dotnetbot.png"), + OutputLink = Path.Combine("Resources", resourcePath, "dotnetbot.png"), RequiresBackgroundColor = false, Scale = scale, ShouldBeVisible = true, @@ -51,30 +49,31 @@ public void GeneratesImage(string resourcePath, double scale, int expectedOutput var ex = Record.Exception(() => generator.ProcessImage(image)); - Assert.Null(ex); - Assert.True(File.Exists(image.OutputFile)); - - using var imageResource = Image.Load(image.OutputFile); - Assert.Equal(expectedOutput, imageResource.Width); + Assert.Null(ex); + + VerifyImageContents(image); } [Theory] - [InlineData("xxxhdpi", 300)] - [InlineData("xxhdpi", 225)] - [InlineData("xhdpi", 150)] - public void GeneratesImageWithCustomHeightWidth(string resourcePath, int expectedOutput) + [InlineData("dotnetbot.png", "xxxhdpi", 300)] + [InlineData("dotnetbot.png", "xxhdpi", 225)] + [InlineData("dotnetbot.png", "xhdpi", 150)] + [InlineData("dotnetbot.svg", "xxxhdpi", 300)] + [InlineData("dotnetbot.svg", "xxhdpi", 225)] + [InlineData("dotnetbot.svg", "xhdpi", 150)] + public void GeneratesImageWithCustomHeightWidth(string inputFile, string resourcePath, int expectedOutput) { - var config = GetConfiguration(); - config.IntermediateOutputPath += resourcePath; + var config = GetConfiguration(); + config.IntermediateOutputPath += GetOutputDirectorySuffix((nameof(inputFile), inputFile), (nameof(expectedOutput), expectedOutput)); var generator = new ImageResizeGenerator(config); var image = new OutputImage { Height = expectedOutput, Width = expectedOutput, - InputFile = Path.Combine(TestConstants.ImageDirectory, "dotnetbot.png"), + InputFile = Path.Combine(TestConstants.ImageDirectory, inputFile), OutputFile = Path.Combine(config.IntermediateOutputPath, "dotnetbot.png"), - OutputLink = Path.Combine("Resources", "drawable-xxxhdpi", "dotnetbot.png"), + OutputLink = Path.Combine("Resources", resourcePath, "dotnetbot.png"), RequiresBackgroundColor = false, Scale = 0, ShouldBeVisible = true, @@ -83,11 +82,9 @@ public void GeneratesImageWithCustomHeightWidth(string resourcePath, int expecte var ex = Record.Exception(() => generator.ProcessImage(image)); - Assert.Null(ex); - Assert.True(File.Exists(image.OutputFile)); - - using var imageResource = Image.Load(image.OutputFile); - Assert.Equal(expectedOutput, imageResource.Width); + Assert.Null(ex); + + VerifyImageContents(image); } [Theory] @@ -97,7 +94,8 @@ public void GeneratesImageWithCustomHeightWidth(string resourcePath, int expecte [InlineData("icon", "beta-version")] public void AppliesWatermark(string inputImageName, string watermarkImage) { - var config = GetConfiguration(); + var config = GetConfiguration(); + config.IntermediateOutputPath += GetOutputDirectorySuffix((nameof(inputImageName), inputImageName), (nameof(watermarkImage), watermarkImage)); var generator = new ImageResizeGenerator(config); var image = new OutputImage @@ -105,7 +103,7 @@ public void AppliesWatermark(string inputImageName, string watermarkImage) Height = 0, Width = 0, InputFile = Path.Combine(TestConstants.WatermarkImageDirectory, $"{inputImageName}.png"), - OutputFile = Path.Combine($"{config.IntermediateOutputPath}-{inputImageName}-{watermarkImage}", $"{inputImageName}.png"), + OutputFile = Path.Combine(config.IntermediateOutputPath, $"{inputImageName}.png"), OutputLink = Path.Combine("Resources", "drawable-xxxhdpi", $"{inputImageName}.png"), RequiresBackgroundColor = false, Scale = 1, @@ -118,26 +116,7 @@ public void AppliesWatermark(string inputImageName, string watermarkImage) generator.ProcessImage(image); - using var inputImage = Image.Load(image.InputFile); - using var outputImage = Image.Load(image.OutputFile); - using var inputClone = inputImage.CloneAs(); - using var outputClone = outputImage.CloneAs(); - - bool appliedWatermark; - for (var y = 0; y < inputImage.Height; ++y) - { - var inputPixelRowSpan = inputClone.GetPixelRowSpan(y); - var outputPixelRowSpan = outputClone.GetPixelRowSpan(y); - for (var x = 0; x < inputImage.Width; ++x) - { - appliedWatermark = inputPixelRowSpan[x] == outputPixelRowSpan[x]; - if (appliedWatermark) - return; - } - } - - _testOutputHelper.WriteLine("All pixels are the same in the Input and Output Images"); - Assert.True(false); + VerifyImageContents(image); } [Fact] @@ -161,32 +140,7 @@ public void SetsDefaultBackground() generator.ProcessImage(image); - using var inputImage = Image.Load(image.InputFile); - using var outputImage = Image.Load(image.OutputFile); - using var inputClone = inputImage.CloneAs(); - using var outputClone = outputImage.CloneAs(); - - var comparedTransparentPixel = false; - for (var y = 0; y < inputImage.Height; ++y) - { - var inputPixelRowSpan = inputClone.GetPixelRowSpan(y); - var outputPixelRowSpan = outputClone.GetPixelRowSpan(y); - for (var x = 0; x < inputImage.Width; ++x) - { - var startingPixel = inputPixelRowSpan[x]; - if (startingPixel.A == 0) - { - comparedTransparentPixel = true; - var pixel = outputPixelRowSpan[x]; - Assert.Equal(255, pixel.R); - Assert.Equal(255, pixel.G); - Assert.Equal(255, pixel.B); - Assert.Equal(255, pixel.A); - } - } - } - - Assert.True(comparedTransparentPixel); + VerifyImageContents(image); } [Fact] @@ -211,40 +165,18 @@ public void SetsCustomBackground() generator.ProcessImage(image); - using var inputImage = Image.Load(image.InputFile); - using var outputImage = Image.Load(image.OutputFile); - using var inputClone = inputImage.CloneAs(); - using var outputClone = outputImage.CloneAs(); - - var comparedTransparentPixel = false; - for (var y = 0; y < inputImage.Height; ++y) - { - var inputPixelRowSpan = inputClone.GetPixelRowSpan(y); - var outputPixelRowSpan = outputClone.GetPixelRowSpan(y); - for (var x = 0; x < inputImage.Width; ++x) - { - var startingPixel = inputPixelRowSpan[x]; - if (startingPixel.A == 0) - { - comparedTransparentPixel = true; - var pixel = outputPixelRowSpan[x]; - Assert.Equal(138, pixel.R); - Assert.Equal(43, pixel.G); - Assert.Equal(226, pixel.B); - Assert.Equal(255, pixel.A); - } - } - } - - Assert.True(comparedTransparentPixel); + VerifyImageContents(image); } [Theory] - [InlineData("Dev")] - [InlineData("Stage")] - public void AppliesTextBanner(string text) + [InlineData("Dev", 0.5)] + [InlineData("Dev", 1.0)] + [InlineData("Stage", 0.5)] + [InlineData("Something long", 0.5)] + public void AppliesTextBanner(string text, double scale) { - var config = GetConfiguration(); + var config = GetConfiguration(); + config.IntermediateOutputPath += GetOutputDirectorySuffix((nameof(text), text), (nameof(scale), scale)); var generator = new ImageResizeGenerator(config); var image = new OutputImage @@ -255,7 +187,7 @@ public void AppliesTextBanner(string text) OutputFile = Path.Combine(config.IntermediateOutputPath, "dotnetbot.png"), OutputLink = Path.Combine("Resources", "drawable-xxxhdpi", "dotnetbot.png"), RequiresBackgroundColor = true, - Scale = .5, + Scale = scale, ShouldBeVisible = true, Watermark = new WatermarkConfiguration { @@ -263,7 +195,86 @@ public void AppliesTextBanner(string text) } }; - generator.ProcessImage(image); + generator.ProcessImage(image); + + VerifyImageContents(image); + } + + [Theory] + [InlineData("dotnetbot.png", 1)] + [InlineData("dotnetbot.png", .75)] + [InlineData("dotnetbot.png", .5)] + [InlineData("dotnetbot.svg", 1)] + [InlineData("dotnetbot.svg", .75)] + [InlineData("dotnetbot.svg", .5)] + public void AppliesPadding(string inputFile, double paddingFactor) + { + var config = GetConfiguration(); + config.IntermediateOutputPath += GetOutputDirectorySuffix((nameof(paddingFactor), paddingFactor), (nameof(inputFile), inputFile)); + var generator = new ImageResizeGenerator(config); + + var image = new OutputImage + { + Height = 0, + Width = 0, + InputFile = Path.Combine(TestConstants.ImageDirectory, inputFile), + OutputFile = Path.Combine(config.IntermediateOutputPath, "dotnetbot.png"), + OutputLink = Path.Combine("Resources", "drawable-xxxhdpi", "dotnetbot.png"), + RequiresBackgroundColor = false, + Scale = 1.0, + ShouldBeVisible = true, + Watermark = null, + BackgroundColor = "Red", + PaddingColor = "Yellow", + PaddingFactor = paddingFactor + }; + + var ex = Record.Exception(() => generator.ProcessImage(image)); + + Assert.Null(ex); + + VerifyImageContents(image); + } + + private static string GetOutputDirectorySuffix(params (string, object)[] values) + { + var builder = new StringBuilder(); + + foreach (var value in values) + { + var prefix = "and"; + if (builder.Length == 0) + { + prefix = "-with"; + } + + builder.Append($"{prefix}-{value.Item1}-of-{value.Item2}-"); + } + + return builder.ToString(); + } + + private void VerifyImageContents(OutputImage image) + { + var expectedFilePath = Path.Combine(TestConstants.ExpectedImageDirectory, image.OutputFile); + var outputFilePath = image.OutputFile; + + Assert.True(File.Exists(expectedFilePath), $"Expected image file '{expectedFilePath}' does not exist"); + Assert.True(File.Exists(outputFilePath), $"Resulting image file '{outputFilePath}' does not exist"); + + using var expectedImage = SKBitmap.Decode(expectedFilePath); + using var outputImage = SKBitmap.Decode(outputFilePath); + + Assert.Equal(expectedImage.Width, outputImage.Width); + Assert.Equal(expectedImage.Height, outputImage.Height); + + for (var y = 0; y < expectedImage.Height; ++y) + { + for (var x = 0; x < expectedImage.Width; ++x) + { + Assert.Equal(expectedImage.GetPixel(x, y), outputImage.GetPixel(x, y)); + } + } } } } diff --git a/tests/Mobile.BuildTools.Tests/Mobile.BuildTools.Tests.csproj b/tests/Mobile.BuildTools.Tests/Mobile.BuildTools.Tests.csproj index 9bde7885..7bde0d59 100644 --- a/tests/Mobile.BuildTools.Tests/Mobile.BuildTools.Tests.csproj +++ b/tests/Mobile.BuildTools.Tests/Mobile.BuildTools.Tests.csproj @@ -22,13 +22,8 @@ - - - - - - - + + diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesPadding-with-paddingFactor-of-0.5-and-inputFile-of-dotnetbot.png-/dotnetbot.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesPadding-with-paddingFactor-of-0.5-and-inputFile-of-dotnetbot.png-/dotnetbot.png new file mode 100644 index 00000000..63c809b1 Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesPadding-with-paddingFactor-of-0.5-and-inputFile-of-dotnetbot.png-/dotnetbot.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesPadding-with-paddingFactor-of-0.5-and-inputFile-of-dotnetbot.svg-/dotnetbot.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesPadding-with-paddingFactor-of-0.5-and-inputFile-of-dotnetbot.svg-/dotnetbot.png new file mode 100644 index 00000000..10e5284e Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesPadding-with-paddingFactor-of-0.5-and-inputFile-of-dotnetbot.svg-/dotnetbot.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesPadding-with-paddingFactor-of-0.75-and-inputFile-of-dotnetbot.png-/dotnetbot.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesPadding-with-paddingFactor-of-0.75-and-inputFile-of-dotnetbot.png-/dotnetbot.png new file mode 100644 index 00000000..fd70e59d Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesPadding-with-paddingFactor-of-0.75-and-inputFile-of-dotnetbot.png-/dotnetbot.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesPadding-with-paddingFactor-of-0.75-and-inputFile-of-dotnetbot.svg-/dotnetbot.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesPadding-with-paddingFactor-of-0.75-and-inputFile-of-dotnetbot.svg-/dotnetbot.png new file mode 100644 index 00000000..f32d0546 Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesPadding-with-paddingFactor-of-0.75-and-inputFile-of-dotnetbot.svg-/dotnetbot.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesPadding-with-paddingFactor-of-1-and-inputFile-of-dotnetbot.png-/dotnetbot.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesPadding-with-paddingFactor-of-1-and-inputFile-of-dotnetbot.png-/dotnetbot.png new file mode 100644 index 00000000..b72e2edf Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesPadding-with-paddingFactor-of-1-and-inputFile-of-dotnetbot.png-/dotnetbot.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesPadding-with-paddingFactor-of-1-and-inputFile-of-dotnetbot.svg-/dotnetbot.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesPadding-with-paddingFactor-of-1-and-inputFile-of-dotnetbot.svg-/dotnetbot.png new file mode 100644 index 00000000..468c605a Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesPadding-with-paddingFactor-of-1-and-inputFile-of-dotnetbot.svg-/dotnetbot.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesTextBanner-with-text-of-Dev-and-scale-of-0.5-/dotnetbot.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesTextBanner-with-text-of-Dev-and-scale-of-0.5-/dotnetbot.png new file mode 100644 index 00000000..eda02f49 Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesTextBanner-with-text-of-Dev-and-scale-of-0.5-/dotnetbot.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesTextBanner-with-text-of-Dev-and-scale-of-1-/dotnetbot.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesTextBanner-with-text-of-Dev-and-scale-of-1-/dotnetbot.png new file mode 100644 index 00000000..620765f7 Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesTextBanner-with-text-of-Dev-and-scale-of-1-/dotnetbot.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesTextBanner-with-text-of-Something long-and-scale-of-0.5-/dotnetbot.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesTextBanner-with-text-of-Something long-and-scale-of-0.5-/dotnetbot.png new file mode 100644 index 00000000..c0ea39c7 Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesTextBanner-with-text-of-Something long-and-scale-of-0.5-/dotnetbot.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesTextBanner-with-text-of-Stage-and-scale-of-0.5-/dotnetbot.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesTextBanner-with-text-of-Stage-and-scale-of-0.5-/dotnetbot.png new file mode 100644 index 00000000..b15d8238 Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesTextBanner-with-text-of-Stage-and-scale-of-0.5-/dotnetbot.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesWatermark-with-inputImageName-of-dotnetbot-and-watermarkImage-of-beta-version-/dotnetbot.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesWatermark-with-inputImageName-of-dotnetbot-and-watermarkImage-of-beta-version-/dotnetbot.png new file mode 100644 index 00000000..6b1d73a3 Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesWatermark-with-inputImageName-of-dotnetbot-and-watermarkImage-of-beta-version-/dotnetbot.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesWatermark-with-inputImageName-of-dotnetbot-and-watermarkImage-of-example-/dotnetbot.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesWatermark-with-inputImageName-of-dotnetbot-and-watermarkImage-of-example-/dotnetbot.png new file mode 100644 index 00000000..e02b808e Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesWatermark-with-inputImageName-of-dotnetbot-and-watermarkImage-of-example-/dotnetbot.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesWatermark-with-inputImageName-of-icon-and-watermarkImage-of-beta-version-/icon.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesWatermark-with-inputImageName-of-icon-and-watermarkImage-of-beta-version-/icon.png new file mode 100644 index 00000000..07a7d756 Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesWatermark-with-inputImageName-of-icon-and-watermarkImage-of-beta-version-/icon.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesWatermark-with-inputImageName-of-icon-and-watermarkImage-of-example-/icon.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesWatermark-with-inputImageName-of-icon-and-watermarkImage-of-example-/icon.png new file mode 100644 index 00000000..568bde56 Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/AppliesWatermark-with-inputImageName-of-icon-and-watermarkImage-of-example-/icon.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImage-with-inputFile-of-dotnetbot.png-and-scale-of-0.5-/dotnetbot.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImage-with-inputFile-of-dotnetbot.png-and-scale-of-0.5-/dotnetbot.png new file mode 100644 index 00000000..b4fbd559 Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImage-with-inputFile-of-dotnetbot.png-and-scale-of-0.5-/dotnetbot.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImage-with-inputFile-of-dotnetbot.png-and-scale-of-0.75-/dotnetbot.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImage-with-inputFile-of-dotnetbot.png-and-scale-of-0.75-/dotnetbot.png new file mode 100644 index 00000000..133efa37 Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImage-with-inputFile-of-dotnetbot.png-and-scale-of-0.75-/dotnetbot.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImage-with-inputFile-of-dotnetbot.png-and-scale-of-1-/dotnetbot.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImage-with-inputFile-of-dotnetbot.png-and-scale-of-1-/dotnetbot.png new file mode 100644 index 00000000..7a5aa75d Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImage-with-inputFile-of-dotnetbot.png-and-scale-of-1-/dotnetbot.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImage-with-inputFile-of-dotnetbot.svg-and-scale-of-0.5-/dotnetbot.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImage-with-inputFile-of-dotnetbot.svg-and-scale-of-0.5-/dotnetbot.png new file mode 100644 index 00000000..489cc695 Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImage-with-inputFile-of-dotnetbot.svg-and-scale-of-0.5-/dotnetbot.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImage-with-inputFile-of-dotnetbot.svg-and-scale-of-0.75-/dotnetbot.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImage-with-inputFile-of-dotnetbot.svg-and-scale-of-0.75-/dotnetbot.png new file mode 100644 index 00000000..ff212477 Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImage-with-inputFile-of-dotnetbot.svg-and-scale-of-0.75-/dotnetbot.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImage-with-inputFile-of-dotnetbot.svg-and-scale-of-1-/dotnetbot.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImage-with-inputFile-of-dotnetbot.svg-and-scale-of-1-/dotnetbot.png new file mode 100644 index 00000000..1a43fe17 Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImage-with-inputFile-of-dotnetbot.svg-and-scale-of-1-/dotnetbot.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImageWithCustomHeightWidth-with-inputFile-of-dotnetbot.png-and-expectedOutput-of-150-/dotnetbot.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImageWithCustomHeightWidth-with-inputFile-of-dotnetbot.png-and-expectedOutput-of-150-/dotnetbot.png new file mode 100644 index 00000000..b4fbd559 Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImageWithCustomHeightWidth-with-inputFile-of-dotnetbot.png-and-expectedOutput-of-150-/dotnetbot.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImageWithCustomHeightWidth-with-inputFile-of-dotnetbot.png-and-expectedOutput-of-225-/dotnetbot.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImageWithCustomHeightWidth-with-inputFile-of-dotnetbot.png-and-expectedOutput-of-225-/dotnetbot.png new file mode 100644 index 00000000..133efa37 Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImageWithCustomHeightWidth-with-inputFile-of-dotnetbot.png-and-expectedOutput-of-225-/dotnetbot.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImageWithCustomHeightWidth-with-inputFile-of-dotnetbot.png-and-expectedOutput-of-300-/dotnetbot.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImageWithCustomHeightWidth-with-inputFile-of-dotnetbot.png-and-expectedOutput-of-300-/dotnetbot.png new file mode 100644 index 00000000..7a5aa75d Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImageWithCustomHeightWidth-with-inputFile-of-dotnetbot.png-and-expectedOutput-of-300-/dotnetbot.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImageWithCustomHeightWidth-with-inputFile-of-dotnetbot.svg-and-expectedOutput-of-150-/dotnetbot.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImageWithCustomHeightWidth-with-inputFile-of-dotnetbot.svg-and-expectedOutput-of-150-/dotnetbot.png new file mode 100644 index 00000000..62cf714c Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImageWithCustomHeightWidth-with-inputFile-of-dotnetbot.svg-and-expectedOutput-of-150-/dotnetbot.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImageWithCustomHeightWidth-with-inputFile-of-dotnetbot.svg-and-expectedOutput-of-225-/dotnetbot.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImageWithCustomHeightWidth-with-inputFile-of-dotnetbot.svg-and-expectedOutput-of-225-/dotnetbot.png new file mode 100644 index 00000000..6d174f77 Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImageWithCustomHeightWidth-with-inputFile-of-dotnetbot.svg-and-expectedOutput-of-225-/dotnetbot.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImageWithCustomHeightWidth-with-inputFile-of-dotnetbot.svg-and-expectedOutput-of-300-/dotnetbot.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImageWithCustomHeightWidth-with-inputFile-of-dotnetbot.svg-and-expectedOutput-of-300-/dotnetbot.png new file mode 100644 index 00000000..06de57fe Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/GeneratesImageWithCustomHeightWidth-with-inputFile-of-dotnetbot.svg-and-expectedOutput-of-300-/dotnetbot.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/SetsCustomBackground/dotnetbot.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/SetsCustomBackground/dotnetbot.png new file mode 100644 index 00000000..c1624d5d Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/SetsCustomBackground/dotnetbot.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/SetsDefaultBackground/dotnetbot.png b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/SetsDefaultBackground/dotnetbot.png new file mode 100644 index 00000000..a1bfb60f Binary files /dev/null and b/tests/Mobile.BuildTools.Tests/Templates/Images/Expected/Tests/ImageResizerGeneratorFixture/SetsDefaultBackground/dotnetbot.png differ diff --git a/tests/Mobile.BuildTools.Tests/Templates/Images/dotnetbot.svg b/tests/Mobile.BuildTools.Tests/Templates/Images/dotnetbot.svg new file mode 100644 index 00000000..abfaff26 --- /dev/null +++ b/tests/Mobile.BuildTools.Tests/Templates/Images/dotnetbot.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Mobile.BuildTools.Tests/TestConstants.cs b/tests/Mobile.BuildTools.Tests/TestConstants.cs index d235fba9..5b7cb108 100644 --- a/tests/Mobile.BuildTools.Tests/TestConstants.cs +++ b/tests/Mobile.BuildTools.Tests/TestConstants.cs @@ -5,7 +5,8 @@ namespace Mobile.BuildTools.Tests { internal static class TestConstants - { + { + public static readonly string ExpectedImageDirectory = Path.Combine("Templates", "Images", "Expected"); public static readonly string ImageDirectory = Path.Combine("Templates", "Images"); public static readonly string AndroidImageDirectory = Path.Combine(ImageDirectory, "MonoAndroid"); public static readonly string AppleImageDirectory = Path.Combine(ImageDirectory, "Xamarin.iOS");