Merge pull request #2628 from MediaBrowser/dev

Dev
This commit is contained in:
Luke 2017-05-10 15:15:14 -04:00 committed by GitHub
commit 6ee9da3717
30 changed files with 862 additions and 100 deletions

View File

@ -53,6 +53,11 @@ namespace Emby.Common.Implementations.IO
if (separator == '/') if (separator == '/')
{ {
result = result.Replace('\\', '/'); result = result.Replace('\\', '/');
if (result.StartsWith("smb:/", StringComparison.OrdinalIgnoreCase) && !result.StartsWith("smb://", StringComparison.OrdinalIgnoreCase))
{
result = result.Replace("smb:/", "smb://");
}
} }
return result; return result;

View File

@ -105,17 +105,6 @@ namespace Emby.Drawing.ImageMagick
} }
} }
public void CropWhiteSpace(string inputPath, string outputPath)
{
CheckDisposed();
using (var wand = new MagickWand(inputPath))
{
wand.CurrentImage.TrimImage(10);
wand.SaveImage(outputPath);
}
}
public ImageSize GetImageSize(string path) public ImageSize GetImageSize(string path)
{ {
CheckDisposed(); CheckDisposed();
@ -150,6 +139,11 @@ namespace Emby.Drawing.ImageMagick
{ {
using (var originalImage = new MagickWand(inputPath)) using (var originalImage = new MagickWand(inputPath))
{ {
if (options.CropWhiteSpace)
{
originalImage.CurrentImage.TrimImage(10);
}
ScaleImage(originalImage, width, height, options.Blur ?? 0); ScaleImage(originalImage, width, height, options.Blur ?? 0);
if (autoOrient) if (autoOrient)

View File

@ -75,27 +75,24 @@ namespace Emby.Drawing.Net
} }
} }
public void CropWhiteSpace(string inputPath, string outputPath) private Image GetImage(string path, bool cropWhitespace)
{ {
using (var image = (Bitmap)Image.FromFile(inputPath)) if (cropWhitespace)
{ {
using (var croppedImage = image.CropWhitespace()) using (var originalImage = (Bitmap)Image.FromFile(path))
{ {
_fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(outputPath)); return originalImage.CropWhitespace();
using (var outputStream = _fileSystem.GetFileStream(outputPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, false))
{
croppedImage.Save(System.Drawing.Imaging.ImageFormat.Png, outputStream, 100);
}
} }
} }
return Image.FromFile(path);
} }
public void EncodeImage(string inputPath, string cacheFilePath, bool autoOrient, int width, int height, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat) public void EncodeImage(string inputPath, string cacheFilePath, bool autoOrient, int width, int height, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat)
{ {
var hasPostProcessing = !string.IsNullOrEmpty(options.BackgroundColor) || options.UnplayedCount.HasValue || options.AddPlayedIndicator || options.PercentPlayed > 0; var hasPostProcessing = !string.IsNullOrEmpty(options.BackgroundColor) || options.UnplayedCount.HasValue || options.AddPlayedIndicator || options.PercentPlayed > 0;
using (var originalImage = Image.FromFile(inputPath)) using (var originalImage = GetImage(inputPath, options.CropWhiteSpace))
{ {
var newWidth = Convert.ToInt32(width); var newWidth = Convert.ToInt32(width);
var newHeight = Convert.ToInt32(height); var newHeight = Convert.ToInt32(height);

View File

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

View File

@ -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);
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,380 @@
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()
{
using (var bitmap = new SKBitmap())
{
return typeof(SKBitmap).GetTypeInfo().Assembly.GetName().Version.ToString();
}
}
private static bool IsWhiteSpace(SKColor color)
{
return (color.Red == 255 && color.Green == 255 && color.Blue == 255) || color.Alpha == 0;
}
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;
default:
return SKEncodedImageFormat.Png;
}
}
private static bool IsAllWhiteRow(SKBitmap bmp, int row)
{
for (var i = 0; i < bmp.Width; ++i)
{
if (!IsWhiteSpace(bmp.GetPixel(i, row)))
{
return false;
}
}
return true;
}
private static bool IsAllWhiteColumn(SKBitmap bmp, int col)
{
for (var i = 0; i < bmp.Height; ++i)
{
if (!IsWhiteSpace(bmp.GetPixel(col, i)))
{
return false;
}
}
return true;
}
private SKBitmap CropWhiteSpace(SKBitmap bitmap)
{
CheckDisposed();
var topmost = 0;
for (int row = 0; row < bitmap.Height; ++row)
{
if (IsAllWhiteRow(bitmap, row))
topmost = row;
else break;
}
int bottommost = 0;
for (int row = bitmap.Height - 1; row >= 0; --row)
{
if (IsAllWhiteRow(bitmap, row))
bottommost = row;
else break;
}
int leftmost = 0, rightmost = 0;
for (int col = 0; col < bitmap.Width; ++col)
{
if (IsAllWhiteColumn(bitmap, col))
leftmost = col;
else
break;
}
for (int col = bitmap.Width - 1; col >= 0; --col)
{
if (IsAllWhiteColumn(bitmap, col))
rightmost = col;
else
break;
}
var newRect = SKRectI.Create(leftmost, topmost, rightmost - leftmost, bottommost - topmost);
using (var image = SKImage.FromBitmap(bitmap))
{
using (var subset = image.Subset(newRect))
{
return SKBitmap.FromImage(subset);
//using (var data = subset.Encode(StripCollageBuilder.GetEncodedFormat(outputPath), 90))
//{
// using (var fileStream = _fileSystem.GetFileStream(outputPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
// {
// data.AsStream().CopyTo(fileStream);
// }
//}
}
}
}
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
};
}
}
}
private SKBitmap GetBitmap(string path, bool cropWhitespace)
{
if (cropWhitespace)
{
using (var bitmap = SKBitmap.Decode(path))
{
return CropWhiteSpace(bitmap);
}
}
return SKBitmap.Decode(path);
}
public void EncodeImage(string inputPath, string outputPath, bool autoOrient, int width, int height, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat)
{
if (string.IsNullOrWhiteSpace(inputPath))
{
throw new ArgumentNullException("inputPath");
}
if (string.IsNullOrWhiteSpace(inputPath))
{
throw new ArgumentNullException("outputPath");
}
var skiaOutputFormat = GetImageFormat(selectedOutputFormat);
var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);
var hasForegroundColor = !string.IsNullOrWhiteSpace(options.ForegroundLayer);
var blur = options.Blur ?? 0;
var hasIndicator = !options.AddPlayedIndicator && !options.UnplayedCount.HasValue && options.PercentPlayed.Equals(0);
using (var bitmap = GetBitmap(inputPath, options.CropWhiteSpace))
{
using (var resizedBitmap = new SKBitmap(width, height, bitmap.ColorType, bitmap.AlphaType))
{
// scale image
var resizeMethod = options.Image.Type == MediaBrowser.Model.Entities.ImageType.Logo ||
options.Image.Type == MediaBrowser.Model.Entities.ImageType.Art
? SKBitmapResizeMethod.Lanczos3
: SKBitmapResizeMethod.Lanczos3;
bitmap.Resize(resizedBitmap, resizeMethod);
// If all we're doing is resizing then we can stop now
if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
{
using (var outputStream = new SKFileWStream(outputPath))
{
resizedBitmap.Encode(outputStream, skiaOutputFormat, quality);
return;
}
}
// 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 (hasBackgroundColor)
{
canvas.Clear(SKColor.Parse(options.BackgroundColor));
}
// Add blur if option is present
if (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 (hasForegroundColor)
{
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);
}
if (hasIndicator)
{
DrawIndicator(canvas, width, height, options);
}
using (var outputStream = new SKFileWStream(outputPath))
{
saveBitmap.Encode(outputStream, skiaOutputFormat, 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
{
// @todo create Poster collage capability
new StripCollageBuilder(_appPaths, _fileSystem).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
}
}
private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options)
{
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; }
}
}
}

View File

@ -0,0 +1,164 @@
using SkiaSharp;
using MediaBrowser.Common.Configuration;
using System;
using System.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;
}
public static 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)
{
// @todo
}
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 BuildThumbCollageBitmap(string[] paths, int width, int height)
{
var bitmap = new SKBitmap(width, height);
using (var canvas = new SKCanvas(bitmap))
{
canvas.Clear(SKColors.Black);
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);
var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111);
int imageIndex = 0;
for (int i = 0; i < 4; i++)
{
using (var currentBitmap = SKBitmap.Decode(paths[imageIndex]))
{
int iWidth = (int)Math.Abs(iHeight * currentBitmap.Width / currentBitmap.Height);
using (var resizeBitmap = new SKBitmap(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType))
{
currentBitmap.Resize(resizeBitmap, SKBitmapResizeMethod.Lanczos3);
int ix = (int)Math.Abs((iWidth - iSlice) / 2);
using (var image = SKImage.FromBitmap(resizeBitmap))
{
using (var subset = image.Subset(SKRectI.Create(ix, 0, iSlice, iHeight)))
{
canvas.DrawImage(subset, (horizontalImagePadding * (i + 1)) + (iSlice * i), 0);
using (var croppedBitmap = SKBitmap.FromImage(subset))
{
using (var flipped = new SKBitmap(croppedBitmap.Width, croppedBitmap.Height / 2, croppedBitmap.ColorType, croppedBitmap.AlphaType))
{
croppedBitmap.Resize(flipped, SKBitmapResizeMethod.Lanczos3);
using (var gradient = new SKPaint())
{
var matrix = SKMatrix.MakeScale(1, -1);
matrix.SetScaleTranslate(1, -1, 0, flipped.Height);
gradient.Shader = SKShader.CreateLinearGradient(new SKPoint(0, 0), new SKPoint(0, flipped.Height), new[] { new SKColor(0, 0, 0, 0), SKColors.Black }, null, SKShaderTileMode.Clamp, matrix);
canvas.DrawBitmap(flipped, (horizontalImagePadding * (i + 1)) + (iSlice * i), iHeight + verticalSpacing, gradient);
}
}
}
}
}
}
}
imageIndex++;
if (imageIndex >= paths.Length)
imageIndex = 0;
}
}
return bitmap;
}
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;
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -136,14 +136,6 @@ namespace Emby.Drawing
} }
} }
private string CroppedWhitespaceImageCachePath
{
get
{
return Path.Combine(_appPaths.ImageCachePath, "cropped-images");
}
}
public void AddParts(IEnumerable<IImageEnhancer> enhancers) public void AddParts(IEnumerable<IImageEnhancer> enhancers)
{ {
ImageEnhancers = enhancers.ToArray(); ImageEnhancers = enhancers.ToArray();
@ -186,14 +178,6 @@ namespace Emby.Drawing
return new Tuple<string, string, DateTime>(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); return new Tuple<string, string, DateTime>(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
} }
if (options.CropWhiteSpace && _imageEncoder.SupportsImageEncoding)
{
var tuple = await GetWhitespaceCroppedImage(originalImagePath, dateModified).ConfigureAwait(false);
originalImagePath = tuple.Item1;
dateModified = tuple.Item2;
}
if (options.Enhancers.Count > 0) if (options.Enhancers.Count > 0)
{ {
var tuple = await GetEnhancedImage(new ItemImageInfo var tuple = await GetEnhancedImage(new ItemImageInfo
@ -400,46 +384,6 @@ namespace Emby.Drawing
return requestedFormat; return requestedFormat;
} }
/// <summary>
/// Crops whitespace from an image, caches the result, and returns the cached path
/// </summary>
private async Task<Tuple<string, DateTime>> GetWhitespaceCroppedImage(string originalImagePath, DateTime dateModified)
{
var name = originalImagePath;
name += "datemodified=" + dateModified.Ticks;
var croppedImagePath = GetCachePath(CroppedWhitespaceImageCachePath, name, Path.GetExtension(originalImagePath));
// Check again in case of contention
if (_fileSystem.FileExists(croppedImagePath))
{
return GetResult(croppedImagePath);
}
try
{
_fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(croppedImagePath));
var tmpPath = Path.ChangeExtension(Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString("N")), Path.GetExtension(croppedImagePath));
_fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(tmpPath));
_imageEncoder.CropWhiteSpace(originalImagePath, tmpPath);
CopyFile(tmpPath, croppedImagePath);
return GetResult(tmpPath);
}
catch (NotImplementedException)
{
// No need to spam the log with an error message
return new Tuple<string, DateTime>(originalImagePath, dateModified);
}
catch (Exception ex)
{
// We have to have a catch-all here because some of the .net image methods throw a plain old Exception
_logger.ErrorException("Error cropping image {0}", ex, originalImagePath);
return new Tuple<string, DateTime>(originalImagePath, dateModified);
}
}
private Tuple<string, DateTime> GetResult(string path) private Tuple<string, DateTime> GetResult(string path)
{ {
return new Tuple<string, DateTime>(path, _fileSystem.GetLastWriteTimeUtc(path)); return new Tuple<string, DateTime>(path, _fileSystem.GetLastWriteTimeUtc(path));

View File

@ -761,7 +761,10 @@ namespace Emby.Server.Core
return null; return null;
} }
X509Certificate2 localCert = new X509Certificate2(certificateLocation, info.Password); // Don't use an empty string password
var password = string.IsNullOrWhiteSpace(info.Password) ? null : info.Password;
X509Certificate2 localCert = new X509Certificate2(certificateLocation, password);
//localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA; //localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA;
if (!localCert.HasPrivateKey) if (!localCert.HasPrivateKey)
{ {

View File

@ -126,7 +126,7 @@ namespace Emby.Server.Implementations.AppBase
Logger.Info("Saving system configuration"); Logger.Info("Saving system configuration");
var path = CommonApplicationPaths.SystemConfigurationFilePath; var path = CommonApplicationPaths.SystemConfigurationFilePath;
FileSystem.CreateDirectory(Path.GetDirectoryName(path)); FileSystem.CreateDirectory(FileSystem.GetDirectoryName(path));
lock (_configurationSyncLock) lock (_configurationSyncLock)
{ {
@ -293,7 +293,7 @@ namespace Emby.Server.Implementations.AppBase
_configurations.AddOrUpdate(key, configuration, (k, v) => configuration); _configurations.AddOrUpdate(key, configuration, (k, v) => configuration);
var path = GetConfigurationFile(key); var path = GetConfigurationFile(key);
FileSystem.CreateDirectory(Path.GetDirectoryName(path)); FileSystem.CreateDirectory(FileSystem.GetDirectoryName(path));
lock (_configurationSyncLock) lock (_configurationSyncLock)
{ {

View File

@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.AppBase
// If the file didn't exist before, or if something has changed, re-save // If the file didn't exist before, or if something has changed, re-save
if (buffer == null || !buffer.SequenceEqual(newBytes)) if (buffer == null || !buffer.SequenceEqual(newBytes))
{ {
fileSystem.CreateDirectory(Path.GetDirectoryName(path)); fileSystem.CreateDirectory(fileSystem.GetDirectoryName(path));
// Save it after load in case we got new items // Save it after load in case we got new items
fileSystem.WriteAllBytes(path, newBytes); fileSystem.WriteAllBytes(path, newBytes);

View File

@ -158,7 +158,7 @@ namespace Emby.Server.Implementations.Devices
_libraryMonitor.ReportFileSystemChangeBeginning(path); _libraryMonitor.ReportFileSystemChangeBeginning(path);
_fileSystem.CreateDirectory(Path.GetDirectoryName(path)); _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path));
try try
{ {

View File

@ -46,7 +46,7 @@ namespace Emby.Server.Implementations.Devices
public Task SaveDevice(DeviceInfo device) public Task SaveDevice(DeviceInfo device)
{ {
var path = Path.Combine(GetDevicePath(device.Id), "device.json"); var path = Path.Combine(GetDevicePath(device.Id), "device.json");
_fileSystem.CreateDirectory(Path.GetDirectoryName(path)); _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path));
lock (_syncLock) lock (_syncLock)
{ {
@ -180,7 +180,7 @@ namespace Emby.Server.Implementations.Devices
public void AddCameraUpload(string deviceId, LocalFileInfo file) public void AddCameraUpload(string deviceId, LocalFileInfo file)
{ {
var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json"); var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json");
_fileSystem.CreateDirectory(Path.GetDirectoryName(path)); _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path));
lock (_syncLock) lock (_syncLock)
{ {

View File

@ -97,7 +97,7 @@ namespace Emby.Server.Implementations.FFMpeg
else else
{ {
info = existingVersion; info = existingVersion;
versionedDirectoryPath = Path.GetDirectoryName(info.EncoderPath); versionedDirectoryPath = _fileSystem.GetDirectoryName(info.EncoderPath);
excludeFromDeletions.Add(versionedDirectoryPath); excludeFromDeletions.Add(versionedDirectoryPath);
} }
} }
@ -135,7 +135,7 @@ namespace Emby.Server.Implementations.FFMpeg
{ {
EncoderPath = encoder, EncoderPath = encoder,
ProbePath = probe, ProbePath = probe,
Version = Path.GetFileName(Path.GetDirectoryName(probe)) Version = Path.GetFileName(_fileSystem.GetDirectoryName(probe))
}; };
} }
} }

View File

@ -1197,6 +1197,7 @@ namespace Emby.Server.Implementations.Library
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
_logger.Info("Post-scan task cancelled: {0}", task.GetType().Name); _logger.Info("Post-scan task cancelled: {0}", task.GetType().Name);
throw;
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -63,7 +63,7 @@ namespace Emby.Server.Implementations.Library.Validators
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
// Don't clutter the log // Don't clutter the log
break; throw;
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -53,7 +53,7 @@ namespace Emby.Server.Implementations.Library.Validators
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
// Don't clutter the log // Don't clutter the log
break; throw;
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -54,7 +54,7 @@ namespace Emby.Server.Implementations.Library.Validators
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
// Don't clutter the log // Don't clutter the log
break; throw;
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -54,7 +54,7 @@ namespace Emby.Server.Implementations.Library.Validators
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
// Don't clutter the log // Don't clutter the log
break; throw;
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -53,7 +53,7 @@ namespace Emby.Server.Implementations.Library.Validators
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
// Don't clutter the log // Don't clutter the log
break; throw;
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -26,6 +26,8 @@ namespace Emby.Server.Implementations.Library.Validators
while (yearNumber < maxYear) while (yearNumber < maxYear)
{ {
cancellationToken.ThrowIfCancellationRequested();
try try
{ {
var year = _libraryManager.GetYear(yearNumber); var year = _libraryManager.GetYear(yearNumber);
@ -35,7 +37,7 @@ namespace Emby.Server.Implementations.Library.Validators
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
// Don't clutter the log // Don't clutter the log
break; throw;
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -16,12 +16,6 @@ namespace MediaBrowser.Controller.Drawing
/// <value>The supported output formats.</value> /// <value>The supported output formats.</value>
ImageFormat[] SupportedOutputFormats { get; } ImageFormat[] SupportedOutputFormats { get; }
/// <summary> /// <summary>
/// Crops the white space.
/// </summary>
/// <param name="inputPath">The input path.</param>
/// <param name="outputPath">The output path.</param>
void CropWhiteSpace(string inputPath, string outputPath);
/// <summary>
/// Encodes the image. /// Encodes the image.
/// </summary> /// </summary>
/// <param name="inputPath">The input path.</param> /// <param name="inputPath">The input path.</param>

View File

@ -86,6 +86,7 @@ namespace MediaBrowser.Controller.Drawing
PercentPlayed.Equals(0) && PercentPlayed.Equals(0) &&
!UnplayedCount.HasValue && !UnplayedCount.HasValue &&
!Blur.HasValue && !Blur.HasValue &&
!CropWhiteSpace &&
string.IsNullOrEmpty(BackgroundColor) && string.IsNullOrEmpty(BackgroundColor) &&
string.IsNullOrEmpty(ForegroundLayer); string.IsNullOrEmpty(ForegroundLayer);
} }

View File

@ -2,6 +2,7 @@
using Emby.Drawing; using Emby.Drawing;
using Emby.Drawing.Net; using Emby.Drawing.Net;
using Emby.Drawing.ImageMagick; using Emby.Drawing.ImageMagick;
using Emby.Drawing.Skia;
using Emby.Server.Core; using Emby.Server.Core;
using Emby.Server.Implementations; using Emby.Server.Implementations;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
@ -23,6 +24,15 @@ namespace MediaBrowser.Server.Startup.Common
{ {
if (!startupOptions.ContainsOption("-enablegdi")) if (!startupOptions.ContainsOption("-enablegdi"))
{ {
try
{
//return new SkiaEncoder(logManager.GetLogger("ImageMagick"), appPaths, httpClient, fileSystem);
}
catch
{
logger.Error("Error loading ImageMagick. Will revert to GDI.");
}
try try
{ {
return new ImageMagickEncoder(logManager.GetLogger("ImageMagick"), appPaths, httpClient, fileSystem); return new ImageMagickEncoder(logManager.GetLogger("ImageMagick"), appPaths, httpClient, fileSystem);

View File

@ -191,9 +191,11 @@
<ItemGroup> <ItemGroup>
<Content Include="..\packages\SkiaSharp.1.57.1\runtimes\win7-x64\native\libSkiaSharp.dll"> <Content Include="..\packages\SkiaSharp.1.57.1\runtimes\win7-x64\native\libSkiaSharp.dll">
<Link>x64\libSkiaSharp.dll</Link> <Link>x64\libSkiaSharp.dll</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="..\packages\SkiaSharp.1.57.1\runtimes\win7-x86\native\libSkiaSharp.dll"> <Content Include="..\packages\SkiaSharp.1.57.1\runtimes\win7-x86\native\libSkiaSharp.dll">
<Link>x86\libSkiaSharp.dll</Link> <Link>x86\libSkiaSharp.dll</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="..\Tools\Installation\MediaBrowser.InstallUtil.dll"> <Content Include="..\Tools\Installation\MediaBrowser.InstallUtil.dll">
<Link>MediaBrowser.InstallUtil.dll</Link> <Link>MediaBrowser.InstallUtil.dll</Link>
@ -1110,6 +1112,10 @@
<Project>{c97a239e-a96c-4d64-a844-ccf8cc30aecb}</Project> <Project>{c97a239e-a96c-4d64-a844-ccf8cc30aecb}</Project>
<Name>Emby.Drawing.Net</Name> <Name>Emby.Drawing.Net</Name>
</ProjectReference> </ProjectReference>
<ProjectReference Include="..\Emby.Drawing.Skia\Emby.Drawing.Skia.csproj">
<Project>{2312da6d-ff86-4597-9777-bceec32d96dd}</Project>
<Name>Emby.Drawing.Skia</Name>
</ProjectReference>
<ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj"> <ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj">
<Project>{08fff49b-f175-4807-a2b5-73b0ebd9f716}</Project> <Project>{08fff49b-f175-4807-a2b5-73b0ebd9f716}</Project>
<Name>Emby.Drawing</Name> <Name>Emby.Drawing</Name>

View File

@ -15,6 +15,7 @@ using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Xml; using System.Xml;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Xml; using MediaBrowser.Model.Xml;
@ -227,6 +228,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers
} }
} }
protected virtual string MovieDbParserSearchString
{
get { return "themoviedb.org/movie/"; }
}
private void ParseProviderLinks(T item, string xml) private void ParseProviderLinks(T item, string xml)
{ {
//Look for a match for the Regex pattern "tt" followed by 7 digits //Look for a match for the Regex pattern "tt" followed by 7 digits
@ -238,7 +244,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
// Support Tmdb // Support Tmdb
// http://www.themoviedb.org/movie/36557 // http://www.themoviedb.org/movie/36557
var srch = "themoviedb.org/movie/"; var srch = MovieDbParserSearchString;
var index = xml.IndexOf(srch, StringComparison.OrdinalIgnoreCase); var index = xml.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
if (index != -1) if (index != -1)
@ -250,6 +256,23 @@ namespace MediaBrowser.XbmcMetadata.Parsers
item.SetProviderId(MetadataProviders.Tmdb, tmdbId); item.SetProviderId(MetadataProviders.Tmdb, tmdbId);
} }
} }
if (item is Series)
{
srch = "thetvdb.com/?tab=series&id=";
index = xml.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
if (index != -1)
{
var tvdbId = xml.Substring(index + srch.Length).TrimEnd('/');
int value;
if (!string.IsNullOrWhiteSpace(tvdbId) && int.TryParse(tvdbId, NumberStyles.Any, CultureInfo.InvariantCulture, out value))
{
item.SetProviderId(MetadataProviders.Tvdb, tvdbId);
}
}
}
} }
protected virtual void FetchDataFromXmlNode(XmlReader reader, MetadataResult<T> itemResult) protected virtual void FetchDataFromXmlNode(XmlReader reader, MetadataResult<T> itemResult)

View File

@ -13,6 +13,19 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{ {
public class SeriesNfoParser : BaseNfoParser<Series> public class SeriesNfoParser : BaseNfoParser<Series>
{ {
protected override bool SupportsUrlAfterClosingXmlTag
{
get
{
return true;
}
}
protected override string MovieDbParserSearchString
{
get { return "themoviedb.org/tv/"; }
}
/// <summary> /// <summary>
/// Fetches the data from XML node. /// Fetches the data from XML node.
/// </summary> /// </summary>

View File

@ -1,3 +1,3 @@
using System.Reflection; using System.Reflection;
[assembly: AssemblyVersion("3.2.15.2")] [assembly: AssemblyVersion("3.2.15.3")]