Skip to content

Commit b2dda00

Browse files
committed
refactor(render): Refactor rendering engine to support animated images and optimize resource management
- Standardize on the IList interface in Drawer and refactor data models to simplify template variables - Add image caching to AssetProvider and optimize resource path resolution logic - Implement animation metadata inheritance in NodeRenderer to support GIF/PNG/WEBP animation frames - Remove hardcoded image resizing logic from ServerStreamWriterExtensions - Refactor TemplateReader expression evaluation and remove redundant EvaluateSetValueAsync method - Fix null reference issue in asynchronous function evaluation within AsyncNCalcEngine - Add measurement support for ResizedNode to improve the node rendering system
1 parent b1648fd commit b2dda00

File tree

9 files changed

+181
-118
lines changed

9 files changed

+181
-118
lines changed

src/Render/AssetProvider.cs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ namespace Limekuma.Render;
88

99
public sealed class AssetProvider
1010
{
11+
private readonly ConcurrentDictionary<string, Image> _assets;
1112
private readonly ConcurrentDictionary<string, LoadedFont> _fontCache;
1213
private readonly Dictionary<string, (string, List<string>?)> _fontRules;
1314
private readonly Dictionary<string, (string, string?)> _pathRules;
@@ -20,6 +21,7 @@ public AssetProvider(string resourcePath)
2021
{
2122
_fontRules = [];
2223
_pathRules = [];
24+
_assets = [];
2325
_fontCache = [];
2426
LoadResources(resourcePath);
2527
}
@@ -29,7 +31,7 @@ public AssetProvider(string resourcePath)
2931
public Image LoadImage(string ns, string key)
3032
{
3133
string path = ResolveResourcePath(ns, key);
32-
return Image.Load(path);
34+
return LoadImage(path);
3335
}
3436

3537
public (FontFamily, List<FontFamily>) ResolveFont(string key)
@@ -55,7 +57,7 @@ public Size Measure(string text, string family, float size)
5557
(FontFamily fontFamily, List<FontFamily> fallbacks) = ResolveFont(family);
5658
float scaledSize = (float)(size * 72 / 300d);
5759
Font font = fontFamily.CreateFont(scaledSize);
58-
FontRectangle rect = TextMeasurer.MeasureAdvance(text, new TextOptions(font)
60+
FontRectangle rect = TextMeasurer.MeasureAdvance(text, new(font)
5961
{
6062
FallbackFontFamilies = fallbacks,
6163
Dpi = 300
@@ -71,7 +73,7 @@ public Size Measure(string text, string family, float size)
7173
}
7274

7375
(string path, _) = pathRule;
74-
return Path.GetFullPath(path);
76+
return path;
7577
}
7678

7779
private void LoadResources(string resourcePath)
@@ -103,7 +105,7 @@ private void LoadPathRules(XElement resourcesNode)
103105
continue;
104106
}
105107

106-
_pathRules[ns] = new(Path.GetFullPath(path), node.Attribute("rule")?.Value);
108+
_pathRules[ns] = new(path, node.Attribute("rule")?.Value);
107109
}
108110
}
109111

@@ -140,7 +142,7 @@ private string ResolveResourcePath(string ns, string key)
140142
{
141143
if (!_pathRules.TryGetValue(ns, out (string, string?) pathRule))
142144
{
143-
return Path.GetFullPath(key);
145+
return key;
144146
}
145147

146148
(string path, string? rule) = pathRule;
@@ -152,10 +154,20 @@ private string ResolveResourcePath(string ns, string key)
152154
return Path.Combine(path, Smart.Format(rule, new { key }));
153155
}
154156

157+
private Image LoadImage(string path)
158+
{
159+
Image loaded = _assets.GetOrAdd(path, p =>
160+
{
161+
Image image = Image.Load(path);
162+
return image;
163+
});
164+
165+
return loaded;
166+
}
167+
155168
private FontFamily LoadFont(string path)
156169
{
157-
string fullPath = Path.GetFullPath(path);
158-
LoadedFont loaded = _fontCache.GetOrAdd(fullPath, p =>
170+
LoadedFont loaded = _fontCache.GetOrAdd(path, p =>
159171
{
160172
FontCollection collection = new();
161173
FontFamily family = collection.Add(p);

src/Render/Drawer.cs

Lines changed: 45 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,67 +2,47 @@
22
using Limekuma.Render.ExpressionEngine;
33
using Limekuma.Render.Nodes;
44
using SixLabors.ImageSharp;
5+
using System.Collections;
56

67
namespace Limekuma.Render;
78

89
public sealed class Drawer
910
{
10-
public async Task<Image> DrawBestsAsync(CommonUser user, IEnumerable<CommonRecord> ever,
11-
IEnumerable<CommonRecord> current, int everTotal, int currentTotal, string typename, string prober) =>
11+
public async Task<Image> DrawBestsAsync(CommonUser user, IList<CommonRecord> ever,
12+
IList<CommonRecord> current, int everTotal, int currentTotal, string typename, string prober) =>
1213
await DrawBestsAsync(user, ever, current, everTotal, currentTotal, typename, prober, false, false);
1314

14-
public async Task<Image> DrawBestsAsync(CommonUser user, IEnumerable<CommonRecord> ever,
15-
IEnumerable<CommonRecord> current, int everTotal, int currentTotal, string typename, string prober,
15+
public async Task<Image> DrawBestsAsync(CommonUser user, IList<CommonRecord> ever,
16+
IList<CommonRecord> current, int everTotal, int currentTotal, string typename, string prober,
1617
bool isAnime) =>
1718
await DrawBestsAsync(user, ever, current, everTotal, currentTotal, typename, prober, isAnime, false);
1819

19-
public async Task<Image> DrawBestsAsync(CommonUser user, IEnumerable<CommonRecord> ever,
20-
IEnumerable<CommonRecord> current, int everTotal, int currentTotal, string typename, string prober,
20+
public async Task<Image> DrawBestsAsync(CommonUser user, IList<CommonRecord> ever,
21+
IList<CommonRecord> current, int everTotal, int currentTotal, string typename, string prober,
2122
bool isAnime, bool drawLevelSeg) => await DrawBestsAsync(user, ever, current, everTotal, currentTotal, typename,
2223
prober, isAnime, drawLevelSeg, "./Resources/Layouts/bests.xml");
2324

24-
public async Task<Image> DrawBestsAsync(CommonUser user, IEnumerable<CommonRecord> ever,
25-
IEnumerable<CommonRecord> current, int everTotal, int currentTotal, string typename, string prober,
25+
public async Task<Image> DrawBestsAsync(CommonUser user, IList<CommonRecord> ever,
26+
IList<CommonRecord> current, int everTotal, int currentTotal, string typename, string prober,
2627
bool isAnime, bool drawLevelSeg, string xmlPath)
2728
{
28-
List<CommonRecord> everList = [.. ever];
29-
List<CommonRecord> currentList = [.. current];
30-
List<object> everCards = [.. everList.Select((record, idx) => new { Record = record, Index = idx + 1 })];
31-
List<object> currentCards =
32-
[.. currentList.Select((record, idx) => new { Record = record, Index = idx + everList.Count + 1 })];
33-
int everDelta = everList.Count > 34 ? everList[0].DXRating - everList[^1].DXRating : everList.Count > 0 ? everList[0].DXRating : 0;
34-
int currentDelta = currentList.Count > 14 ? currentList[0].DXRating - currentList[^1].DXRating : currentList.Count > 0 ? currentList[0].DXRating : 0;
35-
int everMax = everList.Count > 0 ? everList[0].DXRating : 0;
36-
int everMin = everList.Count > 0 ? everList[^1].DXRating : 0;
37-
int currentMax = currentList.Count > 0 ? currentList[0].DXRating : 0;
38-
int currentMin = currentList.Count > 0 ? currentList[^1].DXRating : 0;
39-
int realRating = everTotal + currentTotal;
40-
string proberState = "on";
41-
if (user.Rating != realRating)
29+
int everMax = ever.Count > 0 ? ever[0].DXRating : 0;
30+
int everMin = ever.Count > 0 ? ever[^1].DXRating : 0;
31+
int currentMax = current.Count > 0 ? current[0].DXRating : 0;
32+
int currentMin = current.Count > 0 ? current[^1].DXRating : 0;
33+
bool mayMask = ever.Any(r => r.DXScore is 0 && (r.DXStar > 0 || r.Rank > Ranks.A)) || current.Any(r => r.DXScore is 0 && (r.DXStar > 0 || r.Rank > Ranks.A));
34+
Dictionary<string, object> scope = new(StringComparer.OrdinalIgnoreCase)
4235
{
43-
proberState = "off";
44-
}
45-
else if (everList.Any(r => r.DXScore is 0 && (r.DXStar > 0 || r.Rank < Ranks.C)) ||
46-
currentList.Any(r => r.DXScore is 0 && (r.DXStar > 0 || r.Rank < Ranks.C)))
47-
{
48-
proberState = "warning";
49-
}
50-
51-
Dictionary<string, object?> scope = new(StringComparer.OrdinalIgnoreCase)
52-
{
53-
["user"] = user,
54-
["everCards"] = everCards,
55-
["currentCards"] = currentCards,
56-
["everDelta"] = everDelta,
57-
["currentDelta"] = currentDelta,
58-
["everTotal"] = everTotal,
59-
["currentTotal"] = currentTotal,
60-
["realRating"] = realRating,
61-
["typename"] = typename,
62-
["prober"] = prober,
63-
["isAnime"] = isAnime,
64-
["drawLevelSeg"] = drawLevelSeg,
65-
["proberState"] = proberState,
36+
["userInfo"] = user,
37+
["everRecords"] = ever,
38+
["currentRecords"] = current,
39+
["everRating"] = everTotal,
40+
["currentRating"] = currentTotal,
41+
["typeName"] = typename,
42+
["proberName"] = prober,
43+
["animeMode"] = isAnime,
44+
["needSuggestion"] = drawLevelSeg,
45+
["mayMask"] = mayMask,
6646
["everMax"] = everMax,
6747
["everMin"] = everMin,
6848
["currentMax"] = currentMax,
@@ -71,35 +51,33 @@ public async Task<Image> DrawBestsAsync(CommonUser user, IEnumerable<CommonRecor
7151
return await DrawAsync(scope, xmlPath);
7252
}
7353

74-
public async Task<Image> DrawListAsync(CommonUser user, IEnumerable<CommonRecord> records, int page, int total,
75-
IEnumerable<int> counts, string level, string prober) => await DrawListAsync(user, records, page, total, counts,
76-
level, prober, "./Resources/Layouts/list.xml");
54+
public async Task<Image> DrawListAsync(CommonUser user, IList<CommonRecord> records, int page, int total,
55+
IList<int> counts, int startIndex, string level, string prober) => await DrawListAsync(user, records, page, total, counts,
56+
startIndex, level, prober, "./Resources/Layouts/list.xml");
7757

78-
public async Task<Image> DrawListAsync(CommonUser user, IEnumerable<CommonRecord> records, int page, int total,
79-
IEnumerable<int> counts, string level, string prober, string xmlPath)
58+
public async Task<Image> DrawListAsync(CommonUser user, IList<CommonRecord> records, int page, int total,
59+
IList<int> counts, int startIndex, string level, string prober, string xmlPath)
8060
{
81-
List<CommonRecord> list = [.. records];
82-
List<object> recordCards = [.. list.Select((record, idx) => new { Record = record, Index = idx + 1 })];
83-
List<int> countList = counts.ToList();
84-
int totalCount = countList.Count > 0 ? countList[^1] : 0;
85-
bool warning = list.Any(r => r.DXScore is 0 && (r.DXStar > 0 || r.Rank < Ranks.C));
86-
Dictionary<string, object?> scope = new(StringComparer.OrdinalIgnoreCase)
61+
int totalCount = counts.Count > 0 ? counts[^1] : 0;
62+
bool mayMask = records.Any(r => r.DXScore is 0 && (r.DXStar > 0 || r.Rank > Ranks.A));
63+
Dictionary<string, object> scope = new(StringComparer.OrdinalIgnoreCase)
8764
{
88-
["user"] = user,
89-
["recordCards"] = recordCards,
90-
["page"] = page,
91-
["total"] = total,
92-
["counts"] = countList[..^1],
93-
["statsTotalCount"] = totalCount,
65+
["userInfo"] = user,
66+
["pageRecords"] = records,
67+
["pageNumber"] = page,
68+
["totalPages"] = total,
69+
["statCounts"] = counts.ToList()[..^1],
70+
["totalCount"] = totalCount,
71+
["startIndex"] = startIndex,
9472
["level"] = level,
95-
["prober"] = prober,
96-
["proberState"] = warning ? "warning" : "on",
97-
["isAnime"] = false,
73+
["proberName"] = prober,
74+
["mayMask"] = mayMask,
75+
["animeMode"] = false,
9876
};
9977
return await DrawAsync(scope, xmlPath);
10078
}
10179

102-
private async Task<Image> DrawAsync(Dictionary<string, object?> scope, string xmlPath)
80+
private async Task<Image> DrawAsync(IDictionary<string, object> scope, string xmlPath)
10381
{
10482
AsyncNCalcEngine expr = new();
10583
RegisterFunctions(expr);
@@ -112,5 +90,6 @@ private async Task<Image> DrawAsync(Dictionary<string, object?> scope, string xm
11290
private void RegisterFunctions(AsyncNCalcEngine expr)
11391
{
11492
expr.RegisterFunction("ToString", (object x) => Convert.ToString(x));
93+
expr.RegisterFunction("Count", (IList x) => x.Count);
11594
}
11695
}

src/Render/ExpressionEngine/AsyncNCalcEngine.cs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,20 +52,22 @@ public sealed class AsyncNCalcEngine
5252
}
5353
expression.EvaluateFunctionAsync += async (name, args) =>
5454
{
55-
if (_functions.TryGetValue(name, out Delegate? func))
55+
if (!_functions.TryGetValue(name, out Delegate? func))
5656
{
57-
ParameterInfo[] parameters = func.Method.GetParameters();
58-
object?[] funcArgs = new object[args.Parameters.Length];
59-
for (int i = 0; i < args.Parameters.Length; ++i)
60-
{
61-
object? paramValue = await args.Parameters[i].EvaluateAsync();
62-
funcArgs[i] = CoerceValue(paramValue,
63-
i < parameters.Length ? parameters[i].ParameterType : typeof(object));
64-
}
57+
return;
58+
}
6559

66-
object? result = func.DynamicInvoke(funcArgs);
67-
args.Result = result;
60+
ParameterInfo[] parameters = func.Method.GetParameters();
61+
object?[] funcArgs = new object[args.Parameters.Length];
62+
for (int i = 0; i < args.Parameters.Length; ++i)
63+
{
64+
object? paramValue = await args.Parameters[i].EvaluateAsync();
65+
funcArgs[i] = CoerceValue(paramValue,
66+
i < parameters.Length ? parameters[i].ParameterType : typeof(object));
6867
}
68+
69+
object? result = func.DynamicInvoke(funcArgs);
70+
args.Result = result;
6971
};
7072

7173
return await expression.EvaluateAsync();

src/Render/NodeRenderer.Measurement.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public static partial class NodeRenderer
1010
{
1111
ImageNode image => MeasureImageNode(image, assets),
1212
TextNode text => MeasureTextNode(text, measurer),
13+
ResizedNode resized => MeasureResizedNode(resized, assets, measurer),
1314
StackNode stack => MeasureStackNode(stack, assets, measurer),
1415
GridNode grid => MeasureGridNode(grid, assets, measurer),
1516
LayerNode layer => MeasureLayerNode(layer, assets, measurer),
@@ -20,13 +21,21 @@ public static partial class NodeRenderer
2021

2122
private static Size MeasureImageNode(ImageNode image, AssetProvider assets)
2223
{
23-
using Image img = assets.LoadImage(image.Namespace, image.ResourceKey);
24+
Image img = assets.LoadImage(image.Namespace, image.ResourceKey);
2425
return new(img.Width, img.Height);
2526
}
2627

2728
private static Size MeasureTextNode(TextNode text, AssetProvider measurer) =>
2829
measurer.Measure(text.Text, text.FontFamily, text.FontSize);
2930

31+
private static Size MeasureResizedNode(ResizedNode resized, AssetProvider assets, AssetProvider measurer)
32+
{
33+
Size baseSize = resized.DesiredSize ?? Measure(resized.Child, assets, measurer);
34+
int width = Math.Max(1, (int)Math.Round(baseSize.Width * resized.Scale));
35+
int height = Math.Max(1, (int)Math.Round(baseSize.Height * resized.Scale));
36+
return new(width, height);
37+
}
38+
3039
private static Size MeasureStackNode(StackNode stack, AssetProvider assets, AssetProvider measurer)
3140
{
3241
List<Node> flowChildren = ExpandFlowChildren(stack.Children);
@@ -116,4 +125,4 @@ private static List<Node> ExpandFlowChildren(IEnumerable<Node> children)
116125

117126
return output;
118127
}
119-
}
128+
}

0 commit comments

Comments
 (0)