Forge
csharpdeeb25a2
1#nullable enable
2
3extern alias svgctrl;
4using System.Globalization;
5using Avalonia.Controls;
6using Avalonia.Media.Imaging;
7using SvgImage = svgctrl::Avalonia.Svg.SvgImage;
8using SvgSource = svgctrl::Avalonia.Svg.SvgSource;
9
10namespace CascadeIDE.Views;
11
12/// <summary>Загрузка inline/file изображений для Markdown preview.</summary>
13public static class MarkdownPreviewImageFactory
14{
15 public static Control? TryCreate(string? url, string? alt, string? sourceFilePath)
16 {
17 if (string.IsNullOrWhiteSpace(url))
18 return null;
19
20 var trimmed = url.Trim();
21 if (trimmed.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
22 return TryCreateFromDataUri(trimmed, alt);
23
24 var absolute = ResolveFilePath(trimmed, sourceFilePath);
25 if (absolute is null || !File.Exists(absolute))
26 return BuildPlaceholder(alt, trimmed);
27
28 var ext = Path.GetExtension(absolute);
29 if (ext.Equals(".svg", StringComparison.OrdinalIgnoreCase))
30 return TryCreateSvgFromFile(absolute, alt);
31
32 try
33 {
34 using var stream = File.OpenRead(absolute);
35 var bitmap = new Bitmap(stream);
36 return WrapImage(new Image { Source = bitmap }, alt);
37 }
38 catch
39 {
40 return BuildPlaceholder(alt, trimmed);
41 }
42 }
43
44 private static Control? TryCreateFromDataUri(string dataUri, string? alt)
45 {
46 var comma = dataUri.IndexOf(',', StringComparison.Ordinal);
47 if (comma < 0)
48 return BuildPlaceholder(alt, dataUri);
49
50 var meta = dataUri[5..comma];
51 var payload = dataUri[(comma + 1)..];
52 var isBase64 = meta.Contains(";base64", StringComparison.OrdinalIgnoreCase);
53 if (!isBase64)
54 return BuildPlaceholder(alt, "non-base64 data URI");
55
56 try
57 {
58 var bytes = Convert.FromBase64String(payload);
59 if (meta.Contains("image/svg+xml", StringComparison.OrdinalIgnoreCase))
60 return TryCreateSvgFromBytes(bytes, alt);
61
62 using var stream = new MemoryStream(bytes);
63 var bitmap = new Bitmap(stream);
64 return WrapImage(new Image { Source = bitmap }, alt);
65 }
66 catch
67 {
68 return BuildPlaceholder(alt, dataUri);
69 }
70 }
71
72 private static Control? TryCreateSvgFromFile(string path, string? alt)
73 {
74 try
75 {
76 var source = SvgSource.Load(path, null);
77 if (source?.Picture is null)
78 return BuildPlaceholder(alt, path);
79
80 return WrapImage(new SvgImage { Source = source }, alt);
81 }
82 catch
83 {
84 return BuildPlaceholder(alt, path);
85 }
86 }
87
88 private static Control? TryCreateSvgFromBytes(byte[] bytes, string? alt)
89 {
90 try
91 {
92 using var stream = new MemoryStream(bytes);
93 var source = SvgSource.Load(stream, null);
94 if (source?.Picture is null)
95 return BuildPlaceholder(alt, "svg");
96
97 return WrapImage(new SvgImage { Source = source }, alt);
98 }
99 catch
100 {
101 return BuildPlaceholder(alt, "svg");
102 }
103 }
104
105 private static Control WrapImage(object image, string? alt)
106 {
107 var host = image is Control control
108 ? control
109 : new ContentControl { Content = image };
110
111 host.MaxWidth = 960;
112 host.HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Left;
113
114 if (string.IsNullOrWhiteSpace(alt))
115 return host;
116
117 return new StackPanel
118 {
119 Spacing = 4,
120 Children =
121 {
122 host,
123 new TextBlock
124 {
125 Text = alt,
126 Opacity = 0.7,
127 FontSize = 11,
128 TextWrapping = Avalonia.Media.TextWrapping.Wrap,
129 },
130 },
131 };
132 }
133
134 private static Control BuildPlaceholder(string? alt, string detail) =>
135 new TextBlock
136 {
137 Text = string.IsNullOrWhiteSpace(alt)
138 ? $"[Image: {detail}]"
139 : $"[Image: {alt}] ({detail})",
140 Opacity = 0.75,
141 TextWrapping = Avalonia.Media.TextWrapping.Wrap,
142 };
143
144 private static string? ResolveFilePath(string url, string? sourceFilePath)
145 {
146 if (Path.IsPathRooted(url))
147 return url;
148
149 if (string.IsNullOrWhiteSpace(sourceFilePath))
150 return null;
151
152 var dir = Path.GetDirectoryName(sourceFilePath);
153 if (string.IsNullOrWhiteSpace(dir))
154 return null;
155
156 return Path.GetFullPath(Path.Combine(dir, url.Replace('/', Path.DirectorySeparatorChar)));
157 }
158}
159
View only · write via MCP/CIDE