diff --git a/src/BootstrapBlazor.Server/Components/Samples/Html2Images.razor b/src/BootstrapBlazor.Server/Components/Samples/Html2Images.razor
new file mode 100644
index 00000000000..17ee68b1641
--- /dev/null
+++ b/src/BootstrapBlazor.Server/Components/Samples/Html2Images.razor
@@ -0,0 +1,22 @@
+@page "/html2image"
+
+
@Localizer["Html2ImageTitle"]
+
+@Localizer["Html2ImageIntro"]
+
+
+
+
+ @if (!string.IsNullOrEmpty(_imageData))
+ {
+
+
+
+ }
+
diff --git a/src/BootstrapBlazor.Server/Components/Samples/Html2Images.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/Html2Images.razor.cs
new file mode 100644
index 00000000000..1b5ed4bcab0
--- /dev/null
+++ b/src/BootstrapBlazor.Server/Components/Samples/Html2Images.razor.cs
@@ -0,0 +1,71 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License
+// See the LICENSE file in the project root for more information.
+// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
+
+namespace BootstrapBlazor.Server.Components.Samples;
+
+///
+/// Html2Image 组件
+///
+public partial class Html2Images
+{
+ ///
+ /// 获得 IconTheme 实例
+ ///
+ [Inject]
+ [NotNull]
+ private IIconTheme? IconTheme { get; set; }
+
+ [Inject]
+ [NotNull]
+ private IStringLocalizer? LocalizerFoo { get; set; }
+
+ [Inject]
+ [NotNull]
+ private IHtml2Image? Html2ImageService { get; set; }
+
+ [Inject]
+ [NotNull]
+ private IStringLocalizer? Localizer { get; set; }
+
+ [Inject]
+ [NotNull]
+ private NavigationManager? NavigationManager { get; set; }
+
+ [NotNull]
+ private List? Items { get; set; }
+
+ private string? _imageData;
+
+ ///
+ ///
+ ///
+ protected override void OnInitialized()
+ {
+ base.OnInitialized();
+
+ Items = Foo.GenerateFoo(LocalizerFoo);
+ }
+
+ private async Task OnExportAsync()
+ {
+ _imageData = await Html2ImageService.GetDataAsync("#table-9527", new Html2ImageOptions()
+ {
+ //IncludeStyleProperties = [
+ // $"{NavigationManager.BaseUri}_content/BootstrapBlazor.FontAwesome/css/font-awesome.min.css",
+ // $"{NavigationManager.BaseUri}_content/BootstrapBlazor/css/bootstrap.blazor.bundle.min.css",
+ // $"{NavigationManager.BaseUri}BootstrapBlazor.Server.styles.css",
+ // $"{NavigationManager.BaseUri}css/site.css"
+ //]
+ });
+ StateHasChanged();
+
+ //if (stream != null)
+ //{
+ // var reader = new StreamReader(stream);
+ // var data = await reader.ReadToEndAsync();
+ // reader.Close();
+ //}
+ }
+}
diff --git a/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs b/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs
index 6e1e1e7150b..00297b76b1a 100644
--- a/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs
+++ b/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs
@@ -237,6 +237,7 @@ void AddQuickStar(DemoMenuItem item)
},
new()
{
+ IsUpdate = true,
Text = Localizer["Labels"],
Url = "label"
},
@@ -316,7 +317,6 @@ void AddForm(DemoMenuItem item)
},
new()
{
- IsUpdate = true,
Text = Localizer["Cascader"],
Url = "cascader"
},
@@ -405,7 +405,6 @@ void AddForm(DemoMenuItem item)
},
new()
{
- IsUpdate = true,
Text = Localizer["MultiSelect"],
Url = "multi-select"
},
@@ -431,14 +430,12 @@ void AddForm(DemoMenuItem item)
},
new()
{
- IsUpdate = true,
Match = NavLinkMatch.All,
Text = Localizer["Select"],
Url = "select"
},
new()
{
- IsUpdate = true,
Text = Localizer["SelectObject"],
Url = "select-object"
},
@@ -966,7 +963,6 @@ void AddTable(DemoMenuItem item)
},
new()
{
- IsUpdate = true,
Text = Localizer["TableLookup"],
Url = "table/lookup"
},
@@ -1484,6 +1480,12 @@ void AddServices(DemoMenuItem item)
Url = "geolocation"
},
new()
+ {
+ IsNew = true,
+ Text = Localizer["Html2Image"],
+ Url = "html2image"
+ },
+ new()
{
Text = Localizer["Html2Pdf"],
Url = "html2pdf"
diff --git a/src/BootstrapBlazor.Server/Locales/en-US.json b/src/BootstrapBlazor.Server/Locales/en-US.json
index e1dae3769ac..6bce2e98083 100644
--- a/src/BootstrapBlazor.Server/Locales/en-US.json
+++ b/src/BootstrapBlazor.Server/Locales/en-US.json
@@ -4771,6 +4771,7 @@
"BaiduOcr": "IBaiduOcr",
"AzureOpenAI": "AzureOpenAI",
"HtmlRenderer": "HtmlRenderer",
+ "Html2Image": "IHtml2Image",
"Html2Pdf": "IHtml2Pdf",
"Mask": "MaskService",
"ContextMenu": "ContextMenu",
@@ -6989,5 +6990,12 @@
"NormalIntro": "Set the text to be displayed by setting the Text parameter",
"TypedOptionsTitle": "TypedOptions",
"TypedOptionsIntro": "Customize typing speed, delay, and other settings by setting the properties of the TypedOptions parameter"
+ },
+ "BootstrapBlazor.Server.Components.Samples.Html2Images": {
+ "Html2ImageTitle": "Html to Image",
+ "Html2ImageIntro": "Convert any area of the web page into an image service",
+ "Html2ImageElementTitle": "ToPng",
+ "Html2ImageElementIntro": "Get the base64-encoded image by calling the GetDataAsync method",
+ "Html2ImageButtonText": "Image"
}
}
diff --git a/src/BootstrapBlazor.Server/Locales/zh-CN.json b/src/BootstrapBlazor.Server/Locales/zh-CN.json
index 1a56417b3b0..bfe3dde3b4c 100644
--- a/src/BootstrapBlazor.Server/Locales/zh-CN.json
+++ b/src/BootstrapBlazor.Server/Locales/zh-CN.json
@@ -4771,6 +4771,7 @@
"BaiduOcr": "文字识别服务 IBaiduOcr",
"AzureOpenAI": "AI 聊天服务 AzureOpenAI",
"HtmlRenderer": "Html 转换器 HtmlRenderer",
+ "Html2Image": "Html 转 Image IHtml2Image",
"Html2Pdf": "Html 转 Pdf IHtml2Pdf",
"Mask": "遮罩服务 MaskService",
"ContextMenu": "右键菜单 ContextMenu",
@@ -6989,5 +6990,12 @@
"NormalIntro": "通过设置 Text 参数设置要显示的文本",
"TypedOptionsTitle": "TypedOptions",
"TypedOptionsIntro": "通过设置 TypedOptions 参数的属性自定义打字速度、延时等设定"
+ },
+ "BootstrapBlazor.Server.Components.Samples.Html2Images": {
+ "Html2ImageTitle": "Html2Image 网页元素转成图片服务",
+ "Html2ImageIntro": "将网页中任意区域内容转化成图片服务",
+ "Html2ImageElementTitle": "ToPng",
+ "Html2ImageElementIntro": "通过调用 GetDataAsync 方法获得 base64-encoded 图片",
+ "Html2ImageButtonText": "Image"
}
}
diff --git a/src/BootstrapBlazor.Server/docs.json b/src/BootstrapBlazor.Server/docs.json
index 1ccd4be4a4c..d2d41806f84 100644
--- a/src/BootstrapBlazor.Server/docs.json
+++ b/src/BootstrapBlazor.Server/docs.json
@@ -88,6 +88,7 @@
"group-box": "GroupBoxes",
"handwritten": "Handwrittens",
"html-renderer": "HtmlRenderers",
+ "html2images": "Html2Images",
"html2pdf": "Html2Pdfs",
"label": "Labels",
"layout": "Layouts",
@@ -294,6 +295,7 @@
"link": {
"AntDesign": "http://www.antblazor.com/",
"Pear Admin": "http://www.pearadmin.com/",
- "SAPHP": "https://www.swiftadmin.net/"
+ "SAPHP": "https://www.swiftadmin.net/",
+ "Veterinary Hospital": "http://animal.jucun.zone/"
}
}
diff --git a/src/BootstrapBlazor/Extensions/BootstrapBlazorServiceCollectionExtensions.cs b/src/BootstrapBlazor/Extensions/BootstrapBlazorServiceCollectionExtensions.cs
index 12af4f4ac80..1a5b0327d7f 100644
--- a/src/BootstrapBlazor/Extensions/BootstrapBlazorServiceCollectionExtensions.cs
+++ b/src/BootstrapBlazor/Extensions/BootstrapBlazorServiceCollectionExtensions.cs
@@ -39,6 +39,9 @@ public static IServiceCollection AddBootstrapBlazor(this IServiceCollection serv
// Html2Pdf 服务
services.TryAddSingleton();
+ // Html2Image 服务
+ services.TryAddScoped();
+
// Table 导出服务
services.TryAddScoped();
diff --git a/src/BootstrapBlazor/Options/Html2ImageOptions.cs b/src/BootstrapBlazor/Options/Html2ImageOptions.cs
new file mode 100644
index 00000000000..14102492cb7
--- /dev/null
+++ b/src/BootstrapBlazor/Options/Html2ImageOptions.cs
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License
+// See the LICENSE file in the project root for more information.
+// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
+
+using System.Text.Json.Serialization;
+
+namespace BootstrapBlazor.Components;
+
+///
+/// Html2Image 选项类
+///
+public class Html2ImageOptions
+{
+ ///
+ /// 获得/设置 样式集合 默认 null
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? IncludeStyleProperties { get; set; }
+}
diff --git a/src/BootstrapBlazor/Services/DefaultHtml2ImageService.cs b/src/BootstrapBlazor/Services/DefaultHtml2ImageService.cs
new file mode 100644
index 00000000000..9a342a7a1ab
--- /dev/null
+++ b/src/BootstrapBlazor/Services/DefaultHtml2ImageService.cs
@@ -0,0 +1,68 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License
+// See the LICENSE file in the project root for more information.
+// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
+
+using Microsoft.Extensions.Logging;
+
+namespace BootstrapBlazor.Components;
+
+///
+/// 默认 Html to Image 实现
+///
+///
+///
+class DefaultHtml2ImageService(IJSRuntime runtime, ILogger logger) : IHtml2Image
+{
+ private JSModule? _jsModule;
+
+ ///
+ ///
+ ///
+ public Task GetDataAsync(string selector, Html2ImageOptions options) => Execute(selector, "toPng", options);
+
+ ///
+ ///
+ ///
+ public Task GetStreamAsync(string selector, Html2ImageOptions options) => ToBlob(selector, options);
+
+ private async Task Execute(string selector, string methodName, Html2ImageOptions options)
+ {
+ string? data = null;
+ try
+ {
+ _jsModule ??= await runtime.LoadModuleByName("html2image");
+ if (_jsModule != null)
+ {
+ data = await _jsModule.InvokeAsync("execute", selector, methodName, options);
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "{Execute} throw exception", nameof(Execute));
+ }
+ return data;
+ }
+
+ private async Task ToBlob(string selector, Html2ImageOptions options)
+ {
+ Stream? data = null;
+ try
+ {
+ _jsModule ??= await runtime.LoadModuleByName("html2image");
+ if (_jsModule != null)
+ {
+ var streamRef = await _jsModule.InvokeAsync("execute", selector, "toBlob", options);
+ if (streamRef != null)
+ {
+ data = await streamRef.OpenReadStreamAsync(streamRef.Length);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "{ToBlob} throw exception", nameof(ToBlob));
+ }
+ return data;
+ }
+}
diff --git a/src/BootstrapBlazor/Services/IHtml2Image.cs b/src/BootstrapBlazor/Services/IHtml2Image.cs
new file mode 100644
index 00000000000..36fd9943208
--- /dev/null
+++ b/src/BootstrapBlazor/Services/IHtml2Image.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License
+// See the LICENSE file in the project root for more information.
+// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
+
+namespace BootstrapBlazor.Components;
+
+///
+/// IHtml2Image 接口
+///
+public interface IHtml2Image
+{
+ ///
+ /// Export method
+ ///
+ /// 选择器
+ ///
+ Task GetDataAsync(string selector, Html2ImageOptions options);
+
+ ///
+ /// Export method
+ ///
+ /// 选择器
+ ///
+ Task GetStreamAsync(string selector, Html2ImageOptions options);
+}
diff --git a/src/BootstrapBlazor/wwwroot/lib/html2image/html-to-image.js b/src/BootstrapBlazor/wwwroot/lib/html2image/html-to-image.js
new file mode 100644
index 00000000000..b448f79499b
--- /dev/null
+++ b/src/BootstrapBlazor/wwwroot/lib/html2image/html-to-image.js
@@ -0,0 +1,2 @@
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).htmlToImage={})}(this,(function(t){"use strict";function e(t,e,n,r){return new(n||(n=Promise))((function(i,o){function u(t){try{a(r.next(t))}catch(t){o(t)}}function c(t){try{a(r.throw(t))}catch(t){o(t)}}function a(t){var e;t.done?i(t.value):(e=t.value,e instanceof n?e:new n((function(t){t(e)}))).then(u,c)}a((r=r.apply(t,e||[])).next())}))}function n(t,e){var n,r,i,o,u={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:c(0),throw:c(1),return:c(2)},"function"==typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function c(c){return function(a){return function(c){if(n)throw new TypeError("Generator is already executing.");for(;o&&(o=0,c[0]&&(u=0)),u;)try{if(n=1,r&&(i=2&c[0]?r.return:c[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,c[1])).done)return i;switch(r=0,i&&(c=[2&c[0],i.value]),c[0]){case 0:case 1:i=c;break;case 4:return u.label++,{value:c[1],done:!1};case 5:u.label++,r=c[1],c=[0];continue;case 7:c=u.ops.pop(),u.trys.pop();continue;default:if(!(i=u.trys,(i=i.length>0&&i[i.length-1])||6!==c[0]&&2!==c[0])){u=0;continue}if(3===c[0]&&(!i||c[1]>i[0]&&c[1]l||t.height>l)&&(t.width>l&&t.height>l?t.width>t.height?(t.height*=l/t.width,t.width=l):(t.width*=l/t.height,t.height=l):t.width>l?(t.height*=l/t.width,t.width=l):(t.width*=l/t.height,t.height=l))}(c),c.style.width="".concat(d),c.style.height="".concat(v),r.backgroundColor&&(a.fillStyle=r.backgroundColor,a.fillRect(0,0,c.width,c.height)),a.drawImage(u,0,0,c.width,c.height),[2,c]}}))}))}t.getFontEmbedCSS=function(t,r){return void 0===r&&(r={}),e(this,void 0,void 0,(function(){return n(this,(function(e){return[2,Y(t,r)]}))}))},t.toBlob=function(t,r){return void 0===r&&(r={}),e(this,void 0,void 0,(function(){return n(this,(function(e){switch(e.label){case 0:return[4,et(t,r)];case 1:return[4,f(e.sent())];case 2:return[2,e.sent()]}}))}))},t.toCanvas=et,t.toJpeg=function(t,r){return void 0===r&&(r={}),e(this,void 0,void 0,(function(){return n(this,(function(e){switch(e.label){case 0:return[4,et(t,r)];case 1:return[2,e.sent().toDataURL("image/jpeg",r.quality||1)]}}))}))},t.toPixelData=function(t,r){return void 0===r&&(r={}),e(this,void 0,void 0,(function(){var e,i,o,u;return n(this,(function(n){switch(n.label){case 0:return e=s(t,r),i=e.width,o=e.height,[4,et(t,r)];case 1:return u=n.sent(),[2,u.getContext("2d").getImageData(0,0,i,o).data]}}))}))},t.toPng=function(t,r){return void 0===r&&(r={}),e(this,void 0,void 0,(function(){return n(this,(function(e){switch(e.label){case 0:return[4,et(t,r)];case 1:return[2,e.sent().toDataURL()]}}))}))},t.toSvg=tt}));
+//# sourceMappingURL=html-to-image.js.map
diff --git a/src/BootstrapBlazor/wwwroot/modules/html2image.js b/src/BootstrapBlazor/wwwroot/modules/html2image.js
new file mode 100644
index 00000000000..883ea63d53d
--- /dev/null
+++ b/src/BootstrapBlazor/wwwroot/modules/html2image.js
@@ -0,0 +1,11 @@
+import '../lib/html2image/html-to-image.js'
+
+export async function execute(selector, methodName, options) {
+ let data = null;
+ const el = document.querySelector(selector);
+ if (el) {
+ const fn = htmlToImage[methodName];
+ data = await fn(el, {});
+ }
+ return data;
+}