using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Windows; using System.Windows.Documents; using System.Windows.Media; using Jewelry.Collections; using WPFDark.Internals; namespace WPFDark.Internals { internal class TextRenderer : TextRendererImpl { internal static TextRenderer Italic => LazyInitializer.EnsureInitialized(ref _Italic, () => { var fontFamily = (FontFamily) TextElement.FontFamilyProperty.DefaultMetadata.DefaultValue; var fontSize = (double) TextElement.FontSizeProperty.DefaultMetadata.DefaultValue; return new TextRenderer( fontFamily, fontSize, FontStyles.Italic, FontWeights.Normal, FontStretches.Normal); }); private static TextRenderer? _Italic; internal TextRenderer( FontFamily fontFamily, double fontSize, FontStyle style, FontWeight weight, FontStretch stretch) : base(fontFamily, fontSize, style, weight, stretch) { } } internal class DefaultTextRenderer : TextRendererImpl { internal static readonly DefaultTextRenderer Instance; static DefaultTextRenderer() { var fontFamily = (FontFamily) ThemeManager.Current.TryFindResource("WPFDarkFontFamily"); var fontSize = (double) TextElement.FontSizeProperty.DefaultMetadata.DefaultValue; Instance = new DefaultTextRenderer( fontFamily, fontSize, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal); } internal DefaultTextRenderer( FontFamily fontFamily, double fontSize, FontStyle style, FontWeight weight, FontStretch stretch) : base(fontFamily, fontSize, style, weight, stretch) { } } internal struct IsDefaultTextureRenderer { } internal struct IsNotDefaultTextureRenderer { } internal partial class TextRendererImpl { internal TextRendererImpl( FontFamily fontFamily, double fontSize, FontStyle style, FontWeight weight, FontStretch stretch) { var typeface = new Typeface(fontFamily, style, weight, stretch); if (typeface.TryGetGlyphTypeface(out _glyphTypeface) == false) { // エラーの場合はデフォルトで作り直す typeface = new Typeface( (FontFamily) TextElement.FontFamilyProperty.DefaultMetadata.DefaultValue, (FontStyle) TextElement.FontStyleProperty.DefaultMetadata.DefaultValue, (FontWeight) TextElement.FontWeightProperty.DefaultMetadata.DefaultValue, (FontStretch) TextElement.FontStretchProperty.DefaultMetadata.DefaultValue); // デフォルトでもだめなら以降処理しない if (typeface.TryGetGlyphTypeface(out _glyphTypeface) == false) return; } _fontSize = fontSize; if (typeof(TIsDefault) == typeof(IsDefaultTextureRenderer)) { _fontLineSpacing = DefaultFontLineSpacing; _dotGlyphIndex = DefaultDotGlyphIndex; _dotAdvanceWidth = DefaultDotAdvanceWidth; } else { _glyphDataCache = new Dictionary(); _toGlyphMap = _glyphTypeface.CharacterToGlyphMap; _advanceWidthsDict = _glyphTypeface.AdvanceWidths; _fontLineSpacing = fontFamily.LineSpacing; (_dotGlyphIndex, _dotAdvanceWidth) = CalcGlyphIndexAndWidth('.'); } #if DEBUG // グリフデータテーブルを作る // 作ったものは、手動でソースコードに組み込む。 //MakeGlyphDataTable(fontFamily, _glyphTypeface, _fontSize); #endif } #if !NETCOREAPP3_1 [MethodImpl(MethodImplOptions.AggressiveInlining)] internal double Draw( Visual visual, string text, double x, double y, Brush brush, DrawingContext dc, double maxWidth, TextAlignment align, BiaTextTrimmingMode trimming, bool isUseCache) { return Draw( visual, text.AsSpan(), x, y, brush, dc, maxWidth, align, trimming, isUseCache); } internal double CalcWidth(string text) { return CalcWidth(text.AsSpan()); } #endif internal double Draw( Visual visual, ReadOnlySpan text, double x, double y, Brush brush, DrawingContext dc, double maxWidth, TextAlignment align, BiaTextTrimmingMode trimming, bool isUseCache) { if (NumberHelper.AreCloseZero(_fontSize)) return 0; if (text.Length == 0) return 0; maxWidth = Math.Ceiling(maxWidth); if (maxWidth <= 0) return 0; var gr = trimming switch { BiaTextTrimmingMode.None => MakeGlyphRunNone(visual, text, maxWidth, isUseCache), BiaTextTrimmingMode.Standard => MakeGlyphRunStandard(visual, text, maxWidth, isUseCache), BiaTextTrimmingMode.Filepath => MakeGlyphRunFilepath(visual, text, maxWidth, isUseCache), _ => throw new ArgumentOutOfRangeException(nameof(trimming), trimming, null) }; if (gr == default) return 0; switch (align) { case TextAlignment.Left: break; case TextAlignment.Right: x += maxWidth - gr.Width; break; case TextAlignment.Center: x += (maxWidth - gr.Width) / 2; break; default: throw new ArgumentOutOfRangeException(nameof(align), align, null); } if (NumberHelper.AreCloseZero(x) && NumberHelper.AreCloseZero(y)) { dc.DrawGlyphRun(brush, gr.GlyphRun); } else { var hash = HashCodeMaker.Make(x, y); if (_translateCache.TryGetValue(hash, out var t) == false) { t = new TranslateTransform(x, y); _translateCache.Add(hash, t); } dc.PushTransform(t); dc.DrawGlyphRun(brush, gr.GlyphRun); dc.Pop(); } return gr.Width; } #if NETCOREAPP3_1 [MethodImpl(MethodImplOptions.AggressiveOptimization)] #endif internal double CalcWidth(ReadOnlySpan text) { if (NumberHelper.AreCloseZero(_fontSize)) return 0; if (text.Length == 0) return 0; var textHashCode = HashCodeMaker.Make(text); if (_textWidthCache.TryGetValue(textHashCode, out var textWidth)) return textWidth; foreach (var c in text) textWidth += CalcWidth(c); _textWidthCache.Add(textHashCode, textWidth); return textWidth; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal double CalcWidth(char c) { if (typeof(TIsDefault) == typeof(IsDefaultTextureRenderer)) { return Unsafe.Add(ref GetDefaultAdvanceWidthTable(), (IntPtr) c); } else { Debug.Assert(_glyphDataCache != null); Debug.Assert(_toGlyphMap != null); Debug.Assert(_advanceWidthsDict != null); if (_glyphDataCache.TryGetValue(c, out var data) == false) { if (_toGlyphMap.TryGetValue(c, out data.GlyphIndex) == false) _toGlyphMap.TryGetValue(' ', out data.GlyphIndex); data.AdvanceWidth = _advanceWidthsDict[data.GlyphIndex] * _fontSize; _glyphDataCache.Add(c, data); } return data.AdvanceWidth; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal (ushort GlyphIndex, double AdvanceWidth) CalcGlyphIndexAndWidth(char c) { if (typeof(TIsDefault) == typeof(IsDefaultTextureRenderer)) { var glyphIndex = Unsafe.Add(ref GetDefaultGlyphIndexTable(), (IntPtr) c); var advanceWidth = Unsafe.Add(ref GetDefaultAdvanceWidthTable(), (IntPtr) c); return (glyphIndex, advanceWidth); } else { Debug.Assert(_glyphDataCache != null); Debug.Assert(_toGlyphMap != null); Debug.Assert(_advanceWidthsDict != null); if (_glyphDataCache.TryGetValue(c, out var data) == false) { if (_toGlyphMap.TryGetValue(c, out data.GlyphIndex) == false) _toGlyphMap.TryGetValue(' ', out data.GlyphIndex); data.AdvanceWidth = _advanceWidthsDict[data.GlyphIndex] * _fontSize; _glyphDataCache.Add(c, data); } return (data.GlyphIndex, data.AdvanceWidth); } } internal double FontHeight => _fontLineSpacing * _fontSize; #if NETCOREAPP3_1 [MethodImpl(MethodImplOptions.AggressiveOptimization)] #endif private (GlyphRun GlyphRun, double Width) MakeGlyphRunNone(Visual visual, ReadOnlySpan text, double maxWidth, bool isUseCache) { var srcTextLength = text.Length; // ReSharper disable MergeConditionalExpression var glyphIndexesArray = text.Length >= 128 ? ArrayPool.Shared.Rent(srcTextLength) : null; var advanceWidthsArray = text.Length >= 128 ? ArrayPool.Shared.Rent(srcTextLength) : null; var glyphIndexes = glyphIndexesArray != null ? glyphIndexesArray.AsSpan(0, srcTextLength) : stackalloc ushort[srcTextLength]; var advanceWidths = advanceWidthsArray != null ? advanceWidthsArray.AsSpan(0, srcTextLength) : stackalloc double[srcTextLength]; // ReSharper restore MergeConditionalExpression try { var textWidth = 0.0; var isTrimmed = false; var newCount = 0; { for (var i = 0; i != text.Length; ++i) { (glyphIndexes[i], advanceWidths[i]) = CalcGlyphIndexAndWidth(text[i]); var oldTextWidth = textWidth; textWidth += advanceWidths[i]; if (textWidth > maxWidth) { textWidth = oldTextWidth; newCount = i - 1; isTrimmed = true; break; } } } if (isTrimmed && newCount <= 0) return default; if (NumberHelper.AreCloseZero(textWidth)) return default; var dpi = visual.PixelsPerDip(); long textKey = default; if (isUseCache) { textKey = MakeHashCode(text, textWidth, dpi, BiaTextTrimmingMode.None); if (_textCache.TryGetValue(textKey, out var cachedGr)) return cachedGr; } var textLength = isTrimmed ? newCount : text.Length; var newGlyphIndexes = new ushort[textLength]; var newAdvanceWidths = new double[textLength]; glyphIndexes.Slice(0, textLength).CopyTo(newGlyphIndexes.AsSpan()); advanceWidths.Slice(0, textLength).CopyTo(newAdvanceWidths.AsSpan()); var gr = (new GlyphRun( _glyphTypeface, 0, false, _fontSize, (float) dpi, newGlyphIndexes, new Point(0, _glyphTypeface!.Baseline * _fontSize), newAdvanceWidths, null, null, null, null, null, null), textWidth); if (isUseCache) _textCache.Add(textKey, gr); return gr; } finally { if (glyphIndexesArray != null) ArrayPool.Shared.Return(glyphIndexesArray); if (advanceWidthsArray != null) ArrayPool.Shared.Return(advanceWidthsArray); } } #if NETCOREAPP3_1 [MethodImpl(MethodImplOptions.AggressiveOptimization)] #endif private (GlyphRun GlyphRun, double Width) MakeGlyphRunStandard(Visual visual, ReadOnlySpan text, double maxWidth, bool isUseCache) { // ※ +3 「...」 が増えることがあるためのバッファ var srcTextLength = text.Length + 3; // ReSharper disable MergeConditionalExpression var glyphIndexesArray = text.Length >= 128 ? ArrayPool.Shared.Rent(srcTextLength) : null; var advanceWidthsArray = text.Length >= 128 ? ArrayPool.Shared.Rent(srcTextLength) : null; var glyphIndexes = glyphIndexesArray != null ? glyphIndexesArray.AsSpan(0, srcTextLength) : stackalloc ushort[srcTextLength]; var advanceWidths = advanceWidthsArray != null ? advanceWidthsArray.AsSpan(0, srcTextLength) : stackalloc double[srcTextLength]; // ReSharper restore MergeConditionalExpression try { var textWidth = 0.0; var isTrimmed = false; var newCount = 0; { for (var i = 0; i != text.Length; ++i) { (glyphIndexes[i], advanceWidths[i]) = CalcGlyphIndexAndWidth(text[i]); textWidth += advanceWidths[i]; if (textWidth > maxWidth) { (textWidth, newCount) = TrimGlyphRunStandard(glyphIndexes, advanceWidths, textWidth, maxWidth, i + 1); isTrimmed = true; break; } } } if (NumberHelper.AreCloseZero(textWidth)) return default; var dpi = visual.PixelsPerDip(); long textKey = default; if (isUseCache) { textKey = MakeHashCode(text, textWidth, dpi, BiaTextTrimmingMode.None); if (_textCache.TryGetValue(textKey, out var cachedGr)) return cachedGr; } var textLength = isTrimmed ? newCount : text.Length; var newGlyphIndexes = new ushort[textLength]; var newAdvanceWidths = new double[textLength]; glyphIndexes.Slice(0, textLength).CopyTo(newGlyphIndexes.AsSpan()); advanceWidths.Slice(0, textLength).CopyTo(newAdvanceWidths.AsSpan()); if (_glyphTypeface is null) throw new InvalidDataException(); var gr = (new GlyphRun( _glyphTypeface, 0, false, _fontSize, (float) dpi, newGlyphIndexes, new Point(0, _glyphTypeface.Baseline * _fontSize), newAdvanceWidths, null, null, null, null, null, null), textWidth); if (isUseCache) _textCache.Add(textKey, gr); return gr; } finally { if (glyphIndexesArray != null) ArrayPool.Shared.Return(glyphIndexesArray); if (advanceWidthsArray != null) ArrayPool.Shared.Return(advanceWidthsArray); } } private (double Width, int NewCount) TrimGlyphRunStandard( Span glyphIndexes, Span advanceWidths, double textWidth, double maxWidth, int bufferSize) { Debug.Assert(textWidth > maxWidth); // 文字列に ... を加える分を考慮して削る文字数を求める var dot3Width = _dotAdvanceWidth * 3.0; var removeCount = 1; var newTextWidth = textWidth; { for (var i = bufferSize - 1; i >= 0; --i) { newTextWidth -= advanceWidths[i]; if (maxWidth - newTextWidth >= dot3Width) break; ++removeCount; } } var newCount = bufferSize - removeCount + 3; if (newCount < 3) return (0.0, 0); // 文字列に ... を追加する glyphIndexes[newCount - 1 - 2] = _dotGlyphIndex; glyphIndexes[newCount - 1 - 1] = _dotGlyphIndex; glyphIndexes[newCount - 1 - 0] = _dotGlyphIndex; advanceWidths[newCount - 1 - 2] = _dotAdvanceWidth; advanceWidths[newCount - 1 - 1] = _dotAdvanceWidth; advanceWidths[newCount - 1 - 0] = _dotAdvanceWidth; return (newTextWidth + dot3Width, newCount); } #if NETCOREAPP3_1 [MethodImpl(MethodImplOptions.AggressiveOptimization)] #endif private (GlyphRun GlyphRun, double Width) MakeGlyphRunFilepath(Visual visual, ReadOnlySpan text, double maxWidth, bool isUseCache) { var buffer = ArrayPool.Shared.Rent(text.Length); try { if (CalcWidth(text) > maxWidth) text = TrimmingFilepathText(text, maxWidth, buffer); return MakeGlyphRunStandard(visual, text, maxWidth, isUseCache); } finally { ArrayPool.Shared.Return(buffer); } } #if NETCOREAPP3_1 [MethodImpl(MethodImplOptions.AggressiveOptimization)] #endif private ReadOnlySpan TrimmingFilepathText(ReadOnlySpan text, double maxWidth, char[] buffer) { // ref: https://www.codeproject.com/Tips/467054/WPF-PathTrimmingTextBlock bool widthOk; #if NETCOREAPP3_1 var filename = Path.GetFileName(text); var directory = Path.GetDirectoryName(text); #else var textString = text.ToString(); var filename = Path.GetFileName(textString).AsSpan(); var directory = Path.GetDirectoryName(textString).AsSpan(); #endif var changedWidth = false; var sepSpan = "...\\".AsSpan(); var sepWidth = CalcWidth(sepSpan); var filepathWidth = CalcWidth(filename); var directoryWidth = CalcWidth(directory); do { var pathWidth = directoryWidth + sepWidth + filepathWidth; widthOk = pathWidth < maxWidth; if (widthOk == false) { changedWidth = true; directoryWidth -= CalcWidth(directory[directory.Length - 1]); directory = directory.Slice(0, directory.Length - 1); if (directory.Length == 0) { // "...\\" + new string(filename); sepSpan.CopyTo(buffer); filename.CopyTo(buffer.AsSpan(sepSpan.Length)); return buffer.AsSpan(0, sepSpan.Length + filename.Length); } } } while (widthOk == false); if (changedWidth) { // new string(directory) + "...\\" + new string(filename); directory.CopyTo(buffer); sepSpan.CopyTo(buffer.AsSpan(directory.Length)); filename.CopyTo(buffer.AsSpan(directory.Length + sepSpan.Length)); return buffer.AsSpan(0, directory.Length + sepSpan.Length + filename.Length); } else return text; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static long MakeHashCode( ReadOnlySpan text, double textWidth, double dpi, BiaTextTrimmingMode textTrimming) { unchecked { var hashCode = HashCodeMaker.Make(text); hashCode = (hashCode * 397) ^ (long) textTrimming; return (hashCode * 397) ^ HashCodeMaker.Make(textWidth, dpi); } } private readonly LruCache _textCache = new LruCache(10_0000); private readonly LruCache _textWidthCache = new LruCache(10_0000); private readonly LruCache _translateCache = new LruCache(1000); private readonly IDictionary? _toGlyphMap; private readonly IDictionary? _advanceWidthsDict; // 最大65536エントリ private readonly Dictionary? _glyphDataCache; private readonly GlyphTypeface? _glyphTypeface; private readonly ushort _dotGlyphIndex; private readonly double _dotAdvanceWidth; private readonly double _fontSize; private readonly double _fontLineSpacing; #if DEBUG // ReSharper disable once UnusedMember.Local private static void MakeGlyphDataTable(FontFamily fontFamily, GlyphTypeface glyphTypeface, double fontSize) { var toGlyphMap = glyphTypeface.CharacterToGlyphMap; var advanceWidthsDict = glyphTypeface.AdvanceWidths; toGlyphMap.TryGetValue(' ', out var dummyIndex); var glyphIndexArray = new ushort[char.MaxValue + 1]; var advanceWidthArray = new double[char.MaxValue + 1]; for (var i = 0; i <= char.MaxValue; ++i) { if (toGlyphMap.TryGetValue(i, out var glyphIndex) == false) glyphIndex = dummyIndex; var advanceWidth = advanceWidthsDict[glyphIndex] * fontSize; glyphIndexArray[i] = glyphIndex; advanceWidthArray[i] = advanceWidth; } var glyphIndexByteArray = MemoryMarshal.Cast(glyphIndexArray).ToArray(); var advanceWidthByteArray = MemoryMarshal.Cast(advanceWidthArray).ToArray(); var dotGlyphIndex = Unsafe.Add(ref MemoryMarshal.GetReference(MemoryMarshal.Cast(glyphIndexByteArray)), (IntPtr) '.'); var dotAdvanceWidth = Unsafe.Add(ref MemoryMarshal.GetReference(MemoryMarshal.Cast(advanceWidthByteArray)), (IntPtr) '.'); var sb = new StringBuilder(); sb.AppendLine("// ReSharper disable All"); sb.AppendLine("using System;"); sb.AppendLine("using System.Runtime.CompilerServices;"); sb.AppendLine("using System.Runtime.InteropServices;"); sb.AppendLine("namespace WPFDark.Internals"); sb.AppendLine("{"); sb.AppendLine("internal partial class TextRendererImpl"); sb.AppendLine("{"); sb.AppendLine($"private const double DefaultFontLineSpacing = {fontFamily.LineSpacing};"); sb.AppendLine($"private const ushort DefaultDotGlyphIndex = {dotGlyphIndex};"); sb.AppendLine($"private const double DefaultDotAdvanceWidth = {dotAdvanceWidth};"); sb.AppendLine("[MethodImpl(MethodImplOptions.AggressiveInlining)]"); sb.AppendLine("private static ref ushort GetDefaultGlyphIndexTable(){"); sb.AppendLine("var byteTable = new ReadOnlySpan(new byte[] {"); sb.AppendLine(JoinStrings(glyphIndexByteArray)); sb.AppendLine("});"); sb.AppendLine("return ref MemoryMarshal.GetReference(MemoryMarshal.Cast(byteTable));"); sb.AppendLine("}"); sb.AppendLine("[MethodImpl(MethodImplOptions.AggressiveInlining)]"); sb.AppendLine("private static ref double GetDefaultAdvanceWidthTable(){"); sb.AppendLine("var byteTable = new ReadOnlySpan(new byte[] {"); sb.AppendLine(JoinStrings(advanceWidthByteArray)); sb.AppendLine("});"); sb.AppendLine("return ref MemoryMarshal.GetReference(MemoryMarshal.Cast(byteTable));"); sb.AppendLine("}"); sb.AppendLine("}"); sb.AppendLine("}"); var outputDir = Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile), "TextRenderer.table.cs"); File.WriteAllText(outputDir, sb.ToString()); } private static string JoinStrings(IEnumerable source) { var sb = new StringBuilder(); var c = 0; foreach (var v in source) { ++c; sb.Append($"0x{v:x2}"); if (c > 0 && (c % 32 == 0)) sb.AppendLine(","); else sb.Append(','); } return sb.ToString(); } #endif } }