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 @@ +