added drawing project

This commit is contained in:
Luke Pulverenti 2015-04-08 10:38:02 -04:00
parent 78e96917e1
commit 4820fe8097
28 changed files with 1065 additions and 342 deletions

View File

@ -6,7 +6,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
namespace MediaBrowser.Server.Implementations.Drawing namespace Emby.Drawing.Common
{ {
/// <summary> /// <summary>
/// Taken from http://stackoverflow.com/questions/111345/getting-image-dimensions-without-reading-the-entire-file/111349 /// Taken from http://stackoverflow.com/questions/111345/getting-image-dimensions-without-reading-the-entire-file/111349

View File

@ -48,22 +48,42 @@
<Compile Include="..\SharedVersion.cs"> <Compile Include="..\SharedVersion.cs">
<Link>Properties\SharedVersion.cs</Link> <Link>Properties\SharedVersion.cs</Link>
</Compile> </Compile>
<Compile Include="ImageHeader.cs" /> <Compile Include="GDI\GDIImageEncoder.cs" />
<Compile Include="GDI\ImageExtensions.cs" />
<Compile Include="GDI\PercentPlayedDrawer.cs" />
<Compile Include="GDI\PlayedIndicatorDrawer.cs" />
<Compile Include="GDI\UnplayedCountIndicator.cs" />
<Compile Include="IImageEncoder.cs" />
<Compile Include="Common\ImageHeader.cs" />
<Compile Include="ImageMagick\ImageMagickEncoder.cs" />
<Compile Include="ImageMagick\StripCollageBuilder.cs" />
<Compile Include="ImageProcessor.cs" /> <Compile Include="ImageProcessor.cs" />
<Compile Include="PercentPlayedDrawer.cs" /> <Compile Include="ImageMagick\PercentPlayedDrawer.cs" />
<Compile Include="PlayedIndicatorDrawer.cs" /> <Compile Include="ImageMagick\PlayedIndicatorDrawer.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="UnplayedCountIndicator.cs" /> <Compile Include="ImageMagick\UnplayedCountIndicator.cs" />
</ItemGroup>
<ItemGroup>
<Folder Include="GDI\" />
</ItemGroup> </ItemGroup>
<ItemGroup />
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="ImageMagick\fonts\MontserratLight.otf" /> <EmbeddedResource Include="ImageMagick\fonts\MontserratLight.otf" />
<EmbeddedResource Include="ImageMagick\fonts\robotoregular.ttf" /> <EmbeddedResource Include="ImageMagick\fonts\robotoregular.ttf" />
<EmbeddedResource Include="ImageMagick\fonts\webdings.ttf" /> <EmbeddedResource Include="ImageMagick\fonts\webdings.ttf" />
<None Include="packages.config" /> <None Include="packages.config" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
<Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project>
<Name>MediaBrowser.Common</Name>
</ProjectReference>
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj">
<Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project>
<Name>MediaBrowser.Controller</Name>
</ProjectReference>
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
<Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
<Name>MediaBrowser.Model</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets. Other similar extension points exist, see Microsoft.Common.targets.

View File

@ -0,0 +1,233 @@
using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Logging;
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using ImageFormat = MediaBrowser.Model.Drawing.ImageFormat;
namespace Emby.Drawing.GDI
{
public class GDIImageEncoder : IImageEncoder
{
private readonly IFileSystem _fileSystem;
private readonly ILogger _logger;
public GDIImageEncoder(IFileSystem fileSystem, ILogger logger)
{
_fileSystem = fileSystem;
_logger = logger;
}
public string[] SupportedInputFormats
{
get
{
return new[]
{
"png",
"jpeg",
"jpg",
"gif",
"bmp"
};
}
}
public ImageFormat[] SupportedOutputFormats
{
get
{
return new[] { ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png };
}
}
public ImageSize GetImageSize(string path)
{
using (var image = Image.FromFile(path))
{
return new ImageSize
{
Width = image.Width,
Height = image.Height
};
}
}
public void CropWhiteSpace(string inputPath, string outputPath)
{
using (var image = (Bitmap)Image.FromFile(inputPath))
{
using (var croppedImage = image.CropWhitespace())
{
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
using (var outputStream = _fileSystem.GetFileStream(outputPath, FileMode.Create, FileAccess.Write, FileShare.Read, false))
{
croppedImage.Save(System.Drawing.Imaging.ImageFormat.Png, outputStream, 100);
}
}
}
}
public void EncodeImage(string inputPath, string cacheFilePath, int width, int height, int quality, ImageProcessingOptions options)
{
var hasPostProcessing = !string.IsNullOrEmpty(options.BackgroundColor) || options.UnplayedCount.HasValue || options.AddPlayedIndicator || options.PercentPlayed > 0;
using (var originalImage = Image.FromFile(inputPath))
{
var newWidth = Convert.ToInt32(width);
var newHeight = Convert.ToInt32(height);
var selectedOutputFormat = options.OutputFormat;
// Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here
// Also, Webp only supports Format32bppArgb and Format32bppRgb
var pixelFormat = selectedOutputFormat == ImageFormat.Webp
? PixelFormat.Format32bppArgb
: PixelFormat.Format32bppPArgb;
using (var thumbnail = new Bitmap(newWidth, newHeight, pixelFormat))
{
// Mono throw an exeception if assign 0 to SetResolution
if (originalImage.HorizontalResolution > 0 && originalImage.VerticalResolution > 0)
{
// Preserve the original resolution
thumbnail.SetResolution(originalImage.HorizontalResolution, originalImage.VerticalResolution);
}
using (var thumbnailGraph = Graphics.FromImage(thumbnail))
{
thumbnailGraph.CompositingQuality = CompositingQuality.HighQuality;
thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality;
thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic;
thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality;
thumbnailGraph.CompositingMode = !hasPostProcessing ?
CompositingMode.SourceCopy :
CompositingMode.SourceOver;
SetBackgroundColor(thumbnailGraph, options);
thumbnailGraph.DrawImage(originalImage, 0, 0, newWidth, newHeight);
DrawIndicator(thumbnailGraph, newWidth, newHeight, options);
var outputFormat = GetOutputFormat(originalImage, selectedOutputFormat);
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
// Save to the cache location
using (var cacheFileStream = _fileSystem.GetFileStream(cacheFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, false))
{
// Save to the memory stream
thumbnail.Save(outputFormat, cacheFileStream, quality);
}
}
}
}
}
/// <summary>
/// Sets the color of the background.
/// </summary>
/// <param name="graphics">The graphics.</param>
/// <param name="options">The options.</param>
private void SetBackgroundColor(Graphics graphics, ImageProcessingOptions options)
{
var color = options.BackgroundColor;
if (!string.IsNullOrEmpty(color))
{
Color drawingColor;
try
{
drawingColor = ColorTranslator.FromHtml(color);
}
catch
{
drawingColor = ColorTranslator.FromHtml("#" + color);
}
graphics.Clear(drawingColor);
}
}
/// <summary>
/// Draws the indicator.
/// </summary>
/// <param name="graphics">The graphics.</param>
/// <param name="imageWidth">Width of the image.</param>
/// <param name="imageHeight">Height of the image.</param>
/// <param name="options">The options.</param>
private void DrawIndicator(Graphics graphics, int imageWidth, int imageHeight, ImageProcessingOptions options)
{
if (!options.AddPlayedIndicator && !options.UnplayedCount.HasValue && options.PercentPlayed.Equals(0))
{
return;
}
try
{
if (options.AddPlayedIndicator)
{
var currentImageSize = new Size(imageWidth, imageHeight);
new PlayedIndicatorDrawer().DrawPlayedIndicator(graphics, currentImageSize);
}
else if (options.UnplayedCount.HasValue)
{
var currentImageSize = new Size(imageWidth, imageHeight);
new UnplayedCountIndicator().DrawUnplayedCountIndicator(graphics, currentImageSize, options.UnplayedCount.Value);
}
if (options.PercentPlayed > 0)
{
var currentImageSize = new Size(imageWidth, imageHeight);
new PercentPlayedDrawer().Process(graphics, currentImageSize, options.PercentPlayed);
}
}
catch (Exception ex)
{
_logger.ErrorException("Error drawing indicator overlay", ex);
}
}
/// <summary>
/// Gets the output format.
/// </summary>
/// <param name="image">The image.</param>
/// <param name="outputFormat">The output format.</param>
/// <returns>ImageFormat.</returns>
private System.Drawing.Imaging.ImageFormat GetOutputFormat(Image image, ImageFormat outputFormat)
{
switch (outputFormat)
{
case ImageFormat.Bmp:
return System.Drawing.Imaging.ImageFormat.Bmp;
case ImageFormat.Gif:
return System.Drawing.Imaging.ImageFormat.Gif;
case ImageFormat.Jpg:
return System.Drawing.Imaging.ImageFormat.Jpeg;
case ImageFormat.Png:
return System.Drawing.Imaging.ImageFormat.Png;
default:
return image.RawFormat;
}
}
public void CreateImageCollage(ImageCollageOptions options)
{
}
public void Dispose()
{
}
}
}

View File

@ -0,0 +1,217 @@
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
namespace Emby.Drawing.GDI
{
public static class ImageExtensions
{
/// <summary>
/// Saves the image.
/// </summary>
/// <param name="outputFormat">The output format.</param>
/// <param name="image">The image.</param>
/// <param name="toStream">To stream.</param>
/// <param name="quality">The quality.</param>
public static void Save(this Image image, ImageFormat outputFormat, Stream toStream, int quality)
{
// Use special save methods for jpeg and png that will result in a much higher quality image
// All other formats use the generic Image.Save
if (ImageFormat.Jpeg.Equals(outputFormat))
{
SaveAsJpeg(image, toStream, quality);
}
else if (ImageFormat.Png.Equals(outputFormat))
{
image.Save(toStream, ImageFormat.Png);
}
else
{
image.Save(toStream, outputFormat);
}
}
/// <summary>
/// Saves the JPEG.
/// </summary>
/// <param name="image">The image.</param>
/// <param name="target">The target.</param>
/// <param name="quality">The quality.</param>
public static void SaveAsJpeg(this Image image, Stream target, int quality)
{
using (var encoderParameters = new EncoderParameters(1))
{
encoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, quality);
image.Save(target, GetImageCodecInfo("image/jpeg"), encoderParameters);
}
}
private static readonly ImageCodecInfo[] Encoders = ImageCodecInfo.GetImageEncoders();
/// <summary>
/// Gets the image codec info.
/// </summary>
/// <param name="mimeType">Type of the MIME.</param>
/// <returns>ImageCodecInfo.</returns>
private static ImageCodecInfo GetImageCodecInfo(string mimeType)
{
foreach (var encoder in Encoders)
{
if (string.Equals(encoder.MimeType, mimeType, StringComparison.OrdinalIgnoreCase))
{
return encoder;
}
}
return Encoders.Length == 0 ? null : Encoders[0];
}
/// <summary>
/// Crops an image by removing whitespace and transparency from the edges
/// </summary>
/// <param name="bmp">The BMP.</param>
/// <returns>Bitmap.</returns>
/// <exception cref="System.Exception"></exception>
public static Bitmap CropWhitespace(this Bitmap bmp)
{
var width = bmp.Width;
var height = bmp.Height;
var topmost = 0;
for (int row = 0; row < height; ++row)
{
if (IsAllWhiteRow(bmp, row, width))
topmost = row;
else break;
}
int bottommost = 0;
for (int row = height - 1; row >= 0; --row)
{
if (IsAllWhiteRow(bmp, row, width))
bottommost = row;
else break;
}
int leftmost = 0, rightmost = 0;
for (int col = 0; col < width; ++col)
{
if (IsAllWhiteColumn(bmp, col, height))
leftmost = col;
else
break;
}
for (int col = width - 1; col >= 0; --col)
{
if (IsAllWhiteColumn(bmp, col, height))
rightmost = col;
else
break;
}
if (rightmost == 0) rightmost = width; // As reached left
if (bottommost == 0) bottommost = height; // As reached top.
var croppedWidth = rightmost - leftmost;
var croppedHeight = bottommost - topmost;
if (croppedWidth == 0) // No border on left or right
{
leftmost = 0;
croppedWidth = width;
}
if (croppedHeight == 0) // No border on top or bottom
{
topmost = 0;
croppedHeight = height;
}
// Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here
var thumbnail = new Bitmap(croppedWidth, croppedHeight, PixelFormat.Format32bppPArgb);
// Preserve the original resolution
TrySetResolution(thumbnail, bmp.HorizontalResolution, bmp.VerticalResolution);
using (var thumbnailGraph = Graphics.FromImage(thumbnail))
{
thumbnailGraph.CompositingQuality = CompositingQuality.HighQuality;
thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality;
thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic;
thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality;
thumbnailGraph.CompositingMode = CompositingMode.SourceCopy;
thumbnailGraph.DrawImage(bmp,
new RectangleF(0, 0, croppedWidth, croppedHeight),
new RectangleF(leftmost, topmost, croppedWidth, croppedHeight),
GraphicsUnit.Pixel);
}
return thumbnail;
}
/// <summary>
/// Tries the set resolution.
/// </summary>
/// <param name="bmp">The BMP.</param>
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
private static void TrySetResolution(Bitmap bmp, float x, float y)
{
if (x > 0 && y > 0)
{
bmp.SetResolution(x, y);
}
}
/// <summary>
/// Determines whether or not a row of pixels is all whitespace
/// </summary>
/// <param name="bmp">The BMP.</param>
/// <param name="row">The row.</param>
/// <param name="width">The width.</param>
/// <returns><c>true</c> if [is all white row] [the specified BMP]; otherwise, <c>false</c>.</returns>
private static bool IsAllWhiteRow(Bitmap bmp, int row, int width)
{
for (var i = 0; i < width; ++i)
{
if (!IsWhiteSpace(bmp.GetPixel(i, row)))
{
return false;
}
}
return true;
}
/// <summary>
/// Determines whether or not a column of pixels is all whitespace
/// </summary>
/// <param name="bmp">The BMP.</param>
/// <param name="col">The col.</param>
/// <param name="height">The height.</param>
/// <returns><c>true</c> if [is all white column] [the specified BMP]; otherwise, <c>false</c>.</returns>
private static bool IsAllWhiteColumn(Bitmap bmp, int col, int height)
{
for (var i = 0; i < height; ++i)
{
if (!IsWhiteSpace(bmp.GetPixel(col, i)))
{
return false;
}
}
return true;
}
/// <summary>
/// Determines if a color is whitespace
/// </summary>
/// <param name="color">The color.</param>
/// <returns><c>true</c> if [is white space] [the specified color]; otherwise, <c>false</c>.</returns>
private static bool IsWhiteSpace(Color color)
{
return (color.R == 255 && color.G == 255 && color.B == 255) || color.A == 0;
}
}
}

View File

@ -0,0 +1,34 @@
using System;
using System.Drawing;
namespace Emby.Drawing.GDI
{
public class PercentPlayedDrawer
{
private const int IndicatorHeight = 8;
public void Process(Graphics graphics, Size imageSize, double percent)
{
var y = imageSize.Height - IndicatorHeight;
using (var backdroundBrush = new SolidBrush(Color.FromArgb(225, 0, 0, 0)))
{
const int innerX = 0;
var innerY = y;
var innerWidth = imageSize.Width;
var innerHeight = imageSize.Height;
graphics.FillRectangle(backdroundBrush, innerX, innerY, innerWidth, innerHeight);
using (var foregroundBrush = new SolidBrush(Color.FromArgb(82, 181, 75)))
{
double foregroundWidth = innerWidth;
foregroundWidth *= percent;
foregroundWidth /= 100;
graphics.FillRectangle(foregroundBrush, innerX, innerY, Convert.ToInt32(Math.Round(foregroundWidth)), innerHeight);
}
}
}
}
}

View File

@ -0,0 +1,32 @@
using System.Drawing;
namespace Emby.Drawing.GDI
{
public class PlayedIndicatorDrawer
{
private const int IndicatorHeight = 40;
public const int IndicatorWidth = 40;
private const int FontSize = 40;
private const int OffsetFromTopRightCorner = 10;
public void DrawPlayedIndicator(Graphics graphics, Size imageSize)
{
var x = imageSize.Width - IndicatorWidth - OffsetFromTopRightCorner;
using (var backdroundBrush = new SolidBrush(Color.FromArgb(225, 82, 181, 75)))
{
graphics.FillEllipse(backdroundBrush, x, OffsetFromTopRightCorner, IndicatorWidth, IndicatorHeight);
x = imageSize.Width - 45 - OffsetFromTopRightCorner;
using (var font = new Font("Webdings", FontSize, FontStyle.Regular, GraphicsUnit.Pixel))
{
using (var fontBrush = new SolidBrush(Color.White))
{
graphics.DrawString("a", font, fontBrush, x, OffsetFromTopRightCorner - 2);
}
}
}
}
}
}

View File

@ -0,0 +1,50 @@
using System.Drawing;
namespace Emby.Drawing.GDI
{
public class UnplayedCountIndicator
{
private const int IndicatorHeight = 41;
public const int IndicatorWidth = 41;
private const int OffsetFromTopRightCorner = 10;
public void DrawUnplayedCountIndicator(Graphics graphics, Size imageSize, int count)
{
var x = imageSize.Width - IndicatorWidth - OffsetFromTopRightCorner;
using (var backdroundBrush = new SolidBrush(Color.FromArgb(225, 82, 181, 75)))
{
graphics.FillEllipse(backdroundBrush, x, OffsetFromTopRightCorner, IndicatorWidth, IndicatorHeight);
var text = count.ToString();
x = imageSize.Width - IndicatorWidth - OffsetFromTopRightCorner;
var y = OffsetFromTopRightCorner + 6;
var fontSize = 24;
if (text.Length == 1)
{
x += 10;
}
else if (text.Length == 2)
{
x += 3;
}
else if (text.Length == 3)
{
x += 1;
y += 1;
fontSize = 20;
}
using (var font = new Font("Sans-Serif", fontSize, FontStyle.Regular, GraphicsUnit.Pixel))
{
using (var fontBrush = new SolidBrush(Color.White))
{
graphics.DrawString(text, font, fontBrush, x, y);
}
}
}
}
}
}

View File

@ -0,0 +1,48 @@
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Model.Drawing;
using System;
namespace Emby.Drawing
{
public interface IImageEncoder : IDisposable
{
/// <summary>
/// Gets the supported input formats.
/// </summary>
/// <value>The supported input formats.</value>
string[] SupportedInputFormats { get; }
/// <summary>
/// Gets the supported output formats.
/// </summary>
/// <value>The supported output formats.</value>
ImageFormat[] SupportedOutputFormats { get; }
/// <summary>
/// Gets the size of the image.
/// </summary>
/// <param name="path">The path.</param>
/// <returns>ImageSize.</returns>
ImageSize GetImageSize(string path);
/// <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.
/// </summary>
/// <param name="inputPath">The input path.</param>
/// <param name="outputPath">The output path.</param>
/// <param name="width">The width.</param>
/// <param name="height">The height.</param>
/// <param name="quality">The quality.</param>
/// <param name="options">The options.</param>
void EncodeImage(string inputPath, string outputPath, int width, int height, int quality, ImageProcessingOptions options);
/// <summary>
/// Creates the image collage.
/// </summary>
/// <param name="options">The options.</param>
void CreateImageCollage(ImageCollageOptions options);
}
}

View File

@ -0,0 +1,224 @@
using ImageMagickSharp;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Logging;
using System;
using System.IO;
namespace Emby.Drawing.ImageMagick
{
public class ImageMagickEncoder : IImageEncoder
{
private readonly ILogger _logger;
private readonly IApplicationPaths _appPaths;
public ImageMagickEncoder(ILogger logger, IApplicationPaths appPaths)
{
_logger = logger;
_appPaths = appPaths;
LogImageMagickVersion();
}
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[]
{
"tiff",
"jpeg",
"jpg",
"png",
"aiff",
"cr2",
"crw",
"dng",
"nef",
"orf",
"pef",
"arw",
"webp",
"gif",
"bmp"
};
}
}
public ImageFormat[] SupportedOutputFormats
{
get
{
if (_webpAvailable)
{
return new[] { ImageFormat.Webp, ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png };
}
return new[] { ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png };
}
}
private void LogImageMagickVersion()
{
_logger.Info("ImageMagick version: " + Wand.VersionString);
TestWebp();
}
private bool _webpAvailable = true;
private void TestWebp()
{
try
{
var tmpPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + ".webp");
Directory.CreateDirectory(Path.GetDirectoryName(tmpPath));
using (var wand = new MagickWand(1, 1, new PixelWand("none", 1)))
{
wand.SaveImage(tmpPath);
}
}
catch (Exception ex)
{
_logger.ErrorException("Error loading webp: ", ex);
_webpAvailable = false;
}
}
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)
{
CheckDisposed();
using (var wand = new MagickWand())
{
wand.PingImage(path);
var img = wand.CurrentImage;
return new ImageSize
{
Width = img.Width,
Height = img.Height
};
}
}
public void EncodeImage(string inputPath, string outputPath, int width, int height, int quality, ImageProcessingOptions options)
{
if (string.IsNullOrWhiteSpace(options.BackgroundColor))
{
using (var originalImage = new MagickWand(inputPath))
{
originalImage.CurrentImage.ResizeImage(width, height);
DrawIndicator(originalImage, width, height, options);
originalImage.CurrentImage.CompressionQuality = quality;
originalImage.SaveImage(outputPath);
}
}
else
{
using (var wand = new MagickWand(width, height, options.BackgroundColor))
{
using (var originalImage = new MagickWand(inputPath))
{
originalImage.CurrentImage.ResizeImage(width, height);
wand.CurrentImage.CompositeImage(originalImage, CompositeOperator.OverCompositeOp, 0, 0);
DrawIndicator(wand, width, height, options);
wand.CurrentImage.CompressionQuality = quality;
wand.SaveImage(outputPath);
}
}
}
}
/// <summary>
/// Draws the indicator.
/// </summary>
/// <param name="wand">The wand.</param>
/// <param name="imageWidth">Width of the image.</param>
/// <param name="imageHeight">Height of the image.</param>
/// <param name="options">The options.</param>
private void DrawIndicator(MagickWand wand, int imageWidth, int imageHeight, ImageProcessingOptions options)
{
if (!options.AddPlayedIndicator && !options.UnplayedCount.HasValue && options.PercentPlayed.Equals(0))
{
return;
}
try
{
if (options.AddPlayedIndicator)
{
var currentImageSize = new ImageSize(imageWidth, imageHeight);
new PlayedIndicatorDrawer(_appPaths).DrawPlayedIndicator(wand, currentImageSize);
}
else if (options.UnplayedCount.HasValue)
{
var currentImageSize = new ImageSize(imageWidth, imageHeight);
new UnplayedCountIndicator(_appPaths).DrawUnplayedCountIndicator(wand, currentImageSize, options.UnplayedCount.Value);
}
if (options.PercentPlayed > 0)
{
new PercentPlayedDrawer().Process(wand, options.PercentPlayed);
}
}
catch (Exception ex)
{
_logger.ErrorException("Error drawing indicator overlay", ex);
}
}
public void CreateImageCollage(ImageCollageOptions options)
{
double ratio = options.Width;
ratio /= options.Height;
if (ratio >= 1.4)
{
new StripCollageBuilder(_appPaths).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height, options.Text);
}
else if (ratio >= .9)
{
new StripCollageBuilder(_appPaths).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height, options.Text);
}
else
{
new StripCollageBuilder(_appPaths).BuildPosterCollage(options.InputPaths, options.OutputPath, options.Width, options.Height, options.Text);
}
}
private bool _disposed;
public void Dispose()
{
_disposed = true;
Wand.CloseEnvironment();
}
private void CheckDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(GetType().Name);
}
}
}
}

View File

@ -1,7 +1,7 @@
using ImageMagickSharp; using ImageMagickSharp;
using System; using System;
namespace MediaBrowser.Server.Implementations.Drawing namespace Emby.Drawing.ImageMagick
{ {
public class PercentPlayedDrawer public class PercentPlayedDrawer
{ {

View File

@ -4,7 +4,7 @@ using MediaBrowser.Model.Drawing;
using System; using System;
using System.IO; using System.IO;
namespace MediaBrowser.Server.Implementations.Drawing namespace Emby.Drawing.ImageMagick
{ {
public class PlayedIndicatorDrawer public class PlayedIndicatorDrawer
{ {

View File

@ -1,13 +1,10 @@
using ImageMagickSharp; using ImageMagickSharp;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Server.Implementations.Drawing;
using MediaBrowser.Server.Implementations.Photos;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
namespace MediaBrowser.Server.Implementations.UserViews namespace Emby.Drawing.ImageMagick
{ {
public class StripCollageBuilder public class StripCollageBuilder
{ {
@ -18,48 +15,57 @@ namespace MediaBrowser.Server.Implementations.UserViews
_appPaths = appPaths; _appPaths = appPaths;
} }
public Stream BuildPosterCollage(IEnumerable<string> paths, int width, int height, bool renderWithText, string text) public void BuildPosterCollage(IEnumerable<string> paths, string outputPath, int width, int height, string text)
{ {
if (renderWithText) if (!string.IsNullOrWhiteSpace(text))
{ {
using (var wand = BuildPosterCollageWandWithText(paths, text, width, height)) using (var wand = BuildPosterCollageWandWithText(paths, text, width, height))
{ {
return DynamicImageHelpers.GetStream(wand, _appPaths); wand.SaveImage(outputPath);
} }
} }
using (var wand = BuildPosterCollageWand(paths, width, height)) else
{ {
return DynamicImageHelpers.GetStream(wand, _appPaths); using (var wand = BuildPosterCollageWand(paths, width, height))
{
wand.SaveImage(outputPath);
}
} }
} }
public Stream BuildSquareCollage(IEnumerable<string> paths, int width, int height, bool renderWithText, string text) public void BuildSquareCollage(IEnumerable<string> paths, string outputPath, int width, int height, string text)
{ {
if (renderWithText) if (!string.IsNullOrWhiteSpace(text))
{ {
using (var wand = BuildSquareCollageWandWithText(paths, text, width, height)) using (var wand = BuildSquareCollageWandWithText(paths, text, width, height))
{ {
return DynamicImageHelpers.GetStream(wand, _appPaths); wand.SaveImage(outputPath);
} }
} }
using (var wand = BuildSquareCollageWand(paths, width, height)) else
{ {
return DynamicImageHelpers.GetStream(wand, _appPaths); using (var wand = BuildSquareCollageWand(paths, width, height))
{
wand.SaveImage(outputPath);
}
} }
} }
public Stream BuildThumbCollage(IEnumerable<string> paths, int width, int height, bool renderWithText, string text) public void BuildThumbCollage(IEnumerable<string> paths, string outputPath, int width, int height, string text)
{ {
if (renderWithText) if (!string.IsNullOrWhiteSpace(text))
{ {
using (var wand = BuildThumbCollageWandWithText(paths, text, width, height)) using (var wand = BuildThumbCollageWandWithText(paths, text, width, height))
{ {
return DynamicImageHelpers.GetStream(wand, _appPaths); wand.SaveImage(outputPath);
} }
} }
using (var wand = BuildThumbCollageWand(paths, width, height)) else
{ {
return DynamicImageHelpers.GetStream(wand, _appPaths); using (var wand = BuildThumbCollageWand(paths, width, height))
{
wand.SaveImage(outputPath);
}
} }
} }

View File

@ -3,7 +3,7 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Drawing;
using System.Globalization; using System.Globalization;
namespace MediaBrowser.Server.Implementations.Drawing namespace Emby.Drawing.ImageMagick
{ {
public class UnplayedCountIndicator public class UnplayedCountIndicator
{ {

View File

@ -1,4 +1,4 @@
using ImageMagickSharp; using Emby.Drawing.Common;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.IO; using MediaBrowser.Common.IO;
using MediaBrowser.Controller; using MediaBrowser.Controller;
@ -18,7 +18,7 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace MediaBrowser.Server.Implementations.Drawing namespace Emby.Drawing
{ {
/// <summary> /// <summary>
/// Class ImageProcessor /// Class ImageProcessor
@ -50,12 +50,14 @@ namespace MediaBrowser.Server.Implementations.Drawing
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IJsonSerializer _jsonSerializer; private readonly IJsonSerializer _jsonSerializer;
private readonly IServerApplicationPaths _appPaths; private readonly IServerApplicationPaths _appPaths;
private readonly IImageEncoder _imageEncoder;
public ImageProcessor(ILogger logger, IServerApplicationPaths appPaths, IFileSystem fileSystem, IJsonSerializer jsonSerializer) public ImageProcessor(ILogger logger, IServerApplicationPaths appPaths, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IImageEncoder imageEncoder)
{ {
_logger = logger; _logger = logger;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_jsonSerializer = jsonSerializer; _jsonSerializer = jsonSerializer;
_imageEncoder = imageEncoder;
_appPaths = appPaths; _appPaths = appPaths;
_saveImageSizeTimer = new Timer(SaveImageSizeCallback, null, Timeout.Infinite, Timeout.Infinite); _saveImageSizeTimer = new Timer(SaveImageSizeCallback, null, Timeout.Infinite, Timeout.Infinite);
@ -85,8 +87,14 @@ namespace MediaBrowser.Server.Implementations.Drawing
} }
_cachedImagedSizes = new ConcurrentDictionary<Guid, ImageSize>(sizeDictionary); _cachedImagedSizes = new ConcurrentDictionary<Guid, ImageSize>(sizeDictionary);
}
LogImageMagickVersionVersion(); public string[] SupportedInputFormats
{
get
{
return _imageEncoder.SupportedInputFormats;
}
} }
private string ResizedImageCachePath private string ResizedImageCachePath
@ -130,44 +138,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
public ImageFormat[] GetSupportedImageOutputFormats() public ImageFormat[] GetSupportedImageOutputFormats()
{ {
if (_webpAvailable) return _imageEncoder.SupportedOutputFormats;
{
return new[] { ImageFormat.Webp, ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png };
}
return new[] { ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png };
}
private bool _webpAvailable = true;
private void TestWebp()
{
try
{
var tmpPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + ".webp");
Directory.CreateDirectory(Path.GetDirectoryName(tmpPath));
using (var wand = new MagickWand(1, 1, new PixelWand("none", 1)))
{
wand.SaveImage(tmpPath);
}
}
catch (Exception ex)
{
_logger.ErrorException("Error loading webp: ", ex);
_webpAvailable = false;
}
}
private void LogImageMagickVersionVersion()
{
try
{
_logger.Info("ImageMagick version: " + Wand.VersionString);
}
catch (Exception ex)
{
_logger.ErrorException("Error loading ImageMagick: ", ex);
}
TestWebp();
} }
public async Task<string> ProcessImage(ImageProcessingOptions options) public async Task<string> ProcessImage(ImageProcessingOptions options)
@ -244,36 +215,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
if (string.IsNullOrWhiteSpace(options.BackgroundColor)) _imageEncoder.EncodeImage(originalImagePath, cacheFilePath, newWidth, newHeight, quality, options);
{
using (var originalImage = new MagickWand(originalImagePath))
{
originalImage.CurrentImage.ResizeImage(newWidth, newHeight);
DrawIndicator(originalImage, newWidth, newHeight, options);
originalImage.CurrentImage.CompressionQuality = quality;
originalImage.SaveImage(cacheFilePath);
}
}
else
{
using (var wand = new MagickWand(newWidth, newHeight, options.BackgroundColor))
{
using (var originalImage = new MagickWand(originalImagePath))
{
originalImage.CurrentImage.ResizeImage(newWidth, newHeight);
wand.CurrentImage.CompositeImage(originalImage, CompositeOperator.OverCompositeOp, 0, 0);
DrawIndicator(wand, newWidth, newHeight, options);
wand.CurrentImage.CompressionQuality = quality;
wand.SaveImage(cacheFilePath);
}
}
}
} }
} }
finally finally
@ -286,7 +228,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
private ImageFormat GetOutputFormat(ImageFormat requestedFormat) private ImageFormat GetOutputFormat(ImageFormat requestedFormat)
{ {
if (requestedFormat == ImageFormat.Webp && !_webpAvailable) if (requestedFormat == ImageFormat.Webp && !_imageEncoder.SupportedOutputFormats.Contains(ImageFormat.Webp))
{ {
return ImageFormat.Png; return ImageFormat.Png;
} }
@ -294,46 +236,6 @@ namespace MediaBrowser.Server.Implementations.Drawing
return requestedFormat; return requestedFormat;
} }
/// <summary>
/// Draws the indicator.
/// </summary>
/// <param name="wand">The wand.</param>
/// <param name="imageWidth">Width of the image.</param>
/// <param name="imageHeight">Height of the image.</param>
/// <param name="options">The options.</param>
private void DrawIndicator(MagickWand wand, int imageWidth, int imageHeight, ImageProcessingOptions options)
{
if (!options.AddPlayedIndicator && !options.UnplayedCount.HasValue && options.PercentPlayed.Equals(0))
{
return;
}
try
{
if (options.AddPlayedIndicator)
{
var currentImageSize = new ImageSize(imageWidth, imageHeight);
new PlayedIndicatorDrawer(_appPaths).DrawPlayedIndicator(wand, currentImageSize);
}
else if (options.UnplayedCount.HasValue)
{
var currentImageSize = new ImageSize(imageWidth, imageHeight);
new UnplayedCountIndicator(_appPaths).DrawUnplayedCountIndicator(wand, currentImageSize, options.UnplayedCount.Value);
}
if (options.PercentPlayed > 0)
{
new PercentPlayedDrawer().Process(wand, options.PercentPlayed);
}
}
catch (Exception ex)
{
_logger.ErrorException("Error drawing indicator overlay", ex);
}
}
/// <summary> /// <summary>
/// Crops whitespace from an image, caches the result, and returns the cached path /// Crops whitespace from an image, caches the result, and returns the cached path
/// </summary> /// </summary>
@ -360,11 +262,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
{ {
Directory.CreateDirectory(Path.GetDirectoryName(croppedImagePath)); Directory.CreateDirectory(Path.GetDirectoryName(croppedImagePath));
using (var wand = new MagickWand(originalImagePath)) _imageEncoder.CropWhiteSpace(originalImagePath, croppedImagePath);
{
wand.CurrentImage.TrimImage(10);
wand.SaveImage(croppedImagePath);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -500,17 +398,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
CheckDisposed(); CheckDisposed();
using (var wand = new MagickWand()) size = _imageEncoder.GetImageSize(path);
{
wand.PingImage(path);
var img = wand.CurrentImage;
size = new ImageSize
{
Width = img.Width,
Height = img.Height
};
}
} }
StartSaveImageSizeTimer(); StartSaveImageSizeTimer();
@ -838,6 +726,11 @@ namespace MediaBrowser.Server.Implementations.Drawing
return Path.Combine(path, filename); return Path.Combine(path, filename);
} }
public void CreateImageCollage(ImageCollageOptions options)
{
_imageEncoder.CreateImageCollage(options);
}
public IEnumerable<IImageEnhancer> GetSupportedEnhancers(IHasImages item, ImageType imageType) public IEnumerable<IImageEnhancer> GetSupportedEnhancers(IHasImages item, ImageType imageType)
{ {
return ImageEnhancers.Where(i => return ImageEnhancers.Where(i =>
@ -860,7 +753,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
public void Dispose() public void Dispose()
{ {
_disposed = true; _disposed = true;
Wand.CloseEnvironment(); _imageEncoder.Dispose();
_saveImageSizeTimer.Dispose(); _saveImageSizeTimer.Dispose();
} }

View File

@ -13,6 +13,12 @@ namespace MediaBrowser.Controller.Drawing
/// </summary> /// </summary>
public interface IImageProcessor public interface IImageProcessor
{ {
/// <summary>
/// Gets the supported input formats.
/// </summary>
/// <value>The supported input formats.</value>
string[] SupportedInputFormats { get; }
/// <summary> /// <summary>
/// Gets the image enhancers. /// Gets the image enhancers.
/// </summary> /// </summary>
@ -93,5 +99,11 @@ namespace MediaBrowser.Controller.Drawing
/// </summary> /// </summary>
/// <returns>ImageOutputFormat[].</returns> /// <returns>ImageOutputFormat[].</returns>
ImageFormat[] GetSupportedImageOutputFormats(); ImageFormat[] GetSupportedImageOutputFormats();
/// <summary>
/// Creates the image collage.
/// </summary>
/// <param name="options">The options.</param>
void CreateImageCollage(ImageCollageOptions options);
} }
} }

View File

@ -0,0 +1,32 @@

namespace MediaBrowser.Controller.Drawing
{
public class ImageCollageOptions
{
/// <summary>
/// Gets or sets the input paths.
/// </summary>
/// <value>The input paths.</value>
public string[] InputPaths { get; set; }
/// <summary>
/// Gets or sets the output path.
/// </summary>
/// <value>The output path.</value>
public string OutputPath { get; set; }
/// <summary>
/// Gets or sets the width.
/// </summary>
/// <value>The width.</value>
public int Width { get; set; }
/// <summary>
/// Gets or sets the height.
/// </summary>
/// <value>The height.</value>
public int Height { get; set; }
/// <summary>
/// Gets or sets the text.
/// </summary>
/// <value>The text.</value>
public string Text { get; set; }
}
}

View File

@ -115,6 +115,7 @@
<Compile Include="Dlna\IMediaReceiverRegistrar.cs" /> <Compile Include="Dlna\IMediaReceiverRegistrar.cs" />
<Compile Include="Dlna\IUpnpService.cs" /> <Compile Include="Dlna\IUpnpService.cs" />
<Compile Include="Drawing\IImageProcessor.cs" /> <Compile Include="Drawing\IImageProcessor.cs" />
<Compile Include="Drawing\ImageCollageOptions.cs" />
<Compile Include="Drawing\ImageProcessingOptions.cs" /> <Compile Include="Drawing\ImageProcessingOptions.cs" />
<Compile Include="Drawing\ImageProcessorExtensions.cs" /> <Compile Include="Drawing\ImageProcessorExtensions.cs" />
<Compile Include="Drawing\ImageStream.cs" /> <Compile Include="Drawing\ImageStream.cs" />

View File

@ -1,5 +1,6 @@
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.IO; using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.Movies;
@ -16,7 +17,7 @@ namespace MediaBrowser.Server.Implementations.Collections
{ {
public class CollectionImageProvider : BaseDynamicImageProvider<BoxSet> public class CollectionImageProvider : BaseDynamicImageProvider<BoxSet>
{ {
public CollectionImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths) : base(fileSystem, providerManager, applicationPaths) public CollectionImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
{ {
} }

View File

@ -1,4 +1,5 @@
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
@ -10,6 +11,12 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers
{ {
public class PhotoAlbumResolver : FolderResolver<PhotoAlbum> public class PhotoAlbumResolver : FolderResolver<PhotoAlbum>
{ {
private readonly IImageProcessor _imageProcessor;
public PhotoAlbumResolver(IImageProcessor imageProcessor)
{
_imageProcessor = imageProcessor;
}
/// <summary> /// <summary>
/// Resolves the specified args. /// Resolves the specified args.
/// </summary> /// </summary>
@ -32,9 +39,9 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers
return null; return null;
} }
private static bool HasPhotos(ItemResolveArgs args) private bool HasPhotos(ItemResolveArgs args)
{ {
return args.FileSystemChildren.Any(i => ((i.Attributes & FileAttributes.Directory) != FileAttributes.Directory) && PhotoResolver.IsImageFile(i.FullName)); return args.FileSystemChildren.Any(i => ((i.Attributes & FileAttributes.Directory) != FileAttributes.Directory) && PhotoResolver.IsImageFile(i.FullName, _imageProcessor));
} }
public override ResolverPriority Priority public override ResolverPriority Priority

View File

@ -1,4 +1,5 @@
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using System; using System;
@ -9,6 +10,12 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers
{ {
public class PhotoResolver : ItemResolver<Photo> public class PhotoResolver : ItemResolver<Photo>
{ {
private readonly IImageProcessor _imageProcessor;
public PhotoResolver(IImageProcessor imageProcessor)
{
_imageProcessor = imageProcessor;
}
/// <summary> /// <summary>
/// Resolves the specified args. /// Resolves the specified args.
/// </summary> /// </summary>
@ -19,7 +26,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers
// Must be an image file within a photo collection // Must be an image file within a photo collection
if (string.Equals(args.GetCollectionType(), CollectionType.Photos, StringComparison.OrdinalIgnoreCase) && if (string.Equals(args.GetCollectionType(), CollectionType.Photos, StringComparison.OrdinalIgnoreCase) &&
!args.IsDirectory && !args.IsDirectory &&
IsImageFile(args.Path)) IsImageFile(args.Path, _imageProcessor))
{ {
return new Photo return new Photo
{ {
@ -30,9 +37,6 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers
return null; return null;
} }
// Some common file name extensions for RAW picture files include: .cr2, .crw, .dng, .nef, .orf, .rw2, .pef, .arw, .sr2, .srf, and .tif.
protected static string[] ImageExtensions = { ".tiff", ".jpeg", ".jpg", ".png", ".aiff", ".cr2", ".crw", ".dng", ".nef", ".orf", ".pef", ".arw", ".webp" };
private static readonly string[] IgnoreFiles = private static readonly string[] IgnoreFiles =
{ {
"folder", "folder",
@ -43,12 +47,12 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers
"poster" "poster"
}; };
internal static bool IsImageFile(string path) internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
{ {
var filename = Path.GetFileNameWithoutExtension(path) ?? string.Empty; var filename = Path.GetFileNameWithoutExtension(path) ?? string.Empty;
return !IgnoreFiles.Contains(filename, StringComparer.OrdinalIgnoreCase) return !IgnoreFiles.Contains(filename, StringComparer.OrdinalIgnoreCase)
&& ImageExtensions.Contains(Path.GetExtension(path) ?? string.Empty, StringComparer.OrdinalIgnoreCase); && imageProcessor.SupportedInputFormats.Contains((Path.GetExtension(path) ?? string.Empty).TrimStart('.'), StringComparer.OrdinalIgnoreCase);
} }
} }

View File

@ -242,8 +242,6 @@
<Compile Include="Persistence\SqliteShrinkMemoryTimer.cs" /> <Compile Include="Persistence\SqliteShrinkMemoryTimer.cs" />
<Compile Include="Persistence\TypeMapper.cs" /> <Compile Include="Persistence\TypeMapper.cs" />
<Compile Include="Photos\BaseDynamicImageProvider.cs" /> <Compile Include="Photos\BaseDynamicImageProvider.cs" />
<Compile Include="Photos\DynamicImageHelpers.cs" />
<Compile Include="UserViews\StripCollageBuilder.cs" />
<Compile Include="Playlists\ManualPlaylistsFolder.cs" /> <Compile Include="Playlists\ManualPlaylistsFolder.cs" />
<Compile Include="Photos\PhotoAlbumImageProvider.cs" /> <Compile Include="Photos\PhotoAlbumImageProvider.cs" />
<Compile Include="Playlists\PlaylistImageProvider.cs" /> <Compile Include="Playlists\PlaylistImageProvider.cs" />

View File

@ -1,13 +1,13 @@
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.IO; using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Server.Implementations.UserViews;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@ -23,12 +23,14 @@ namespace MediaBrowser.Server.Implementations.Photos
protected IFileSystem FileSystem { get; private set; } protected IFileSystem FileSystem { get; private set; }
protected IProviderManager ProviderManager { get; private set; } protected IProviderManager ProviderManager { get; private set; }
protected IApplicationPaths ApplicationPaths { get; private set; } protected IApplicationPaths ApplicationPaths { get; private set; }
protected IImageProcessor ImageProcessor { get; set; }
protected BaseDynamicImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths) protected BaseDynamicImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor)
{ {
ApplicationPaths = applicationPaths; ApplicationPaths = applicationPaths;
ProviderManager = providerManager; ProviderManager = providerManager;
FileSystem = fileSystem; FileSystem = fileSystem;
ImageProcessor = imageProcessor;
} }
public virtual bool Supports(IHasImages item) public virtual bool Supports(IHasImages item)
@ -77,7 +79,7 @@ namespace MediaBrowser.Server.Implementations.Photos
string cacheKey, string cacheKey,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var stream = await CreateImageAsync(item, itemsWithImages, imageType, 0).ConfigureAwait(false); var stream = CreateImageAsync(item, itemsWithImages, imageType, 0);
if (stream == null) if (stream == null)
{ {
@ -113,7 +115,7 @@ namespace MediaBrowser.Server.Implementations.Photos
var items = await GetItemsWithImages(item).ConfigureAwait(false); var items = await GetItemsWithImages(item).ConfigureAwait(false);
var cacheKey = GetConfigurationCacheKey(items, item.Name); var cacheKey = GetConfigurationCacheKey(items, item.Name);
var result = await CreateImageAsync(item, items, type, 0).ConfigureAwait(false); var result = CreateImageAsync(item, items, type, 0);
return new DynamicImageResponse return new DynamicImageResponse
{ {
@ -126,7 +128,7 @@ namespace MediaBrowser.Server.Implementations.Photos
protected abstract Task<List<BaseItem>> GetItemsWithImages(IHasImages item); protected abstract Task<List<BaseItem>> GetItemsWithImages(IHasImages item);
private const string Version = "19"; private const string Version = "20";
protected string GetConfigurationCacheKey(List<BaseItem> items, string itemName) protected string GetConfigurationCacheKey(List<BaseItem> items, string itemName)
{ {
var parts = Version + "_" + (itemName ?? string.Empty) + "_" + var parts = Version + "_" + (itemName ?? string.Empty) + "_" +
@ -135,32 +137,53 @@ namespace MediaBrowser.Server.Implementations.Photos
return parts.GetMD5().ToString("N"); return parts.GetMD5().ToString("N");
} }
protected Task<Stream> GetThumbCollage(IHasImages primaryItem, List<BaseItem> items) protected Stream GetThumbCollage(IHasImages primaryItem, List<BaseItem> items)
{ {
var stream = new StripCollageBuilder(ApplicationPaths).BuildThumbCollage(GetStripCollageImagePaths(items), 960, 540, true, primaryItem.Name); return GetThumbCollage(primaryItem, items, 960, 540, true, primaryItem.Name);
return Task.FromResult(stream);
} }
private IEnumerable<String> GetStripCollageImagePaths(IEnumerable<BaseItem> items) protected virtual IEnumerable<String> GetStripCollageImagePaths(IHasImages primaryItem, IEnumerable<BaseItem> items)
{ {
return items return items
.Select(i => i.GetImagePath(ImageType.Primary) ?? i.GetImagePath(ImageType.Thumb)) .Select(i => i.GetImagePath(ImageType.Primary) ?? i.GetImagePath(ImageType.Thumb))
.Where(i => !string.IsNullOrWhiteSpace(i)); .Where(i => !string.IsNullOrWhiteSpace(i));
} }
protected Task<Stream> GetPosterCollage(IHasImages primaryItem, List<BaseItem> items) protected Stream GetPosterCollage(IHasImages primaryItem, List<BaseItem> items)
{ {
var stream = new StripCollageBuilder(ApplicationPaths).BuildPosterCollage(GetStripCollageImagePaths(items), 600, 900, true, primaryItem.Name); var path = CreateCollage(primaryItem, items, 600, 900, true, primaryItem.Name);
return FileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
return Task.FromResult(stream);
} }
protected Task<Stream> GetSquareCollage(IHasImages primaryItem, List<BaseItem> items) protected Stream GetSquareCollage(IHasImages primaryItem, List<BaseItem> items)
{ {
var stream = new StripCollageBuilder(ApplicationPaths).BuildSquareCollage(GetStripCollageImagePaths(items), 800, 800, true, primaryItem.Name); var path = CreateCollage(primaryItem, items, 800, 800, true, primaryItem.Name);
return FileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
}
return Task.FromResult(stream); protected Stream GetThumbCollage(IHasImages primaryItem, List<BaseItem> items, int width, int height, bool drawText, string text)
{
var path = CreateCollage(primaryItem, items, width, height, drawText, text);
return FileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
}
private string CreateCollage(IHasImages primaryItem, List<BaseItem> items, int width, int height, bool drawText, string text)
{
var outputPath = Path.Combine(ApplicationPaths.TempDirectory, Guid.NewGuid() + ".png");
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
var options = new ImageCollageOptions
{
Height = height,
Width = width,
OutputPath = outputPath,
Text = drawText ? text : null,
InputPaths = GetStripCollageImagePaths(primaryItem, items).ToArray()
};
ImageProcessor.CreateImageCollage(options);
return outputPath;
} }
public string Name public string Name
@ -168,7 +191,7 @@ namespace MediaBrowser.Server.Implementations.Photos
get { return "Dynamic Image Provider"; } get { return "Dynamic Image Provider"; }
} }
protected virtual async Task<Stream> CreateImageAsync(IHasImages item, protected virtual Stream CreateImageAsync(IHasImages item,
List<BaseItem> itemsWithImages, List<BaseItem> itemsWithImages,
ImageType imageType, ImageType imageType,
int imageIndex) int imageIndex)
@ -180,14 +203,14 @@ namespace MediaBrowser.Server.Implementations.Photos
if (imageType == ImageType.Thumb) if (imageType == ImageType.Thumb)
{ {
return await GetThumbCollage(item, itemsWithImages).ConfigureAwait(false); return GetThumbCollage(item, itemsWithImages);
} }
if (imageType == ImageType.Primary) if (imageType == ImageType.Primary)
{ {
return item is PhotoAlbum || item is Playlist ? return item is PhotoAlbum || item is Playlist ?
await GetSquareCollage(item, itemsWithImages).ConfigureAwait(false) : GetSquareCollage(item, itemsWithImages) :
await GetPosterCollage(item, itemsWithImages).ConfigureAwait(false); GetPosterCollage(item, itemsWithImages);
} }
throw new ArgumentException("Unexpected image type"); throw new ArgumentException("Unexpected image type");

View File

@ -1,132 +0,0 @@
using ImageMagickSharp;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.IO;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace MediaBrowser.Server.Implementations.Photos
{
public static class DynamicImageHelpers
{
public static async Task<Stream> GetThumbCollage(List<string> files,
IFileSystem fileSystem,
int width,
int height, IApplicationPaths appPaths)
{
if (files.Any(string.IsNullOrWhiteSpace))
{
throw new ArgumentException("Empty file found in files list");
}
if (files.Count == 0)
{
return null;
}
if (files.Count < 3)
{
return await GetSingleImage(files, fileSystem).ConfigureAwait(false);
}
const int rows = 1;
const int cols = 3;
int cellWidth = 2 * (width / 3);
int cellHeight = height;
var index = 0;
using (var wand = new MagickWand(width, height, new PixelWand(ColorName.None, 1)))
{
for (var row = 0; row < rows; row++)
{
for (var col = 0; col < cols; col++)
{
var x = col * (cellWidth / 2);
var y = row * cellHeight;
if (files.Count > index)
{
using (var innerWand = new MagickWand(files[index]))
{
innerWand.CurrentImage.ResizeImage(cellWidth, cellHeight);
wand.CurrentImage.CompositeImage(innerWand, CompositeOperator.OverCompositeOp, x, y);
}
}
index++;
}
}
return GetStream(wand, appPaths);
}
}
public static async Task<Stream> GetSquareCollage(List<string> files,
IFileSystem fileSystem,
int size, IApplicationPaths appPaths)
{
if (files.Any(string.IsNullOrWhiteSpace))
{
throw new ArgumentException("Empty file found in files list");
}
if (files.Count == 0)
{
return null;
}
if (files.Count < 4)
{
return await GetSingleImage(files, fileSystem).ConfigureAwait(false);
}
const int rows = 2;
const int cols = 2;
int singleSize = size / 2;
var index = 0;
using (var wand = new MagickWand(size, size, new PixelWand(ColorName.None, 1)))
{
for (var row = 0; row < rows; row++)
{
for (var col = 0; col < cols; col++)
{
var x = col * singleSize;
var y = row * singleSize;
using (var innerWand = new MagickWand(files[index]))
{
innerWand.CurrentImage.ResizeImage(singleSize, singleSize);
wand.CurrentImage.CompositeImage(innerWand, CompositeOperator.OverCompositeOp, x, y);
}
index++;
}
}
return GetStream(wand, appPaths);
}
}
private static Task<Stream> GetSingleImage(List<string> files, IFileSystem fileSystem)
{
return Task.FromResult<Stream>(fileSystem.GetFileStream(files[0], FileMode.Open, FileAccess.Read, FileShare.Read));
}
internal static Stream GetStream(MagickWand image, IApplicationPaths appPaths)
{
var tempFile = Path.Combine(appPaths.TempDirectory, Guid.NewGuid().ToString("N") + ".png");
Directory.CreateDirectory(Path.GetDirectoryName(tempFile));
image.CurrentImage.CompressionQuality = 100;
image.SaveImage(tempFile);
return File.OpenRead(tempFile);
}
}
}

View File

@ -1,5 +1,6 @@
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.IO; using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using System.Collections.Generic; using System.Collections.Generic;
@ -10,7 +11,7 @@ namespace MediaBrowser.Server.Implementations.Photos
{ {
public class PhotoAlbumImageProvider : BaseDynamicImageProvider<PhotoAlbum> public class PhotoAlbumImageProvider : BaseDynamicImageProvider<PhotoAlbum>
{ {
public PhotoAlbumImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths) : base(fileSystem, providerManager, applicationPaths) public PhotoAlbumImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
{ {
} }

View File

@ -1,5 +1,6 @@
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.IO; using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
@ -16,7 +17,7 @@ namespace MediaBrowser.Server.Implementations.Playlists
{ {
public class PlaylistImageProvider : BaseDynamicImageProvider<Playlist> public class PlaylistImageProvider : BaseDynamicImageProvider<Playlist>
{ {
public PlaylistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths) : base(fileSystem, providerManager, applicationPaths) public PlaylistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
{ {
} }

View File

@ -1,5 +1,6 @@
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.IO; using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
@ -22,8 +23,7 @@ namespace MediaBrowser.Server.Implementations.UserViews
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
public DynamicImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IUserManager userManager, ILibraryManager libraryManager) public DynamicImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, IUserManager userManager, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
: base(fileSystem, providerManager, applicationPaths)
{ {
_userManager = userManager; _userManager = userManager;
_libraryManager = libraryManager; _libraryManager = libraryManager;
@ -238,7 +238,7 @@ namespace MediaBrowser.Server.Implementations.UserViews
return collectionStripViewTypes.Contains(view.ViewType ?? string.Empty); return collectionStripViewTypes.Contains(view.ViewType ?? string.Empty);
} }
protected override async Task<Stream> CreateImageAsync(IHasImages item, List<BaseItem> itemsWithImages, ImageType imageType, int imageIndex) protected override Stream CreateImageAsync(IHasImages item, List<BaseItem> itemsWithImages, ImageType imageType, int imageIndex)
{ {
var view = (UserView)item; var view = (UserView)item;
if (imageType == ImageType.Primary && IsUsingCollectionStrip(view)) if (imageType == ImageType.Primary && IsUsingCollectionStrip(view))
@ -248,15 +248,17 @@ namespace MediaBrowser.Server.Implementations.UserViews
return null; return null;
} }
return new StripCollageBuilder(ApplicationPaths).BuildThumbCollage(GetStripCollageImagePaths(itemsWithImages, view.ViewType), 960, 540, false, item.Name); return GetThumbCollage(item, itemsWithImages, 960, 540, false, item.Name);
} }
return await base.CreateImageAsync(item, itemsWithImages, imageType, imageIndex); return base.CreateImageAsync(item, itemsWithImages, imageType, imageIndex);
} }
private IEnumerable<String> GetStripCollageImagePaths(IEnumerable<BaseItem> items, string viewType) protected override IEnumerable<String> GetStripCollageImagePaths(IHasImages primaryItem, IEnumerable<BaseItem> items)
{ {
if (string.Equals(viewType, CollectionType.LiveTv, StringComparison.OrdinalIgnoreCase)) var userView = primaryItem as UserView;
if (userView != null && string.Equals(userView.ViewType, CollectionType.LiveTv, StringComparison.OrdinalIgnoreCase))
{ {
var list = new List<string>(); var list = new List<string>();
for (int i = 1; i <= 8; i++) for (int i = 1; i <= 8; i++)
@ -266,9 +268,7 @@ namespace MediaBrowser.Server.Implementations.UserViews
return list; return list;
} }
return items return base.GetStripCollageImagePaths(primaryItem, items);
.Select(i => i.GetImagePath(ImageType.Primary) ?? i.GetImagePath(ImageType.Thumb))
.Where(i => !string.IsNullOrWhiteSpace(i));
} }
private string ExtractLiveTvResource(string name, IApplicationPaths paths) private string ExtractLiveTvResource(string name, IApplicationPaths paths)

View File

@ -1,4 +1,7 @@
using MediaBrowser.Api; using Emby.Drawing;
using Emby.Drawing.GDI;
using Emby.Drawing.ImageMagick;
using MediaBrowser.Api;
using MediaBrowser.Common; using MediaBrowser.Common;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events; using MediaBrowser.Common.Events;
@ -63,7 +66,6 @@ using MediaBrowser.Server.Implementations.Collections;
using MediaBrowser.Server.Implementations.Configuration; using MediaBrowser.Server.Implementations.Configuration;
using MediaBrowser.Server.Implementations.Connect; using MediaBrowser.Server.Implementations.Connect;
using MediaBrowser.Server.Implementations.Devices; using MediaBrowser.Server.Implementations.Devices;
using MediaBrowser.Server.Implementations.Drawing;
using MediaBrowser.Server.Implementations.Dto; using MediaBrowser.Server.Implementations.Dto;
using MediaBrowser.Server.Implementations.EntryPoints; using MediaBrowser.Server.Implementations.EntryPoints;
using MediaBrowser.Server.Implementations.FileOrganization; using MediaBrowser.Server.Implementations.FileOrganization;
@ -440,7 +442,7 @@ namespace MediaBrowser.Server.Startup.Common
var innerProgress = new ActionableProgress<double>(); var innerProgress = new ActionableProgress<double>();
innerProgress.RegisterAction(p => progress.Report((.75 * p) + 15)); innerProgress.RegisterAction(p => progress.Report((.75 * p) + 15));
ImageProcessor = new ImageProcessor(LogManager.GetLogger("ImageProcessor"), ServerConfigurationManager.ApplicationPaths, FileSystemManager, JsonSerializer); ImageProcessor = new ImageProcessor(LogManager.GetLogger("ImageProcessor"), ServerConfigurationManager.ApplicationPaths, FileSystemManager, JsonSerializer, GetImageEncoder());
RegisterSingleInstance(ImageProcessor); RegisterSingleInstance(ImageProcessor);
TVSeriesManager = new TVSeriesManager(UserManager, UserDataManager, LibraryManager); TVSeriesManager = new TVSeriesManager(UserManager, UserDataManager, LibraryManager);
@ -544,6 +546,23 @@ namespace MediaBrowser.Server.Startup.Common
await ((UserManager)UserManager).Initialize().ConfigureAwait(false); await ((UserManager)UserManager).Initialize().ConfigureAwait(false);
} }
private IImageEncoder GetImageEncoder()
{
if (!_startupOptions.ContainsOption("-enablegdi"))
{
try
{
return new ImageMagickEncoder(LogManager.GetLogger("ImageMagick"), ApplicationPaths);
}
catch (Exception ex)
{
Logger.ErrorException("Error loading ImageMagick. Will revert to GDI.", ex);
}
}
return new GDIImageEncoder(FileSystemManager, LogManager.GetLogger("GDI"));
}
protected override INetworkManager CreateNetworkManager(ILogger logger) protected override INetworkManager CreateNetworkManager(ILogger logger)
{ {
return NativeApp.CreateNetworkManager(logger); return NativeApp.CreateNetworkManager(logger);

View File

@ -40,7 +40,6 @@ namespace MediaBrowser.ServerApplication
var applicationPath = currentProcess.MainModule.FileName; var applicationPath = currentProcess.MainModule.FileName;
//Wand.SetMagickCoderModulePath(Path.Combine(Path.GetDirectoryName(applicationPath), "ImageMagickCoders", "x86"));
var appPaths = CreateApplicationPaths(applicationPath, _isRunningAsService); var appPaths = CreateApplicationPaths(applicationPath, _isRunningAsService);
var logManager = new NlogManager(appPaths.LogDirectoryPath, "server"); var logManager = new NlogManager(appPaths.LogDirectoryPath, "server");