diff --git a/MdXaml.AnimatedGif/AnimatedGifLoader.cs b/MdXaml.AnimatedGif/AnimatedGifLoader.cs
new file mode 100644
index 0000000..19688e5
--- /dev/null
+++ b/MdXaml.AnimatedGif/AnimatedGifLoader.cs
@@ -0,0 +1,212 @@
+using MdXaml.Plugins;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media.Animation;
+using System.Windows.Media.Imaging;
+using WpfAnimatedGif;
+
+namespace MdXaml.AnimatedGif
+{
+ public class AnimatedGifLoader : IElementLoader, IPreferredLoader
+ {
+ private static readonly byte[] G87AMagic = Encoding.ASCII.GetBytes("GIF87a");
+ private static readonly byte[] G89AMagic = Encoding.ASCII.GetBytes("GIF89a");
+ private static readonly byte[] NetscapeMagic = Encoding.ASCII.GetBytes("NETSCAPE2.0");
+ private static readonly int MagicLength = G87AMagic.Length;
+
+ public FrameworkElement? Load(Stream stream)
+ {
+ if (!CheckGifAFormat(stream))
+ return null;
+
+ stream.Position = 0;
+
+ var memstr = new MemoryStream();
+ stream.CopyTo(memstr);
+
+ var bitmap = new BitmapImage();
+ bitmap.BeginInit();
+ bitmap.CacheOption = BitmapCacheOption.OnLoad;
+ bitmap.StreamSource = memstr;
+ bitmap.EndInit();
+
+ var image = new Image();
+ ImageBehavior.SetRepeatBehavior(image, RepeatBehavior.Forever);
+ ImageBehavior.SetAnimatedSource(image, bitmap);
+
+ return image;
+ }
+
+ public bool CheckGifAFormat(Stream stream)
+ {
+ byte[] buffer = new byte[768];
+
+ if (stream.Read(buffer, 0, MagicLength) != MagicLength)
+ return false;
+
+ if (!SeqEq(buffer, G87AMagic, MagicLength)
+ && !SeqEq(buffer, G89AMagic, MagicLength))
+ return false;
+
+ if (!TryReadUShortS(stream, buffer, out var width))
+ return false;
+
+ if (!TryReadUShortS(stream, buffer, out var height))
+ return false;
+
+ if (!TryReadByteS(stream, buffer, out var packed))
+ return false;
+
+ if (!TryReadByteS(stream, buffer, out var bgIndex))
+ return false;
+
+ stream.Position++;
+
+ var noTrailer = true;
+ while (noTrailer)
+ {
+
+ if (!TryReadByteS(stream, buffer, out var blockType))
+ return false;
+
+ switch ((int)blockType)
+ {
+ case 0: // Empty
+ break;
+
+ case 0x21: // EXTENSION
+ if (!TryReadByteS(stream, buffer, out var extType))
+ return false;
+
+ switch ((int)extType)
+ {
+ case 0xF9: //GRAPHICS_CONTROL
+ if (!TryReadBlock(stream, buffer, out var _))
+ return false;
+ break;
+
+ case 0xFF: //APPLICATION
+ if (!TryReadBlock(stream, buffer, out var blockLen))
+ return false;
+
+ if (blockLen < NetscapeMagic.Length)
+ return false;
+
+ if (SeqEq(buffer, NetscapeMagic, NetscapeMagic.Length))
+ {
+ var count = 0;
+
+ while (count > 0)
+ if (!TryReadBlock(stream, buffer, out count))
+ return false;
+ }
+ else if (!TryReadBlock(stream, buffer, out var _))
+ return false;
+
+ break;
+ }
+ break;
+
+
+ case 0x2C:// IMAGE_DESCRIPTOR
+
+ // frame bounds ( X, Y, W, H)
+ foreach (var _ in Enumerable.Range(0, 4))
+ if (!TryReadUShortS(stream, buffer, out var _))
+ return false;
+
+ if (!TryReadByteS(stream, buffer, out var descPack))
+ return false;
+
+ if ((descPack & 0x80) != 0)
+ {
+ var colorSize = 2 << (descPack & 7);
+ if (stream.Read(buffer, 0, colorSize * 3) < colorSize * 3)
+ return false;
+ }
+
+ if (!TryReadByteS(stream, buffer, out var _))
+ return false;
+
+ if (!TryReadBlock(stream, buffer, out var _))
+ return false;
+
+ break;
+
+ case 0x3B: // TRAILER
+ noTrailer = false;
+ break;
+
+ default:
+ if (!TryReadBlock(stream, buffer, out var _))
+ return false;
+
+ break;
+ }
+ }
+
+ var globalColorSz = 2 << (packed & 7);
+ if (stream.Read(buffer, 0, globalColorSz * 3) < globalColorSz * 3)
+ return false;
+
+ return true;
+ }
+
+ private static bool SeqEq(byte[] a, byte[] b, int len)
+ {
+ if (a.Length < len || b.Length < len)
+ return false;
+
+ for (int i = 0; i < len; ++i)
+ if (!a[i].Equals(b[i]))
+ return false;
+
+ return true;
+ }
+
+ private static bool TryReadUShortS(Stream stream, byte[] buffer, out ushort read)
+ {
+ if (stream.Read(buffer, 0, 2) < 2)
+ {
+ read = default;
+ return false;
+ }
+
+ read = (ushort)(buffer[0] | (buffer[1] << 8));
+ return true;
+ }
+
+ private static bool TryReadByteS(Stream stream, byte[] buffer, out byte read)
+ {
+ if (stream.Read(buffer, 0, 1) < 1)
+ {
+ read = default;
+ return false;
+ }
+
+ read = buffer[0];
+ return true;
+ }
+
+ private static bool TryReadBlock(Stream stream, byte[] buffer, out int blockSize)
+ {
+ if (!TryReadByteS(stream, buffer, out var len))
+ {
+ blockSize = -1;
+ return false;
+ }
+
+ blockSize = (int)len;
+ var readLen = stream.Read(buffer, 0, blockSize);
+
+ if (readLen < blockSize)
+ return false;
+
+ return true;
+ }
+ }
+}
diff --git a/MdXaml.AnimatedGif/AnimatedGifPluginSetup.cs b/MdXaml.AnimatedGif/AnimatedGifPluginSetup.cs
new file mode 100644
index 0000000..2659de4
--- /dev/null
+++ b/MdXaml.AnimatedGif/AnimatedGifPluginSetup.cs
@@ -0,0 +1,14 @@
+
+
+using MdXaml.Plugins;
+
+namespace MdXaml.AnimatedGif
+{
+ public class AnimatedGifPluginSetup : IPluginSetup
+ {
+ public void Setup(MdXamlPlugins plugins)
+ {
+ plugins.ElementLoader.Add(new AnimatedGifLoader());
+ }
+ }
+}
diff --git a/MdXaml.AnimatedGif/MdXaml.AnimatedGif.csproj b/MdXaml.AnimatedGif/MdXaml.AnimatedGif.csproj
new file mode 100644
index 0000000..65fcccf
--- /dev/null
+++ b/MdXaml.AnimatedGif/MdXaml.AnimatedGif.csproj
@@ -0,0 +1,30 @@
+
+
+
+ $(PackageTargetFrameworks)
+ MdXaml.AniatedGif
+ $(PackageVersion)
+ whistyun
+
+ Displays AniatedGif for MdXaml
+ whistyun 2023
+ https://github.com/whistyun/MdXaml
+ MIT
+ MdXaml.Html.md
+ Markdown WPF Xaml FlowDocument
+ Debug;Release
+
+ true
+ 9
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MdXaml.Html/MdXaml.Html.csproj b/MdXaml.Html/MdXaml.Html.csproj
index 0ebd3ff..8f3da1e 100644
--- a/MdXaml.Html/MdXaml.Html.csproj
+++ b/MdXaml.Html/MdXaml.Html.csproj
@@ -5,7 +5,7 @@
$(PackageVersion)
whistyun
- Markdown XAML processor
+ cheap html processor for MdXalml
© Simon Baynes 2013; whistyun 2022
https://github.com/whistyun/MdXaml
MIT
@@ -23,7 +23,7 @@
-
+
diff --git a/MdXaml.Plugins/IElementLoader.cs b/MdXaml.Plugins/IElementLoader.cs
new file mode 100644
index 0000000..1ddc586
--- /dev/null
+++ b/MdXaml.Plugins/IElementLoader.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media.Imaging;
+
+namespace MdXaml.Plugins
+{
+ public interface IElementLoader
+ {
+ public FrameworkElement? Load(Stream stream);
+ }
+}
diff --git a/MdXaml.Plugins/IMarkdown.cs b/MdXaml.Plugins/IMarkdown.cs
index 947ad16..43ee2ef 100644
--- a/MdXaml.Plugins/IMarkdown.cs
+++ b/MdXaml.Plugins/IMarkdown.cs
@@ -18,7 +18,7 @@ public interface IMarkdown
public InlineUIContainer LoadImage(
string? tag, string urlTxt, string? tooltipTxt,
- Action? onSuccess = null);
+ Action? onSuccess = null);
FlowDocument Transform(string text);
diff --git a/MdXaml.Plugins/IPreferredLoader.cs b/MdXaml.Plugins/IPreferredLoader.cs
new file mode 100644
index 0000000..0c0fa70
--- /dev/null
+++ b/MdXaml.Plugins/IPreferredLoader.cs
@@ -0,0 +1,4 @@
+namespace MdXaml.Plugins
+{
+ public interface IPreferredLoader { }
+}
diff --git a/MdXaml.Plugins/MdXamlPlugins.cs b/MdXaml.Plugins/MdXamlPlugins.cs
index ec49a7d..02cf5df 100644
--- a/MdXaml.Plugins/MdXamlPlugins.cs
+++ b/MdXaml.Plugins/MdXamlPlugins.cs
@@ -20,12 +20,13 @@ public class MdXamlPlugins
public ObservableCollection Block { get; }
public ObservableCollection Inline { get; }
public ObservableCollection ImageLoader { get; }
+ public ObservableCollection ElementLoader { get; }
public MdXamlPlugins() : this(new SyntaxManager())
{
}
- public MdXamlPlugins(SyntaxManager manager) : this(manager, new(), new(), new(), new(), new())
+ public MdXamlPlugins(SyntaxManager manager) : this(manager, new(), new(), new(), new(), new(), new())
{
}
@@ -35,7 +36,8 @@ private MdXamlPlugins(
ObservableCollection topBlock,
ObservableCollection block,
ObservableCollection inline,
- ObservableCollection imageLoader)
+ ObservableCollection imageLoader,
+ ObservableCollection elementLoader)
{
Syntax = manager;
Setups = setups;
@@ -43,6 +45,7 @@ private MdXamlPlugins(
Block = block;
Inline = inline;
ImageLoader = imageLoader;
+ ElementLoader = elementLoader;
Setups.CollectionChanged += Setups_CollectionChanged;
}
@@ -61,7 +64,8 @@ public MdXamlPlugins Clone()
new(TopBlock),
new(Block),
new(Inline),
- new(ImageLoader));
+ new(ImageLoader),
+ new(ElementLoader));
}
}
diff --git a/MdXaml.Plugins/SyntaxManager.cs b/MdXaml.Plugins/SyntaxManager.cs
index 884bee1..5a1c73d 100644
--- a/MdXaml.Plugins/SyntaxManager.cs
+++ b/MdXaml.Plugins/SyntaxManager.cs
@@ -16,6 +16,7 @@ public class SyntaxManager
EnableStrikethrough = false,
EnableListMarkerExt = false,
EnableTextileInline = false,
+ EnableImageResizeExt = false,
};
public static readonly SyntaxManager Standard = new()
{
@@ -26,6 +27,7 @@ public class SyntaxManager
EnableStrikethrough = true,
EnableListMarkerExt = false,
EnableTextileInline = false,
+ EnableImageResizeExt = false,
};
public static readonly SyntaxManager MdXaml = new();
@@ -39,6 +41,8 @@ public class SyntaxManager
public bool EnableListMarkerExt { set; get; } = true;
public bool EnableTextileInline { get; set; } = true;
+ public bool EnableImageResizeExt { get; set; } = true;
+
public void And(SyntaxManager manager)
{
EnableNoteBlock &= manager.EnableNoteBlock;
@@ -48,6 +52,7 @@ public void And(SyntaxManager manager)
EnableStrikethrough &= manager.EnableStrikethrough;
EnableListMarkerExt &= manager.EnableListMarkerExt;
EnableTextileInline &= manager.EnableTextileInline;
+ EnableImageResizeExt &= manager.EnableImageResizeExt;
}
public SyntaxManager Clone()
@@ -60,6 +65,7 @@ public SyntaxManager Clone()
EnableStrikethrough = EnableStrikethrough,
EnableListMarkerExt = EnableListMarkerExt,
EnableTextileInline = EnableTextileInline,
+ EnableImageResizeExt = EnableImageResizeExt,
};
}
}
diff --git a/MdXaml.Svg/MdXaml.Svg.csproj b/MdXaml.Svg/MdXaml.Svg.csproj
index 5943825..2463007 100644
--- a/MdXaml.Svg/MdXaml.Svg.csproj
+++ b/MdXaml.Svg/MdXaml.Svg.csproj
@@ -6,7 +6,7 @@
$(PackageVersion)
whistyun
- Markdown XAML processor
+ Displays SVG for MdXaml
Copyright (c) 2022 whistyun
https://github.com/whistyun/MdXaml
MIT
diff --git a/MdXaml.sln b/MdXaml.sln
index 336d302..1d74c57 100644
--- a/MdXaml.sln
+++ b/MdXaml.sln
@@ -32,6 +32,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MdXaml.Html.Test", "tests\M
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MdXaml.Plugins", "MdXaml.Plugins\MdXaml.Plugins.csproj", "{823DB5D5-17C0-4418-895C-A28DD7D04CE9}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MdXaml.AnimatedGif", "MdXaml.AnimatedGif\MdXaml.AnimatedGif.csproj", "{A7666AD6-CB44-4399-AC61-05DE80D65F9D}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -78,6 +80,10 @@ Global
{823DB5D5-17C0-4418-895C-A28DD7D04CE9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{823DB5D5-17C0-4418-895C-A28DD7D04CE9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{823DB5D5-17C0-4418-895C-A28DD7D04CE9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A7666AD6-CB44-4399-AC61-05DE80D65F9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A7666AD6-CB44-4399-AC61-05DE80D65F9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A7666AD6-CB44-4399-AC61-05DE80D65F9D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A7666AD6-CB44-4399-AC61-05DE80D65F9D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/MdXaml/ImageLoaderManager.cs b/MdXaml/ImageLoaderManager.cs
index 1737cc9..8e39906 100644
--- a/MdXaml/ImageLoaderManager.cs
+++ b/MdXaml/ImageLoaderManager.cs
@@ -2,13 +2,17 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Configuration;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Security.Policy;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
@@ -21,149 +25,78 @@ internal class ImageLoaderManager
private readonly IDictionary> _resultCache
= new ConcurrentDictionary>();
- private readonly List _loaders = new();
-
- public void Register(IImageLoader l) => _loaders.Add(l);
+ private readonly List _iloaders = new();
+ private readonly List _eloaders = new();
public void Restructure(MdXamlPlugins plugins)
{
- _loaders.Clear();
- _loaders.AddRange(plugins.ImageLoader);
+ _iloaders.Clear();
+ _eloaders.Clear();
+ _iloaders.AddRange(plugins.ImageLoader);
+ _eloaders.AddRange(plugins.ElementLoader);
}
- public Result LoadImage(IEnumerable resourceUrls)
+ public Result LoadImage(IEnumerable resourceUrls)
{
- Result? firsterr = null;
-
- foreach (var resourceUrl in resourceUrls)
- {
- var resourceResult = CheckResource(resourceUrl);
- if (resourceResult is not null)
- {
- return resourceResult;
- }
-
- var stream = OpenStreamAsync(resourceUrl).Result;
- if (stream.Value is null)
- {
- firsterr ??= new Result(stream.ErrorMessage);
- continue;
- }
-
- var img = OpenImageDirect(stream.Value);
- if (img is null)
- {
- firsterr ??= new Result("unsupported image format");
- continue;
- }
-
- if (resourceUrl.Scheme == "http" || resourceUrl.Scheme == "https")
- {
- _resultCache[resourceUrl] = new WeakReference(img);
- }
-
- return new Result(img);
- }
+ return PrivateLoadImageAsync(resourceUrls, false).Result;
+ }
- return firsterr ?? throw new ArgumentException("no resourceUrls");
+ public Task> LoadImageAsync(IEnumerable resourceUrls)
+ {
+ return PrivateLoadImageAsync(resourceUrls, true);
}
- public async Task> LoadImageAsync(IEnumerable resourceUrls)
+ public async Task> PrivateLoadImageAsync(IEnumerable resourceUrls, bool dispatch)
{
- Result? firsterr = null;
+ Result? firsterr = null;
foreach (var resourceUrl in resourceUrls)
{
- var resourceResult = CheckResource(resourceUrl);
- if (resourceResult is not null)
+ var cachedResult = CheckResource(resourceUrl);
+ if (cachedResult is not null)
{
- return resourceResult;
- }
+ var task = Create(cachedResult, dispatch);
- var stream = await OpenStreamAsync(resourceUrl);
- if (stream.Value is null)
- {
- firsterr ??= new Result(stream.ErrorMessage);
- continue;
+ return dispatch ? await task : task.Result;
}
- var img = await OpenImageOnUITrhead(stream.Value);
- if (img is null)
+ var streamTask = OpenStreamAsync(resourceUrl);
+ var streamResult = dispatch ? await streamTask : streamTask.Result;
+ if (streamResult.Value is null)
{
- firsterr ??= new Result("unsupported image format");
+ firsterr ??= new Result(streamResult.ErrorMessage);
continue;
}
- if (resourceUrl.Scheme == "http" || resourceUrl.Scheme == "https")
+ using var stream = streamResult.Value;
+ var imageTask = OpenImage(stream, resourceUrl, dispatch);
+ var imageResult = dispatch ? await imageTask : imageTask.Result;
+ if (imageResult is null)
{
- _resultCache[resourceUrl] = new WeakReference(img);
+ firsterr ??= new Result("unsupported image format");
+ continue;
}
- return new Result(img);
+ return new Result(imageResult);
}
return firsterr ?? throw new ArgumentException("no resourceUrls");
}
- private Result? CheckResource(Uri resourceUrl)
+ private BitmapImage? CheckResource(Uri resourceUrl)
{
if ((resourceUrl.Scheme == "http" || resourceUrl.Scheme == "https")
&& _resultCache.TryGetValue(resourceUrl, out var reference))
{
if (reference.TryGetTarget(out var cachedimg))
{
- return new Result(cachedimg);
+ return cachedimg;
}
_resultCache.Remove(resourceUrl);
}
return null;
}
- private Task OpenImageOnUITrhead(Stream stream)
- {
- var dispatcher = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;
- return dispatcher.InvokeAsync(() => OpenImageDirect(stream))
- .Task;
- }
-
- private BitmapImage? OpenImageDirect(Stream stream)
- {
- stream.Position = 0;
-
- try
- {
- var imgSource = new BitmapImage();
- imgSource.BeginInit();
- // close the stream after the BitmapImage is created
- imgSource.CacheOption = BitmapCacheOption.OnLoad;
- imgSource.StreamSource = stream;
- imgSource.EndInit();
-
- stream.Close();
-
- return imgSource;
- }
- catch { }
-
- foreach (var ld in _loaders)
- {
- try
- {
- stream.Position = 0;
- var img = ld.Load(stream);
- if (img is not null)
- {
- stream.Close();
- return img;
- }
- }
- catch { }
- }
-
- stream.Close();
- return null;
- }
-
private static async Task> OpenStreamAsync(Uri resourceUrl)
{
switch (resourceUrl.Scheme)
@@ -227,6 +160,121 @@ static async Task AsMemoryStream(Stream stream)
}
}
+ private Task OpenImage(Stream stream, Uri? cacheKey, bool dispatch)
+ {
+ if (dispatch)
+ {
+ var dispatcher = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;
+ var operation = dispatcher.InvokeAsync(() => OpenImageDirect(stream));
+
+ return operation.Task;
+ }
+ else
+ {
+ return Task.FromResult(OpenImageDirect(stream));
+ }
+
+ FrameworkElement? OpenImageDirect(Stream stream)
+ {
+ foreach (var ld in _iloaders.Where(ld => ld is IPreferredLoader))
+ {
+ stream.Position = 0;
+
+ try
+ {
+ var img = ld.Load(stream);
+ if (img is not null)
+ return Create(cacheKey, img);
+ }
+ catch { }
+ }
+
+ foreach (var ld in _eloaders.Where(ld => ld is IPreferredLoader))
+ {
+ stream.Position = 0;
+
+ try
+ {
+ var img = ld.Load(stream);
+ if (img is not null)
+ return img;
+ }
+ catch { }
+ }
+
+ stream.Position = 0;
+
+ try
+ {
+ var imgSource = new BitmapImage();
+ imgSource.BeginInit();
+ // close the stream after the BitmapImage is created
+ imgSource.CacheOption = BitmapCacheOption.OnLoad;
+ imgSource.StreamSource = stream;
+ imgSource.EndInit();
+
+ return Create(cacheKey, imgSource);
+ }
+ catch { }
+
+ foreach (var ld in _iloaders.Where(ld => ld is not IPreferredLoader))
+ {
+ stream.Position = 0;
+
+ try
+ {
+ var img = ld.Load(stream);
+ if (img is not null)
+ return Create(cacheKey, img);
+ }
+ catch { }
+ }
+
+ foreach (var ld in _eloaders.Where(ld => ld is not IPreferredLoader))
+ {
+ stream.Position = 0;
+
+ try
+ {
+ var img = ld.Load(stream);
+ if (img is not null)
+ return img;
+ }
+ catch { }
+ }
+
+ return null;
+ }
+
+ FrameworkElement Create(Uri resourceKey, BitmapImage bitmap)
+ {
+ if (resourceKey.Scheme == "http" || resourceKey.Scheme == "https")
+ {
+ _resultCache[resourceKey] = new WeakReference(bitmap);
+ }
+
+ return new Image { Source = bitmap };
+ }
+ }
+
+ private Task> Create(BitmapImage image, bool dispatch)
+ {
+ if (dispatch)
+ {
+ var dispatcher = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;
+ var operation = dispatcher.InvokeAsync(() => CreateDirect(image));
+
+ return operation.Task;
+ }
+ else
+ {
+ return Task.FromResult(CreateDirect(image));
+ }
+
+ Result CreateDirect(BitmapImage image)
+ => new Result(new Image { Source = image });
+ }
+
public class Result where T : class
{
public string ErrorMessage { get; }
diff --git a/MdXaml/Markdown.cs b/MdXaml/Markdown.cs
index fe8435a..7a071f3 100644
--- a/MdXaml/Markdown.cs
+++ b/MdXaml/Markdown.cs
@@ -16,6 +16,7 @@
using System.Threading.Tasks;
using System.Windows.Threading;
using MdXaml.Menus;
+using System.Globalization;
// I will not add System.Index and System.Range. There is not exist with net45.
#pragma warning disable IDE0056
@@ -212,6 +213,11 @@ private ParseParam SetupParams(MdXamlPlugins plugins)
// inline parser
inlines.Add(SimpleInlineParser.New(_codeSpan, CodeSpanEvaluator));
+
+ if (plugins.Syntax.EnableImageResizeExt)
+ {
+ inlines.Add(SimpleInlineParser.New(_resizeImage, ImageWithSizeEvaluator));
+ }
inlines.Add(SimpleInlineParser.New(_imageOrHrefInline, ImageOrHrefInlineEvaluator));
if (StrictBoldItalic)
@@ -523,6 +529,40 @@ [ ]*
)", _nestedBracketsPattern, _nestedParensPattern),
RegexOptions.Singleline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
+ private static readonly Regex _resizeImage = new(string.Format(@"
+ ( # wrap whole match in $1
+ (!) # image maker = $2
+ \[
+ ({0}) # link text = $3
+ \]
+ \( # literal paren
+ [ ]*
+ ({1}) # href = $4
+ [ ]*
+ ( # $5
+ (['""]) # quote char = $6
+ (.*?) # title = $7
+ \6 # matching quote
+ [ ]* # ignore any spaces between closing quote and )
+ )? # title is optional
+ \)
+ \{{
+ ([^\}}]+) # size = $8
+ \}}
+ )", _nestedBracketsPattern, _nestedParensPattern),
+ RegexOptions.Singleline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
+
+ private static readonly Regex _stylePattern = new Regex(@"
+ [ ]+
+ (?width|height)
+ [ ]*
+ =
+ [ ]*
+ (?[0-9]*(\.[0-9]+)?)
+ (?(%|em|ex|mm|Q|cm|in|pt|pc|px|))
+ ",
+ RegexOptions.Singleline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
+
private Inline ImageOrHrefInlineEvaluator(Match match)
{
if (String.IsNullOrEmpty(match.Groups[2].Value))
@@ -569,9 +609,36 @@ private Inline TreatsAsImage(Match match)
return LoadImage(linkText, urlTxt, title);
}
+ private Inline ImageWithSizeEvaluator(Match match)
+ {
+ string linkText = match.Groups[3].Value;
+ string urlTxt = match.Groups[4].Value;
+ string title = match.Groups[7].Value;
+ string style = " " + match.Groups[8].Value;
+
+ List> effects = new();
+ foreach (var mch in _stylePattern.Matches(style).Cast())
+ {
+ if (!ImageIndicate.TryCreate(mch.Groups["value"].Value, mch.Groups["unit"].Value, out var indicate))
+ continue;
+
+ effects.Add(mch.Groups["name"].Value == "width" ?
+ indicate.ApplyToWidth : indicate.ApplyToHeight);
+ }
+
+ InlineUIContainer image = LoadImage(linkText, urlTxt, title, (container, image, source) =>
+ {
+ if (container.Child is FrameworkElement element)
+ foreach (var effect in effects)
+ effect(element);
+ });
+
+ return image;
+ }
+
public InlineUIContainer LoadImage(
string? tag, string urlTxt, string? tooltipTxt,
- Action? onSuccess = null)
+ Action? onSuccess = null)
{
var urls = new List();
@@ -2135,7 +2202,7 @@ internal class ImageLoading
private readonly string _urlTxt;
private readonly string? _tooltipTxt;
private readonly InlineUIContainer _container;
- private readonly Action? _onSuccess;
+ private readonly Action? _onSuccess;
private readonly Style? _imageStyle;
@@ -2143,7 +2210,7 @@ public ImageLoading(
Markdown owner,
string? tag, string urlTxt, string? tooltipTxt,
InlineUIContainer container,
- Action? onSuccess)
+ Action? onSuccess)
{
_tag = tag;
_urlTxt = urlTxt;
@@ -2154,17 +2221,17 @@ public ImageLoading(
_imageStyle = owner.ImageStyle;
}
- public void Treats(Task> task)
+ public void Treats(Task> task)
{
var dispatcher = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;
dispatcher.Invoke(async () => Treats(await task));
}
- public void Treats(ImageLoaderManager.Result result)
+ public void Treats(ImageLoaderManager.Result result)
{
- var source = result.Value;
+ var element = result.Value;
- if (source is null)
+ if (element is null)
{
_container.Child = new Label()
{
@@ -2174,65 +2241,64 @@ public void Treats(ImageLoaderManager.Result result)
}
else
{
- CreateImage(source);
+ Setup(element);
}
}
- private void CreateImage(ImageSource source)
+ private void Setup(FrameworkElement element)
{
- var image = new Image()
- {
- Source = source,
- };
+ var image = element as Image;
- if (_imageStyle is null)
+ if (element is null)
{
- image.Margin = new Thickness(0);
+ element.Margin = new Thickness(0);
}
- else
+ else if (image is not null)
{
image.Style = _imageStyle;
}
if (!string.IsNullOrWhiteSpace(_tag))
{
- image.Tag = _tag;
+ element.Tag = _tag;
}
if (!string.IsNullOrWhiteSpace(_tooltipTxt))
{
- image.ToolTip = _tooltipTxt;
+ element.ToolTip = _tooltipTxt;
}
- if (source is BitmapSource bs && bs.IsDownloading)
+ if (image is not null && image.Source is BitmapSource bs)
{
- Binding binding = new(nameof(BitmapImage.Width));
- binding.Source = bs;
- binding.Mode = BindingMode.OneWay;
+ if (bs.IsDownloading)
+ {
+ Binding binding = new(nameof(BitmapImage.Width));
+ binding.Source = bs;
+ binding.Mode = BindingMode.OneWay;
- BindingExpressionBase bindingExpression = BindingOperations.SetBinding(image, Image.WidthProperty, binding);
- bs.DownloadCompleted += downloadCompletedHandler;
+ BindingExpressionBase bindingExpression = BindingOperations.SetBinding(image, Image.WidthProperty, binding);
+ bs.DownloadCompleted += downloadCompletedHandler;
- void downloadCompletedHandler(object? sender, EventArgs e)
+ void downloadCompletedHandler(object? sender, EventArgs e)
+ {
+ bs.DownloadCompleted -= downloadCompletedHandler;
+ bs.Freeze();
+ bindingExpression.UpdateTarget();
+ }
+ }
+ else
{
- bs.DownloadCompleted -= downloadCompletedHandler;
- bs.Freeze();
- bindingExpression.UpdateTarget();
+ image.Width = bs.Width;
}
- }
- else
- {
- image.Width = source.Width;
- }
- _container.Child = image;
+ }
- _onSuccess?.Invoke(_container, image, source);
+ _container.Child = element;
+ _onSuccess?.Invoke(_container, image, image?.Source);
}
}
-
internal struct Candidate : IComparable
{
public Match Match { get; }
@@ -2248,4 +2314,171 @@ public int CompareTo(Candidate other)
=> Match.Index.CompareTo(other.Match.Index);
}
+ internal class ImageIndicate
+ {
+ public double Value { get; }
+ public string Unit { get; }
+
+ public ImageIndicate(double value, string unit)
+ {
+ Value = (Unit = unit) switch
+ {
+ "em" => value * 11,
+ "ex" => value * 11 / 2,
+ "Q" => value * 3.77952755905512 / 4,
+ "mm" => value * 3.77952755905512,
+ "cm" => value * 37.7952755905512,
+ "in" => value * 96,
+ "pt" => value * 1.33333333333333,
+ "pc" => value * 16,
+ "px" => value,
+ _ => value
+ };
+ }
+
+ public void ApplyToHeight(FrameworkElement image)
+ {
+ if (Unit == "%")
+ {
+ image.SetBinding(
+ Image.HeightProperty,
+ new Binding(nameof(Image.Width))
+ {
+ RelativeSource = new RelativeSource(RelativeSourceMode.Self),
+ Converter = new MultiplyConverter(Value / 100)
+ });
+ }
+ else
+ {
+ image.Height = Value;
+ }
+ }
+
+ public void ApplyToWidth(FrameworkElement image)
+ {
+ if (Unit == "%")
+ {
+ var parent = image.Parent;
+
+ for (; ; )
+ {
+ if (parent is FrameworkElement element)
+ {
+ parent = element;
+ break;
+ }
+ else if (parent is FrameworkContentElement content)
+ {
+ parent = content.Parent;
+ }
+ else break;
+ }
+
+ if (parent is FlowDocumentScrollViewer)
+ {
+ var binding = CreateMultiBindingForFlowDocumentScrollViewer();
+ binding.Converter = new MultiMultiplyConverter2(Value / 100);
+ image.SetBinding(Image.WidthProperty, binding);
+ }
+ else
+ {
+ var binding = CreateBinding(nameof(FrameworkElement.ActualWidth), typeof(FrameworkElement));
+ binding.Converter = new MultiplyConverter(Value / 100);
+ image.SetBinding(Image.WidthProperty, binding);
+ }
+ }
+ else
+ {
+ image.Width = Value;
+ }
+ }
+
+ private MultiBinding CreateMultiBindingForFlowDocumentScrollViewer()
+ {
+ var binding = new MultiBinding();
+
+ var totalWidth = CreateBinding(nameof(FlowDocumentScrollViewer.ActualWidth), typeof(FlowDocumentScrollViewer));
+ var verticalBarVis = CreateBinding(nameof(FlowDocumentScrollViewer.VerticalScrollBarVisibility), typeof(FlowDocumentScrollViewer));
+
+ binding.Bindings.Add(totalWidth);
+ binding.Bindings.Add(verticalBarVis);
+
+ return binding;
+ }
+
+ private static Binding CreateBinding(string propName, Type ancestorType)
+ {
+ return new Binding(propName)
+ {
+ RelativeSource = new RelativeSource()
+ {
+ Mode = RelativeSourceMode.FindAncestor,
+ AncestorType = ancestorType,
+ }
+ };
+ }
+
+ public static bool TryCreate(string valueText, string unit, out ImageIndicate indicate)
+ {
+ if (double.TryParse(valueText, out var value))
+ {
+ indicate = new ImageIndicate(value, unit);
+ return true;
+ }
+ else
+ {
+ indicate = default;
+ return false;
+ }
+ }
+
+ class MultiplyConverter : IValueConverter
+ {
+ public double Value { get; }
+
+ public MultiplyConverter(double v)
+ {
+ Value = v;
+ }
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return Value * (Double)value;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return ((Double)value) / Value;
+ }
+ }
+ class MultiMultiplyConverter2 : IMultiValueConverter
+ {
+ public double Value { get; }
+
+ public MultiMultiplyConverter2(double v)
+ {
+ Value = v;
+ }
+
+ public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+ {
+ var value = (double)values[0];
+ var visibility = (ScrollBarVisibility)values[1];
+
+ if (visibility == ScrollBarVisibility.Visible)
+ {
+ return Value * (value - SystemParameters.VerticalScrollBarWidth);
+ }
+ else
+ {
+ return Value * (Double)value;
+ }
+ }
+
+ public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/README.md b/README.md
index 205307e..c4bb2af 100644
--- a/README.md
+++ b/README.md
@@ -100,3 +100,19 @@ Framework: .NET Framework 4.6.2, .NET Core 3, .NET 5
## License
MdXaml is licensed under the MIT license.
+
+## Dependencies (Runtime)
+
+* MdXaml
+ * AvalonEdit (MIT) https://github.com/icsharpcode/AvalonEdit
+
+* MdXaml.Html
+ * AvalonEdit (MIT) https://github.com/icsharpcode/AvalonEdit
+ * HtmlAgilityPack (MIT) https://github.com/zzzprojects/html-agility-pack
+
+* MdXaml.Svg
+ * Svg (MIT) https://github.com/svg-net/SVG
+
+* MdXaml.AnimatedGif
+ * WpfAnimatedGif (Apache-2.0) https://github.com/XamlAnimatedGif/WpfAnimatedGif
+
diff --git a/samples/MdXaml.Demo/App.xaml b/samples/MdXaml.Demo/App.xaml
index 2ef4431..efd2999 100644
--- a/samples/MdXaml.Demo/App.xaml
+++ b/samples/MdXaml.Demo/App.xaml
@@ -5,12 +5,14 @@
xmlns:mdplugins="clr-namespace:MdXaml.Plugins;assembly=MdXaml.Plugins"
xmlns:mdhtml="clr-namespace:MdXaml.Html;assembly=MdXaml.Html"
xmlns:mdsvg="clr-namespace:MdXaml.Svg;assembly=MdXaml.Svg"
+ xmlns:mdagif="clr-namespace:MdXaml.AnimatedGif;assembly=MdXaml.AnimatedGif"
StartupUri="MainWindow.xaml">
+
diff --git a/samples/MdXaml.Demo/MdXaml.Demo.csproj b/samples/MdXaml.Demo/MdXaml.Demo.csproj
index e840b40..f37ed6d 100644
--- a/samples/MdXaml.Demo/MdXaml.Demo.csproj
+++ b/samples/MdXaml.Demo/MdXaml.Demo.csproj
@@ -20,6 +20,7 @@
+