From 3f4f2776beaac948f2f94b9a2a3ea3a429042b3d Mon Sep 17 00:00:00 2001 From: Flamefire Date: Thu, 14 Aug 2014 16:07:49 +0200 Subject: [PATCH] Add first working implementation of the cover generator TODO: Speedup --- Output/Cover/Blue.xml | 38 ++++ Vocaluxe/Base/CCover.cs | 87 ++++++++- Vocaluxe/Base/CNoCoverGenerator.cs | 278 +++++++++++++++++++++++++++++ Vocaluxe/Base/CSongCategorizer.cs | 4 + Vocaluxe/Program.cs | 14 +- Vocaluxe/Vocaluxe.csproj | 1 + 6 files changed, 412 insertions(+), 10 deletions(-) create mode 100644 Vocaluxe/Base/CNoCoverGenerator.cs diff --git a/Output/Cover/Blue.xml b/Output/Cover/Blue.xml index 9befe7d3..069bb79c 100644 --- a/Output/Cover/Blue.xml +++ b/Output/Cover/Blue.xml @@ -5,4 +5,42 @@ Blue flokuep + + + + + %TEXT% + + Outline + + + 500 + + + 0110.5 + 24 + 100 + + EmptyCover.png + true + 0.8 + + + + + %TEXT% + + Outline + + + 500 + + + 0100.5 + 24 + 100 + + EmptyCover.png + + diff --git a/Vocaluxe/Base/CCover.cs b/Vocaluxe/Base/CCover.cs index 61692ce7..177444d8 100644 --- a/Vocaluxe/Base/CCover.cs +++ b/Vocaluxe/Base/CCover.cs @@ -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 _Covers = new Dictionary(); + private static readonly Dictionary _NoCoverThemes = new Dictionary(); private static readonly List _CoverThemes = new List(); 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; + } + /// /// 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(); + } } /// @@ -168,7 +195,7 @@ namespace Vocaluxe.Base } /// - /// Loads all cover which are defined in the cover config file. + /// Loads all covers from the theme /// 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 types = new List(); + 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); + } } } diff --git a/Vocaluxe/Base/CNoCoverGenerator.cs b/Vocaluxe/Base/CNoCoverGenerator.cs new file mode 100644 index 00000000..9f5d8a97 --- /dev/null +++ b/Vocaluxe/Base/CNoCoverGenerator.cs @@ -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 . +#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 +{ + /// + /// Class that generates Covers if the right Cover is missing + /// Themeable via the xml of the Cover theme + /// + 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 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 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 _SplitText(string text) + { + text = text.Replace(".", ". ").Replace("-", "- ").Replace(" ", " "); + string[] parts = text.Split(' '); + List lines = new List(); + 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; + } + } +} \ No newline at end of file diff --git a/Vocaluxe/Base/CSongCategorizer.cs b/Vocaluxe/Base/CSongCategorizer.cs index 4c16bf3a..7104c795 100644 --- a/Vocaluxe/Base/CSongCategorizer.cs +++ b/Vocaluxe/Base/CSongCategorizer.cs @@ -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; } } diff --git a/Vocaluxe/Program.cs b/Vocaluxe/Program.cs index b4720e87..a886ae3d 100644 --- a/Vocaluxe/Program.cs +++ b/Vocaluxe/Program.cs @@ -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(); diff --git a/Vocaluxe/Vocaluxe.csproj b/Vocaluxe/Vocaluxe.csproj index b8276467..0c355598 100644 --- a/Vocaluxe/Vocaluxe.csproj +++ b/Vocaluxe/Vocaluxe.csproj @@ -216,6 +216,7 @@ +