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