Add first working implementation of the cover generator
TODO: Speedup
This commit is contained in:
1 parent
388bd6310f
commit
3f4f2776be
6 files changed
+412
-10
No files matched your search
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in new issue
Block a user