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; +}