Browse Source

update project files

Luke Pulverenti 8 years ago
parent
commit
34c3783f42

+ 6 - 0
Emby.Drawing.Skia/Emby.Drawing.Skia.csproj

@@ -52,7 +52,12 @@
     <Compile Include="..\SharedVersion.cs">
       <Link>Properties\SharedVersion.cs</Link>
     </Compile>
+    <Compile Include="PercentPlayedDrawer.cs" />
+    <Compile Include="PlayedIndicatorDrawer.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="SkiaEncoder.cs" />
+    <Compile Include="StripCollageBuilder.cs" />
+    <Compile Include="UnplayedCountIndicator.cs" />
   </ItemGroup>
   <ItemGroup>
     <Reference Include="SkiaSharp, Version=1.57.0.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
@@ -61,6 +66,7 @@
     </Reference>
   </ItemGroup>
   <ItemGroup>
+    <EmbeddedResource Include="fonts\robotoregular.ttf" />
     <None Include="packages.config" />
   </ItemGroup>
   <Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />

+ 31 - 0
Emby.Drawing.Skia/PercentPlayedDrawer.cs

@@ -0,0 +1,31 @@
+using SkiaSharp;
+using MediaBrowser.Model.Drawing;
+using System;
+
+namespace Emby.Drawing.Skia
+{
+    public class PercentPlayedDrawer
+    {
+        private const int IndicatorHeight = 8;
+
+        public void Process(SKCanvas canvas, ImageSize imageSize, double percent)
+        {
+            using (var paint = new SKPaint())
+            {
+                var endX = imageSize.Width - 1;
+                var endY = imageSize.Height - 1;
+
+                paint.Color = SKColor.Parse("#99000000");
+                paint.Style = SKPaintStyle.Fill;
+                canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, (float)endX, (float)endY), paint);
+
+                double foregroundWidth = endX;
+                foregroundWidth *= percent;
+                foregroundWidth /= 100;
+
+                paint.Color = SKColor.Parse("#FF52B54B");
+                canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(Math.Round(foregroundWidth)), (float)endY), paint);
+            }
+        }
+    }
+}

+ 120 - 0
Emby.Drawing.Skia/PlayedIndicatorDrawer.cs

@@ -0,0 +1,120 @@
+using SkiaSharp;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Model.Drawing;
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.IO;
+using System.Reflection;
+
+namespace Emby.Drawing.Skia
+{
+    public class PlayedIndicatorDrawer
+    {
+        private const int FontSize = 42;
+        private const int OffsetFromTopRightCorner = 38;
+
+        private readonly IApplicationPaths _appPaths;
+        private readonly IHttpClient _iHttpClient;
+        private readonly IFileSystem _fileSystem;
+
+        public PlayedIndicatorDrawer(IApplicationPaths appPaths, IHttpClient iHttpClient, IFileSystem fileSystem)
+        {
+            _appPaths = appPaths;
+            _iHttpClient = iHttpClient;
+            _fileSystem = fileSystem;
+        }
+
+        public async Task DrawPlayedIndicator(SKCanvas canvas, ImageSize imageSize)
+        {
+            var x = imageSize.Width - OffsetFromTopRightCorner;
+
+            using (var paint = new SKPaint())
+            {
+                paint.Color = SKColor.Parse("#CC52B54B");
+                paint.Style = SKPaintStyle.Fill;
+                canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint);
+            }
+
+            using (var paint = new SKPaint())
+            {
+                paint.Color = new SKColor(255, 255, 255, 255);
+                paint.Style = SKPaintStyle.Fill;
+                paint.Typeface = SKTypeface.FromFile(await DownloadFont("webdings.ttf", "https://github.com/MediaBrowser/Emby.Resources/raw/master/fonts/webdings.ttf",
+                    _appPaths, _iHttpClient, _fileSystem).ConfigureAwait(false));
+                paint.TextSize = FontSize;
+                paint.IsAntialias = true;
+
+                canvas.DrawText("a", (float)x-20, OffsetFromTopRightCorner + 12, paint);
+            }
+        }
+
+        internal static string ExtractFont(string name, IApplicationPaths paths, IFileSystem fileSystem)
+        {
+            var filePath = Path.Combine(paths.ProgramDataPath, "fonts", name);
+
+            if (fileSystem.FileExists(filePath))
+            {
+                return filePath;
+            }
+
+            var namespacePath = typeof(PlayedIndicatorDrawer).Namespace + ".fonts." + name;
+            var tempPath = Path.Combine(paths.TempDirectory, Guid.NewGuid().ToString("N") + ".ttf");
+            fileSystem.CreateDirectory(fileSystem.GetDirectoryName(tempPath));
+
+            using (var stream = typeof(PlayedIndicatorDrawer).GetTypeInfo().Assembly.GetManifestResourceStream(namespacePath))
+            {
+                using (var fileStream = fileSystem.GetFileStream(tempPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
+                {
+                    stream.CopyTo(fileStream);
+                }
+            }
+
+            fileSystem.CreateDirectory(fileSystem.GetDirectoryName(filePath));
+
+            try
+            {
+                fileSystem.CopyFile(tempPath, filePath, false);
+            }
+            catch (IOException)
+            {
+
+            }
+
+            return tempPath;
+        }
+
+        internal static async Task<string> DownloadFont(string name, string url, IApplicationPaths paths, IHttpClient httpClient, IFileSystem fileSystem)
+        {
+            var filePath = Path.Combine(paths.ProgramDataPath, "fonts", name);
+
+            if (fileSystem.FileExists(filePath))
+            {
+                return filePath;
+            }
+
+            var tempPath = await httpClient.GetTempFile(new HttpRequestOptions
+            {
+                Url = url,
+                Progress = new Progress<double>()
+
+            }).ConfigureAwait(false);
+
+            fileSystem.CreateDirectory(fileSystem.GetDirectoryName(filePath));
+
+            try
+            {
+                fileSystem.CopyFile(tempPath, filePath, false);
+            }
+            catch (IOException)
+            {
+
+            }
+
+            return tempPath;
+        }
+    }
+}

+ 261 - 0
Emby.Drawing.Skia/SkiaEncoder.cs

@@ -0,0 +1,261 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using SkiaSharp;
+using System;
+using System.Reflection;
+using System.Threading.Tasks;
+
+namespace Emby.Drawing.Skia
+{
+    public class SkiaEncoder : IImageEncoder
+    {
+        private readonly ILogger _logger;
+        private readonly IApplicationPaths _appPaths;
+        private readonly Func<IHttpClient> _httpClientFactory;
+        private readonly IFileSystem _fileSystem;
+
+        public SkiaEncoder(ILogger logger, IApplicationPaths appPaths, Func<IHttpClient> httpClientFactory, IFileSystem fileSystem)
+        {
+            _logger = logger;
+            _appPaths = appPaths;
+            _httpClientFactory = httpClientFactory;
+            _fileSystem = fileSystem;
+
+            LogVersion();
+        }
+
+        public string[] SupportedInputFormats
+        {
+            get
+            {
+                // Some common file name extensions for RAW picture files include: .cr2, .crw, .dng, .nef, .orf, .rw2, .pef, .arw, .sr2, .srf, and .tif.
+                return new[]
+                {
+                    "jpeg",
+                    "jpg",
+                    "png",
+                    "dng",
+                    "webp",
+                    "gif",
+                    "bmp",
+                    "ico",
+                    "astc",
+                    "ktx",
+                    "pkm",
+                    "wbmp"
+                };
+            }
+        }
+
+        public ImageFormat[] SupportedOutputFormats
+        {
+            get
+            {
+                return new[] { ImageFormat.Webp, ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png, ImageFormat.Bmp };
+            }
+        }
+
+        private void LogVersion()
+        {
+            _logger.Info("SkiaSharp version: " + GetVersion());
+        }
+
+        public static string GetVersion()
+        {
+            return typeof(SKCanvas).GetTypeInfo().Assembly.GetName().Version.ToString();
+        }
+
+        public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat)
+        {
+            switch (selectedFormat)
+            {
+                case ImageFormat.Bmp:
+                    return SKEncodedImageFormat.Bmp;
+                case ImageFormat.Jpg:
+                    return SKEncodedImageFormat.Jpeg;
+                case ImageFormat.Gif:
+                    return SKEncodedImageFormat.Gif;
+                case ImageFormat.Webp:
+                    return SKEncodedImageFormat.Webp;
+                case ImageFormat.Png:
+                default:
+                    return SKEncodedImageFormat.Png;
+            }
+        }
+
+        public void CropWhiteSpace(string inputPath, string outputPath)
+        {
+            CheckDisposed();
+
+            using (var bitmap = SKBitmap.Decode(inputPath))
+            {
+                // @todo
+            }
+        }
+
+        public ImageSize GetImageSize(string path)
+        {
+            CheckDisposed();
+
+            using (var s = new SKFileStream(path))
+            {
+                using (var codec = SKCodec.Create(s))
+                {
+                    var info = codec.Info;
+
+                    return new ImageSize
+                    {
+                        Width = info.Width,
+                        Height = info.Height
+                    };
+                }
+            }
+        }
+
+        public void EncodeImage(string inputPath, string outputPath, bool autoOrient, int width, int height, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat)
+        {
+            using (var bitmap = SKBitmap.Decode(inputPath))
+            {
+                using (var resizedBitmap = new SKBitmap(width, height, bitmap.ColorType, bitmap.AlphaType))
+                {
+                    // scale image
+                    bitmap.Resize(resizedBitmap, SKBitmapResizeMethod.Lanczos3);
+
+                    // create bitmap to use for canvas drawing
+                    using (var saveBitmap = new SKBitmap(width, height, bitmap.ColorType, bitmap.AlphaType))
+                    {
+                        // create canvas used to draw into bitmap
+                        using (var canvas = new SKCanvas(saveBitmap))
+                        {
+                            // set background color if present
+                            if (!string.IsNullOrWhiteSpace(options.BackgroundColor))
+                            {
+                                canvas.Clear(SKColor.Parse(options.BackgroundColor));
+                            }
+
+                            // Add blur if option is present
+                            if (options.Blur > 0)
+                            {
+                                using (var paint = new SKPaint())
+                                {
+                                    // create image from resized bitmap to apply blur
+                                    using (var filter = SKImageFilter.CreateBlur(5, 5))
+                                    {
+                                        paint.ImageFilter = filter;
+                                        canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint);
+                                    }
+                                }
+                            }
+                            else
+                            {
+                                // draw resized bitmap onto canvas
+                                canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height));
+                            }
+
+                            // If foreground layer present then draw
+                            if (!string.IsNullOrWhiteSpace(options.ForegroundLayer))
+                            {
+                                Double opacity;
+                                if (!Double.TryParse(options.ForegroundLayer, out opacity)) opacity = .4;
+
+                                var foregroundColor = String.Format("#{0:X2}000000", (Byte)((1-opacity) * 0xFF));
+                                canvas.DrawColor(SKColor.Parse(foregroundColor), SKBlendMode.SrcOver);
+                            }
+
+                            DrawIndicator(canvas, width, height, options);
+
+                            using (var outputStream = new SKFileWStream(outputPath))
+                            {
+                                saveBitmap.Encode(outputStream, GetImageFormat(selectedOutputFormat), quality);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        public void CreateImageCollage(ImageCollageOptions options)
+        {
+            double ratio = options.Width;
+            ratio /= options.Height;
+
+            if (ratio >= 1.4)
+            {
+                new StripCollageBuilder(_appPaths, _fileSystem).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
+            }
+            else if (ratio >= .9)
+            {
+                new StripCollageBuilder(_appPaths, _fileSystem).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
+            }
+            else
+            {
+                new StripCollageBuilder(_appPaths, _fileSystem).BuildPosterCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
+            }
+        }
+
+        private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options)
+        {
+            if (!options.AddPlayedIndicator && !options.UnplayedCount.HasValue && options.PercentPlayed.Equals(0))
+            {
+                return;
+            }
+
+            try
+            {
+                var currentImageSize = new ImageSize(imageWidth, imageHeight);
+
+                if (options.AddPlayedIndicator)
+                {
+                    var task = new PlayedIndicatorDrawer(_appPaths, _httpClientFactory(), _fileSystem).DrawPlayedIndicator(canvas, currentImageSize);
+                    Task.WaitAll(task);
+                }
+                else if (options.UnplayedCount.HasValue)
+                {
+                    new UnplayedCountIndicator(_appPaths, _httpClientFactory(), _fileSystem).DrawUnplayedCountIndicator(canvas, currentImageSize, options.UnplayedCount.Value);
+                }
+
+                if (options.PercentPlayed > 0)
+                {
+                    new PercentPlayedDrawer().Process(canvas, currentImageSize, options.PercentPlayed);
+                }
+            }
+            catch (Exception ex)
+            {
+                _logger.ErrorException("Error drawing indicator overlay", ex);
+            }
+        }
+
+        public string Name
+        {
+            get { return "Skia"; }
+        }
+
+        private bool _disposed;
+        public void Dispose()
+        {
+            _disposed = true;
+        }
+
+        private void CheckDisposed()
+        {
+            if (_disposed)
+            {
+                throw new ObjectDisposedException(GetType().Name);
+            }
+        }
+
+        public bool SupportsImageCollageCreation
+        {
+            get { return true; }
+        }
+
+        public bool SupportsImageEncoding
+        {
+            get { return true; }
+        }
+    }
+}

+ 245 - 0
Emby.Drawing.Skia/StripCollageBuilder.cs

@@ -0,0 +1,245 @@
+using SkiaSharp;
+using MediaBrowser.Common.Configuration;
+using System;
+using System.IO;
+using System.Collections.Generic;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Drawing.Skia
+{
+    public class StripCollageBuilder
+    {
+        private readonly IApplicationPaths _appPaths;
+        private readonly IFileSystem _fileSystem;
+
+        public StripCollageBuilder(IApplicationPaths appPaths, IFileSystem fileSystem)
+        {
+            _appPaths = appPaths;
+            _fileSystem = fileSystem;
+        }
+
+        private SKEncodedImageFormat GetEncodedFormat(string outputPath)
+        {
+            var ext = Path.GetExtension(outputPath).ToLower();
+
+            if (ext == ".jpg" || ext == ".jpeg")
+                return SKEncodedImageFormat.Jpeg;
+
+            if (ext == ".webp")
+                return SKEncodedImageFormat.Webp;
+
+            if (ext == ".gif")
+                return SKEncodedImageFormat.Gif;
+
+            if (ext == ".bmp")
+                return SKEncodedImageFormat.Bmp;
+
+            // default to png
+            return SKEncodedImageFormat.Png;
+        }
+
+        public void BuildPosterCollage(string[] paths, string outputPath, int width, int height)
+        {
+            using (var bitmap = BuildPosterCollageBitmap(paths, width, height))
+            {
+                using (var outputStream = new SKFileWStream(outputPath))
+                {
+                    bitmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
+                }
+            }
+        }
+
+        public void BuildSquareCollage(string[] paths, string outputPath, int width, int height)
+        {
+            using (var bitmap = BuildSquareCollageBitmap(paths, width, height))
+            {
+                using (var outputStream = new SKFileWStream(outputPath))
+                {
+                    bitmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
+                }
+            }
+        }
+
+        public void BuildThumbCollage(string[] paths, string outputPath, int width, int height)
+        {
+            using (var bitmap = BuildThumbCollageBitmap(paths, width, height))
+            {
+                using (var outputStream = new SKFileWStream(outputPath))
+                {
+                    bitmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
+                }
+            }
+        }
+
+        private SKBitmap BuildPosterCollageBitmap(string[] paths, int width, int height)
+        {
+            return null;
+           /* var inputPaths = ImageHelpers.ProjectPaths(paths, 3);
+            using (var wandImages = new MagickWand(inputPaths.ToArray()))
+            {
+                var wand = new MagickWand(width, height);
+                wand.OpenImage("gradient:#111111-#111111");
+                using (var draw = new DrawingWand())
+                {
+                    var iSlice = Convert.ToInt32(width * 0.3);
+                    int iTrans = Convert.ToInt32(height * .25);
+                    int iHeight = Convert.ToInt32(height * .65);
+                    var horizontalImagePadding = Convert.ToInt32(width * 0.0366);
+
+                    foreach (var element in wandImages.ImageList)
+                    {
+                        using (var blackPixelWand = new PixelWand(ColorName.Black))
+                        {
+                            int iWidth = (int)Math.Abs(iHeight * element.Width / element.Height);
+                            element.Gravity = GravityType.CenterGravity;
+                            element.BackgroundColor = blackPixelWand;
+                            element.ResizeImage(iWidth, iHeight, FilterTypes.LanczosFilter);
+                            int ix = (int)Math.Abs((iWidth - iSlice) / 2);
+                            element.CropImage(iSlice, iHeight, ix, 0);
+
+                            element.ExtentImage(iSlice, iHeight, 0 - horizontalImagePadding, 0);
+                        }
+                    }
+
+                    wandImages.SetFirstIterator();
+                    using (var wandList = wandImages.AppendImages())
+                    {
+                        wandList.CurrentImage.TrimImage(1);
+                        using (var mwr = wandList.CloneMagickWand())
+                        {
+                            using (var blackPixelWand = new PixelWand(ColorName.Black))
+                            {
+                                using (var greyPixelWand = new PixelWand(ColorName.Grey70))
+                                {
+                                    mwr.CurrentImage.ResizeImage(wandList.CurrentImage.Width, (wandList.CurrentImage.Height / 2), FilterTypes.LanczosFilter, 1);
+                                    mwr.CurrentImage.FlipImage();
+
+                                    mwr.CurrentImage.AlphaChannel = AlphaChannelType.DeactivateAlphaChannel;
+                                    mwr.CurrentImage.ColorizeImage(blackPixelWand, greyPixelWand);
+
+                                    using (var mwg = new MagickWand(wandList.CurrentImage.Width, iTrans))
+                                    {
+                                        mwg.OpenImage("gradient:black-none");
+                                        var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111);
+                                        mwr.CurrentImage.CompositeImage(mwg, CompositeOperator.CopyOpacityCompositeOp, 0, verticalSpacing);
+
+                                        wandList.AddImage(mwr);
+                                        int ex = (int)(wand.CurrentImage.Width - mwg.CurrentImage.Width) / 2;
+                                        wand.CurrentImage.CompositeImage(wandList.AppendImages(true), CompositeOperator.AtopCompositeOp, ex, Convert.ToInt32(height * .05));
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+
+                return wand;
+            }*/
+        }
+
+        private SKBitmap BuildThumbCollageBitmap(string[] paths, int width, int height)
+        {
+            return null;
+            /*var inputPaths = ImageHelpers.ProjectPaths(paths, 4);
+            using (var wandImages = new MagickWand(inputPaths.ToArray()))
+            {
+                var wand = new MagickWand(width, height);
+                wand.OpenImage("gradient:#111111-#111111");
+                using (var draw = new DrawingWand())
+                {
+                    var iSlice = Convert.ToInt32(width * 0.24125);
+                    int iTrans = Convert.ToInt32(height * .25);
+                    int iHeight = Convert.ToInt32(height * .70);
+                    var horizontalImagePadding = Convert.ToInt32(width * 0.0125);
+
+                    foreach (var element in wandImages.ImageList)
+                    {
+                        using (var blackPixelWand = new PixelWand(ColorName.Black))
+                        {
+                            int iWidth = (int)Math.Abs(iHeight * element.Width / element.Height);
+                            element.Gravity = GravityType.CenterGravity;
+                            element.BackgroundColor = blackPixelWand;
+                            element.ResizeImage(iWidth, iHeight, FilterTypes.LanczosFilter);
+                            int ix = (int)Math.Abs((iWidth - iSlice) / 2);
+                            element.CropImage(iSlice, iHeight, ix, 0);
+
+                            element.ExtentImage(iSlice, iHeight, 0 - horizontalImagePadding, 0);
+                        }
+                    }
+
+                    wandImages.SetFirstIterator();
+                    using (var wandList = wandImages.AppendImages())
+                    {
+                        wandList.CurrentImage.TrimImage(1);
+                        using (var mwr = wandList.CloneMagickWand())
+                        {
+                            using (var blackPixelWand = new PixelWand(ColorName.Black))
+                            {
+                                using (var greyPixelWand = new PixelWand(ColorName.Grey70))
+                                {
+                                    mwr.CurrentImage.ResizeImage(wandList.CurrentImage.Width, (wandList.CurrentImage.Height / 2), FilterTypes.LanczosFilter, 1);
+                                    mwr.CurrentImage.FlipImage();
+
+                                    mwr.CurrentImage.AlphaChannel = AlphaChannelType.DeactivateAlphaChannel;
+                                    mwr.CurrentImage.ColorizeImage(blackPixelWand, greyPixelWand);
+
+                                    using (var mwg = new MagickWand(wandList.CurrentImage.Width, iTrans))
+                                    {
+                                        mwg.OpenImage("gradient:black-none");
+                                        var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111);
+                                        mwr.CurrentImage.CompositeImage(mwg, CompositeOperator.CopyOpacityCompositeOp, 0, verticalSpacing);
+
+                                        wandList.AddImage(mwr);
+                                        int ex = (int)(wand.CurrentImage.Width - mwg.CurrentImage.Width) / 2;
+                                        wand.CurrentImage.CompositeImage(wandList.AppendImages(true), CompositeOperator.AtopCompositeOp, ex, Convert.ToInt32(height * .045));
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+
+                return wand;
+            }*/
+        }
+
+        private SKBitmap BuildSquareCollageBitmap(string[] paths, int width, int height)
+        {
+            var bitmap = new SKBitmap(width, height);
+            var imageIndex = 0;
+            var cellWidth = width / 2;
+            var cellHeight = height / 2;
+
+            using (var canvas = new SKCanvas(bitmap))
+            {
+                for (var x = 0; x < 2; x++)
+                {
+                    for (var y = 0; y < 2; y++)
+                    {
+                        using (var currentBitmap = SKBitmap.Decode(paths[imageIndex]))
+                        {
+                            using (var resizedBitmap = new SKBitmap(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType))
+                            {
+                                // scale image
+                                currentBitmap.Resize(resizedBitmap, SKBitmapResizeMethod.Lanczos3);
+                        
+                                // draw this image into the strip at the next position
+                                var xPos = x * cellWidth;
+                                var yPos = y * cellHeight;
+                                canvas.DrawBitmap(resizedBitmap, xPos, yPos);
+                            }
+                        }
+                        imageIndex++;
+
+                        if (imageIndex >= paths.Length)
+                            imageIndex = 0;
+                    }
+                }
+            }
+
+            return bitmap;
+        }
+    }
+}

+ 68 - 0
Emby.Drawing.Skia/UnplayedCountIndicator.cs

@@ -0,0 +1,68 @@
+using SkiaSharp;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Model.Drawing;
+using System.Globalization;
+using System.Threading.Tasks;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Drawing.Skia
+{
+    public class UnplayedCountIndicator
+    {
+        private const int OffsetFromTopRightCorner = 38;
+
+        private readonly IApplicationPaths _appPaths;
+        private readonly IHttpClient _iHttpClient;
+        private readonly IFileSystem _fileSystem;
+
+        public UnplayedCountIndicator(IApplicationPaths appPaths, IHttpClient iHttpClient, IFileSystem fileSystem)
+        {
+            _appPaths = appPaths;
+            _iHttpClient = iHttpClient;
+            _fileSystem = fileSystem;
+        }
+
+        public void DrawUnplayedCountIndicator(SKCanvas canvas, ImageSize imageSize, int count)
+        {
+            var x = imageSize.Width - OffsetFromTopRightCorner;
+            var text = count.ToString(CultureInfo.InvariantCulture);
+
+            using (var paint = new SKPaint())
+            {
+                paint.Color = SKColor.Parse("#CC52B54B");
+                paint.Style = SKPaintStyle.Fill;
+                canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint);
+            }
+            using (var paint = new SKPaint())
+            {
+                paint.Color = new SKColor(255, 255, 255, 255);
+                paint.Style = SKPaintStyle.Fill;
+                paint.Typeface = SKTypeface.FromFile(PlayedIndicatorDrawer.ExtractFont("robotoregular.ttf", _appPaths, _fileSystem));
+                paint.TextSize = 24;
+                paint.IsAntialias = true;
+
+                var y = OffsetFromTopRightCorner + 9;
+
+                if (text.Length == 1)
+                {
+                    x -= 7;
+                }
+                if (text.Length == 2)
+                {
+                    x -= 13;
+                }
+                else if (text.Length >= 3)
+                {
+                    x -= 15;
+                    y -= 2;
+                    paint.TextSize = 18;
+                }
+
+                canvas.DrawText(text, (float)x, y, paint);
+            }
+        }
+    }
+}

+ 2 - 0
MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj

@@ -191,9 +191,11 @@
   <ItemGroup>
     <Content Include="..\packages\SkiaSharp.1.57.1\runtimes\win7-x64\native\libSkiaSharp.dll">
       <Link>x64\libSkiaSharp.dll</Link>
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     <Content Include="..\packages\SkiaSharp.1.57.1\runtimes\win7-x86\native\libSkiaSharp.dll">
       <Link>x86\libSkiaSharp.dll</Link>
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     <Content Include="..\Tools\Installation\MediaBrowser.InstallUtil.dll">
       <Link>MediaBrowser.InstallUtil.dll</Link>