Add first working implementation of the cover generator

TODO: Speedup
This commit is contained in:
Flamefire committed 2014-08-14 16:07:49 +02:00
1 parent 388bd6310f
commit 3f4f2776be
6 files changed
+412 -10

No files matched your search

+38
View File
@@ -5,4 +5,42 @@
<Folder>Blue</Folder>
<Author>flokuep</Author>
</Info>
<MissingCover>
<Default>
<Text>
<!--<Text>: Nothing or translation tag-->
<Text>%TEXT%</Text>
<!--<Font>: Text font name-->
<Font>Outline</Font>
<!--<Style>: Text style: Normal, Italic, Bold, BoldItalic-->
<Style>Bold</Style>
<Size>500</Size>
<!--<Color>: Text color from ColorScheme (high priority)-->
<!--or <R>, <G>, <B>, <A> (lower priority)-->
<R>0</R><G>1</G><B>1</B><A>0.5</A>
<Margin>24</Margin>
<Indent>100</Indent>
</Text>
<Image>EmptyCover.png</Image>
<ShowFirstCover>true</ShowFirstCover>
<ImageAlpha>0.8</ImageAlpha>
</Default>
<Folder>
<Text>
<!--<Text>: Nothing or translation tag-->
<Text>%TEXT%</Text>
<!--<Font>: Text font name-->
<Font>Outline</Font>
<!--<Style>: Text style: Normal, Italic, Bold, BoldItalic-->
<Style>Bold</Style>
<Size>500</Size>
<!--<Color>: Text color from ColorScheme (high priority)-->
<!--or <R>, <G>, <B>, <A> (lower priority)-->
<R>0</R><G>1</G><B>0</B><A>0.5</A>
<Margin>24</Margin>
<Indent>100</Indent>
</Text>
<Image>EmptyCover.png</Image>
</Folder>
</MissingCover>
</root>
+84 -3
View File
@@ -20,6 +20,7 @@ using System.Collections.Generic;
using System.IO;
using VocaluxeLib;
using VocaluxeLib.Draw;
using VocaluxeLib.Songs;
namespace Vocaluxe.Base
{
@@ -32,7 +33,11 @@ namespace Vocaluxe.Base
static class CCover
{
private const string _NoCoverName = "No Cover";
private const string _NoCoverNameAlt = "NoCover";
private const string _DefaultTypeName = "Default";
private static readonly Dictionary<string, CTexture> _Covers = new Dictionary<string, CTexture>();
private static readonly Dictionary<string, CNoCoverGenerator> _NoCoverThemes = new Dictionary<string, CNoCoverGenerator>();
private static readonly List<SCoverTheme> _CoverThemes = new List<SCoverTheme>();
public static CTexture NoCover { get; private set; }
@@ -92,6 +97,24 @@ namespace Vocaluxe.Base
}
}
public static CTexture GenerateCover(string text, ESongSorting sorting, CCategory category)
{
CSong firstSong = category.GetSong(0);
CTexture cover = _NoCoverThemes[_SongSortingToType(sorting)].GetCover(text, firstSong != null ? Path.Combine(firstSong.Folder, firstSong.CoverFileName) : null);
if (cover == null)
cover = _NoCoverThemes[_DefaultTypeName].GetCover(text, firstSong != null ? Path.Combine(firstSong.Folder, firstSong.CoverFileName) : null);
if (cover == null)
cover = NoCover;
else
{
lock (_Covers)
{
_Covers.Add(text, cover);
}
}
return cover;
}
/// <summary>
/// Returns true if a cover with the given name exists.
/// MUST HOLD _Covers lock at this point
@@ -121,6 +144,10 @@ namespace Vocaluxe.Base
}
_Covers.Clear();
}
lock (_NoCoverThemes)
{
_NoCoverThemes.Clear();
}
}
/// <summary>
@@ -168,7 +195,7 @@ namespace Vocaluxe.Base
}
/// <summary>
/// Loads all cover which are defined in the cover config file.
/// Loads all covers from the theme
/// </summary>
private static void _LoadCovers(string coverThemeName)
{
@@ -184,8 +211,62 @@ namespace Vocaluxe.Base
{
foreach (string file in files)
_AddCover(Path.GetFileNameWithoutExtension(file), file);
if (_CoverExists("No Cover"))
NoCover = _Covers["No Cover"];
if (_CoverExists(_NoCoverName))
NoCover = _Covers[_NoCoverName];
else if (_CoverExists(_NoCoverNameAlt))
NoCover = _Covers[_NoCoverNameAlt];
else
CBase.Log.LogError("Covertheme \"" + coverThemeName + "\" does not include a cover file named \"" + _NoCoverName + "\" and cannot be used!", true, true);
}
_LoadNoCoverThemes(coverTheme);
}
private static string _SongSortingToType(ESongSorting sorting)
{
switch (sorting)
{
case ESongSorting.TR_CONFIG_NONE:
return _DefaultTypeName;
case ESongSorting.TR_CONFIG_FOLDER:
return "Folder";
case ESongSorting.TR_CONFIG_ARTIST:
return "Artist";
case ESongSorting.TR_CONFIG_ARTIST_LETTER:
case ESongSorting.TR_CONFIG_TITLE_LETTER:
return "Letter";
case ESongSorting.TR_CONFIG_EDITION:
return "Edition";
case ESongSorting.TR_CONFIG_GENRE:
return "Genre";
case ESongSorting.TR_CONFIG_LANGUAGE:
return "Language";
case ESongSorting.TR_CONFIG_YEAR:
return "Year";
case ESongSorting.TR_CONFIG_DECADE:
return "Decade";
case ESongSorting.TR_CONFIG_DATEADDED:
return "Date";
default:
throw new ArgumentOutOfRangeException("sorting");
}
}
private static void _LoadNoCoverThemes(SCoverTheme coverTheme)
{
CXMLReader xmlReader = CXMLReader.OpenFile(Path.Combine(CSettings.ProgramFolder, CSettings.FolderNameCover, coverTheme.File));
List<String> types = new List<string>();
foreach (ESongSorting sorting in Enum.GetValues(typeof(ESongSorting)))
types.Add(_SongSortingToType(sorting));
string coverPath = Path.Combine(CSettings.ProgramFolder, CSettings.FolderNameCover, coverTheme.Folder);
lock (_NoCoverThemes)
{
foreach (string type in types)
{
if (_NoCoverThemes.ContainsKey(type))
continue;
CNoCoverGenerator el = new CNoCoverGenerator(xmlReader, "//root/MissingCover/" + type, coverPath);
_NoCoverThemes.Add(type, el);
}
}
}
+278
View File
@@ -0,0 +1,278 @@
#region license
// This file is part of Vocaluxe.
//
// Vocaluxe is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Vocaluxe is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Vocaluxe. If not, see <http://www.gnu.org/licenses/>.
#endregion
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Drawing.Text;
using System.IO;
using Vocaluxe.Base.Fonts;
using VocaluxeLib;
using VocaluxeLib.Draw;
using System.Linq;
namespace Vocaluxe.Base
{
/// <summary>
/// Class that generates Covers if the right Cover is missing
/// Themeable via the xml of the Cover theme
/// </summary>
class CNoCoverGenerator
{
private readonly bool _Valid;
private readonly string _Text;
private readonly SColorF _TextColor;
private readonly string _TextColorName;
private readonly float _TextMargin;
private readonly float _TextIndent;
private readonly int _TextSize;
private readonly string _Font;
private readonly EStyle _Style;
private readonly string _Image;
private readonly bool _ShowFirstCover;
private readonly float _ImageAlpha;
private const float _LineSpace = 5;
public CNoCoverGenerator(CXMLReader xmlReader, string xPath, string basePath)
{
_Valid = xmlReader.ItemExists(xPath);
if (!_Valid)
return;
_Valid &= xmlReader.GetValue(xPath + "/Text/Text", out _Text);
_Valid &= xmlReader.GetValue(xPath + "/Text/Font", out _Font);
_Valid &= xmlReader.TryGetEnumValue(xPath + "/Text/Style", ref _Style);
_Valid &= xmlReader.TryGetIntValue(xPath + "/Text/Size", ref _TextSize);
if (xmlReader.GetValue(xPath + "/Text/Color", out _TextColorName))
_Valid &= CBase.Theme.GetColor(_TextColorName, CBase.Theme.GetSkinIndex(-1), out _TextColor);
else
_Valid &= xmlReader.TryGetColorFromRGBA(xPath + "/Text", ref _TextColor);
_Valid &= xmlReader.TryGetFloatValue(xPath + "/Text/Margin", ref _TextMargin);
if (!xmlReader.TryGetFloatValue(xPath + "/Text/Indent", ref _TextIndent))
_TextIndent = 6 * _TextMargin;
_Valid &= xmlReader.GetValue(xPath + "/Image", out _Image);
string tmp;
if (xmlReader.GetValue(xPath + "/ShowFirstCover", out tmp))
Boolean.TryParse(tmp, out _ShowFirstCover);
else
_ShowFirstCover = false;
if (!xmlReader.TryGetFloatValue(xPath + "/ImageAlpha", ref _ImageAlpha))
_ImageAlpha = 0.5f;
else
_ImageAlpha = _ImageAlpha.Clamp(0f, 1f);
if (_Valid)
{
// ReSharper disable AssignNullToNotNullAttribute
_Image = Path.Combine(basePath, _Image);
// ReSharper restore AssignNullToNotNullAttribute
_Valid = File.Exists(_Image);
}
}
public CTexture GetCover(string text, string firstCoverPath)
{
if (!_Valid)
return null;
text = CLanguage.Translate(_Text.Replace("%TEXT%", text));
using (Bitmap bmp = new Bitmap(_Image))
using (Graphics g = Graphics.FromImage(bmp))
{
g.SmoothingMode = SmoothingMode.AntiAlias;
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.TextRenderingHint = TextRenderingHint.AntiAlias;
if (_ShowFirstCover && !String.IsNullOrEmpty(firstCoverPath) && File.Exists(firstCoverPath))
{
ColorMatrix cm = new ColorMatrix {Matrix33 = 1f - _ImageAlpha};
ImageAttributes ia = new ImageAttributes();
ia.SetColorMatrix(cm);
using (Bitmap bmp2 = new Bitmap(firstCoverPath))
g.DrawImage(bmp2, new Rectangle(0, 0, bmp.Width, bmp.Height), 0, 0, bmp2.Width, bmp2.Height, GraphicsUnit.Pixel, ia);
}
if (text != "")
{
List<string> lines = _SplitText(text);
CFonts.SetFont(_Font);
CFonts.Style = _Style;
float allowedWidth = bmp.Width - 2 * _TextMargin;
if (lines.Count > 1)
allowedWidth -= _TextIndent;
float allowedHeight = bmp.Height - 2 * _TextMargin - _LineSpace * (lines.Count - 1);
CFonts.Height = allowedHeight / lines.Count;
Font fo = CFonts.GetFont();
List<SizeF> sizes = lines.Select(line => g.MeasureString(line, fo, -1, StringFormat.GenericTypographic)).ToList();
float maxWidth = sizes.Select(s => s.Width).Max();
float maxHeight = sizes.Select(s => s.Height).Max();
maxHeight = (maxHeight + CFonts.Height) / 2;
if (maxWidth > allowedWidth)
{
CFonts.Height *= allowedWidth / maxWidth;
maxHeight *= allowedWidth / maxWidth;
}
if (maxHeight > _TextSize)
{
CFonts.Height *= _TextSize / maxHeight;
maxHeight = _TextSize;
}
fo = CFonts.GetFont();
//Have to use size in em not pixels!
float emSize = fo.Size * fo.FontFamily.GetCellAscent(fo.Style) / fo.FontFamily.GetEmHeight(fo.Style);
float outlineSize = CFonts.Outline * CFonts.Height;
SColorF outlineColorF = CFonts.OutlineColor;
outlineColorF.A = outlineColorF.A * _TextColor.A;
using (var path = new GraphicsPath())
using (var pen = new Pen(outlineColorF.AsColor(), outlineSize / 2))
{
pen.LineJoin = LineJoin.Round;
pen.Alignment = PenAlignment.Outset;
float top = (allowedHeight - maxHeight * lines.Count) / 2;
for (int i = 0; i < lines.Count; i++)
{
string line = lines[i];
float left;
if (lines.Count == 1 || (i == 1 && lines.Count == 3))
{
//Center Text if this is the only line or the middle line
float width = g.MeasureString(line, fo, -1, StringFormat.GenericTypographic).Width;
left = (bmp.Width - width) / 2;
}
else if (i == lines.Count - 1)
{
//Place last line at right
float width = g.MeasureString(line, fo, -1, StringFormat.GenericTypographic).Width;
left = bmp.Width - width - _TextMargin;
}
else
left = _TextMargin;
//g.DrawString(line, fo, new SolidBrush(_TextColor.AsColor()), left, top, StringFormat.GenericTypographic);
path.AddString(line, fo.FontFamily, (int)fo.Style, emSize, new PointF(left, top), StringFormat.GenericTypographic);
top += maxHeight + _LineSpace;
}
g.DrawPath(pen, path);
// ReSharper disable ImpureMethodCallOnReadonlyValueField
g.FillPath(new SolidBrush(_TextColor.AsColor()), path);
// ReSharper restore ImpureMethodCallOnReadonlyValueField
}
}
return CDraw.AddTexture(bmp);
}
}
private static List<string> _SplitText(string text)
{
text = text.Replace(".", ". ").Replace("-", "- ").Replace(" ", " ");
string[] parts = text.Split(' ');
List<string> lines = new List<string>();
string line = "";
bool appendingName = false;
foreach (string part in parts)
{
if (part.Length == 1 && !Char.IsLetter(part, 0))
{
//Add non-letter chars to the end of the line (e.g. in "19 - 20")
line = (line != "" ? " " : "") + part;
appendingName = false;
}
else if (part.Length == 1 || (part.Length == 2 && part[1] == '.'))
{
//We have something like the initials (e.g. "J. R. R. Tolkien") - Keep them together
if (!appendingName && line != "")
{
//When we are not already appending those, add the last line and start a new one
lines.Add(line);
line = part;
}
line = (line != "" ? " " : "") + part;
appendingName = true;
}
else
{
if (line != "")
lines.Add(line);
line = part;
appendingName = false;
}
}
if (line != "")
lines.Add(line);
int i;
//If we have more than 3 lines try to distribute them evenly
if (lines.Count > 3)
{
int len = lines.Sum(el => el.Length) + lines.Count - 1;
int avgLen = (int)Math.Ceiling(len / 3d);
i = 1;
while (i < lines.Count)
{
string prev = lines[i - 1];
string cur = lines[i];
if (prev.Length + cur.Length <= avgLen)
{
lines[i - 1] += " " + cur;
lines.RemoveAt(i);
}
else
i++;
}
}
//Further reduce the lines by taking the longest line as the max length
i = 1;
while (i < lines.Count)
{
string prev = lines[i - 1];
string cur = lines[i];
if (prev.Length + cur.Length <= lines.Max(el => el.Length))
{
lines[i - 1] += " " + cur;
lines.RemoveAt(i);
}
else
i++;
}
//If we still have more than 3 lines be more agressive
while (lines.Count > 3)
{
//Join the shortest line with shortest neighbour
int minLen = lines.Min(el => el.Length);
for (i = 0; i < lines.Count; i++)
{
if (lines[i].Length != minLen)
continue;
int other;
if (i == 0)
other = 1;
else if (i == lines.Count - 1)
other = i - 1;
else if (lines[i - 1].Length < lines[i + 1].Length)
other = i - 1;
else
other = i + 1;
lines[i] += " " + lines[other];
lines.RemoveAt(other);
break;
}
}
return lines;
}
}
}
+4
View File
@@ -178,7 +178,11 @@ namespace Vocaluxe.Base
}
foreach (CCategory cat in _Categories)
{
cat.CoverTextureSmall = CCover.Cover(cat.Name);
if (cat.CoverTextureSmall == CCover.NoCover)
cat.CoverTextureSmall = CCover.GenerateCover(cat.Name, CSongs.Sorter.SongSorting, cat);
}
_Changed = false;
}
}
+7 -7
View File
@@ -180,13 +180,6 @@ namespace Vocaluxe
Application.DoEvents();
// Load Cover
CLog.StartBenchmark("Init Cover");
CCover.Init();
CLog.StopBenchmark("Init Cover");
Application.DoEvents();
// Theme System
CLog.StartBenchmark("Init Theme");
if (!CTheme.Init())
@@ -195,6 +188,13 @@ namespace Vocaluxe
Application.DoEvents();
// Load Cover
CLog.StartBenchmark("Init Cover");
CCover.Init();
CLog.StopBenchmark("Init Cover");
Application.DoEvents();
// Init Screens
CLog.StartBenchmark("Init Screens");
CGraphics.Init();
+1
View File
@@ -216,6 +216,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Base\CBackgroundMusic.cs" />
<Compile Include="Base\CNoCoverGenerator.cs" />
<Compile Include="Base\CPlaylistElement.cs" />
<Compile Include="Base\CRecord.cs" />
<Compile Include="Base\CVideoPlayer.cs" />