Files
ShrlAlgoToolkit/WPFDark/Internals/TextRenderer.cs
ShrlAlgo 4d35cadb56 更新
2025-07-11 09:20:23 +08:00

778 lines
27 KiB
C#

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<IsNotDefaultTextureRenderer>
{
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<IsDefaultTextureRenderer>
{
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<TIsDefault>
{
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<int, (ushort GlyphIndex, double AdvanceWidth)>();
_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<char> 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<char> 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<char> text, double maxWidth, bool isUseCache)
{
var srcTextLength = text.Length;
// ReSharper disable MergeConditionalExpression
var glyphIndexesArray =
text.Length >= 128
? ArrayPool<ushort>.Shared.Rent(srcTextLength)
: null;
var advanceWidthsArray =
text.Length >= 128
? ArrayPool<double>.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<ushort>.Shared.Return(glyphIndexesArray);
if (advanceWidthsArray != null)
ArrayPool<double>.Shared.Return(advanceWidthsArray);
}
}
#if NETCOREAPP3_1
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
#endif
private (GlyphRun GlyphRun, double Width) MakeGlyphRunStandard(Visual visual, ReadOnlySpan<char> text, double maxWidth, bool isUseCache)
{
// ※ +3 「...」 が増えることがあるためのバッファ
var srcTextLength = text.Length + 3;
// ReSharper disable MergeConditionalExpression
var glyphIndexesArray =
text.Length >= 128
? ArrayPool<ushort>.Shared.Rent(srcTextLength)
: null;
var advanceWidthsArray =
text.Length >= 128
? ArrayPool<double>.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<ushort>.Shared.Return(glyphIndexesArray);
if (advanceWidthsArray != null)
ArrayPool<double>.Shared.Return(advanceWidthsArray);
}
}
private (double Width, int NewCount) TrimGlyphRunStandard(
Span<ushort> glyphIndexes,
Span<double> 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<char> text, double maxWidth, bool isUseCache)
{
var buffer = ArrayPool<char>.Shared.Rent(text.Length);
try
{
if (CalcWidth(text) > maxWidth)
text = TrimmingFilepathText(text, maxWidth, buffer);
return MakeGlyphRunStandard(visual, text, maxWidth, isUseCache);
}
finally
{
ArrayPool<char>.Shared.Return(buffer);
}
}
#if NETCOREAPP3_1
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
#endif
private ReadOnlySpan<char> TrimmingFilepathText(ReadOnlySpan<char> 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<char> 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<long, (GlyphRun, double)> _textCache = new LruCache<long, (GlyphRun, double)>(10_0000);
private readonly LruCache<long, double> _textWidthCache = new LruCache<long, double>(10_0000);
private readonly LruCache<long, TranslateTransform> _translateCache = new LruCache<long, TranslateTransform>(1000);
private readonly IDictionary<int, ushort>? _toGlyphMap;
private readonly IDictionary<ushort, double>? _advanceWidthsDict;
// 最大65536エントリ
private readonly Dictionary<int, (ushort GlyphIndex, double AdvanceWidth)>? _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<ushort, byte>(glyphIndexArray).ToArray();
var advanceWidthByteArray = MemoryMarshal.Cast<double, byte>(advanceWidthArray).ToArray();
var dotGlyphIndex = Unsafe.Add(ref MemoryMarshal.GetReference(MemoryMarshal.Cast<byte, ushort>(glyphIndexByteArray)), (IntPtr) '.');
var dotAdvanceWidth = Unsafe.Add(ref MemoryMarshal.GetReference(MemoryMarshal.Cast<byte, double>(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<TIsDefault>");
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<byte>(new byte[] {");
sb.AppendLine(JoinStrings(glyphIndexByteArray));
sb.AppendLine("});");
sb.AppendLine("return ref MemoryMarshal.GetReference(MemoryMarshal.Cast<byte, ushort>(byteTable));");
sb.AppendLine("}");
sb.AppendLine("[MethodImpl(MethodImplOptions.AggressiveInlining)]");
sb.AppendLine("private static ref double GetDefaultAdvanceWidthTable(){");
sb.AppendLine("var byteTable = new ReadOnlySpan<byte>(new byte[] {");
sb.AppendLine(JoinStrings(advanceWidthByteArray));
sb.AppendLine("});");
sb.AppendLine("return ref MemoryMarshal.GetReference(MemoryMarshal.Cast<byte, double>(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<T>(IEnumerable<T> 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
}
}