Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions BootstrapBlazor.Extensions.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
<Project Path="src/components/BootstrapBlazor.CodeEditor/BootstrapBlazor.CodeEditor.csproj" />
<Project Path="src/components/BootstrapBlazor.Dock/BootstrapBlazor.Dock.csproj" />
<Project Path="src/components/BootstrapBlazor.DockView/BootstrapBlazor.DockView.csproj" />
<Project Path="src/components/BootstrapBlazor.Dom2Image/BootstrapBlazor.Dom2Image.csproj" />
<Project Path="src/components/BootstrapBlazor.DriverJs/BootstrapBlazor.DriverJs.csproj" />
<Project Path="src/components/BootstrapBlazor.ElementIcon/BootstrapBlazor.ElementIcon.csproj" />
<Project Path="src/components/BootstrapBlazor.FileSystem/BootstrapBlazor.FileSystem.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<Version>10.0.0</Version>
</PropertyGroup>

<PropertyGroup>
<PackageTags>Bootstrap Blazor WebAssembly wasm UI Components Dom Image</PackageTags>
<Description>Bootstrap UI components extensions of DomToImage use snapDom lib</Description>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BootstrapBlazor" Version="$(BBVersion)" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/

using BootstrapBlazor.Components;

namespace Microsoft.Extensions.DependencyInjection;

/// <summary>
/// BootstrapBlazor 服务扩展类
/// </summary>
public static class BootstrapBlazorDom2ImageServiceExtensions
{
/// <summary>
/// 添加 AzureOpenAIService 服务
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XML comment states "添加 AzureOpenAIService 服务" (Add AzureOpenAIService service), but this method is adding IDom2ImageService. This appears to be a copy-paste error from another service extension.

Suggested change
/// 添加 AzureOpenAIService 服务
/// 添加 Dom2Image 服务

Copilot uses AI. Check for mistakes.
/// </summary>
/// <param name="services"></param>
public static IServiceCollection AddBootstrapBlazorDom2ImageService(this IServiceCollection services)
{
services.AddScoped<IDom2ImageService, DefaultDom2ImageService>();
#if NET8_0_OR_GREATER
services.AddKeyedScoped<IDom2ImageService, DefaultDom2ImageService>("BootstrapBlazor.Dom2Image");
#endif
return services;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/

using Microsoft.Extensions.Logging;
using Microsoft.JSInterop;

namespace BootstrapBlazor.Components;

/// <summary>
/// 默认 Html to Image 实现
/// <param name="runtime"></param>
/// <param name="logger"></param>
/// </summary>
class DefaultDom2ImageService(IJSRuntime runtime, ILogger<DefaultDom2ImageService> logger) : IDom2ImageService
{
private JSModule? _jsModule;

/// <summary>
/// <inheritdoc/>
/// </summary>
public async Task<string?> GetUrlAsync(string selector, Dom2ImageOptions? options = null, CancellationToken token = default)
{
string? data = null;
try
{
_jsModule ??= await LoadModule();
data = await _jsModule.InvokeAsync<string?>("getUrl", token, selector, options);
}
catch (OperationCanceledException) { }
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Poor error handling: empty catch block.

Copilot uses AI. Check for mistakes.
catch (Exception ex)
{
logger.LogError(ex, "{GetUrlAsync} throw exception: {ex}", nameof(GetUrlAsync), ex.Format());
}
Comment on lines +31 to +34
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generic catch clause.

Copilot uses AI. Check for mistakes.
return data;
}

/// <summary>
/// <inheritdoc/>
/// </summary>
public async Task<Stream?> GetStreamAsync(string selector, Dom2ImageOptions? options = null, CancellationToken token = default)
{
Stream? data = null;
try
{
_jsModule ??= await LoadModule();
var streamReference = await _jsModule.InvokeAsync<IJSStreamReference?>("getStream", selector, options);
if (streamReference != null)
{
data = await streamReference.OpenReadStreamAsync(streamReference.Length, token);
}
}
catch (OperationCanceledException) { }
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Poor error handling: empty catch block.

Suggested change
catch (OperationCanceledException) { }
catch (OperationCanceledException ex)
{
logger.LogWarning(ex, "{GetStreamAsync} operation was canceled.", nameof(GetStreamAsync));
}

Copilot uses AI. Check for mistakes.
catch (Exception ex)
{
logger.LogError(ex, "{GetStreamAsync} throw exception: {ex}", nameof(GetStreamAsync), ex.Format());
}
Comment on lines +54 to +57
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generic catch clause.

Copilot uses AI. Check for mistakes.
return data;
}

/// <summary>
/// <inheritdoc/>
/// </summary>
/// <param name="selector"></param>
/// <param name="fileName"></param>
/// <param name="format"></param>
/// <param name="backgroundColor"></param>
/// <param name="options"></param>
/// <returns></returns>
public async Task DownloadAsync(string selector, string fileName = "capture", string? format = "png", string? backgroundColor = null, Dom2ImageOptions? options = null)
{
try
{
_jsModule ??= await LoadModule();
await _jsModule.InvokeAsync<IJSStreamReference?>("downloadAsync", selector, fileName, format, backgroundColor, options);
}
catch (OperationCanceledException) { }
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Poor error handling: empty catch block.

Suggested change
catch (OperationCanceledException) { }
catch (OperationCanceledException ex)
{
logger.LogWarning(ex, "{DownloadAsync} operation was canceled.", nameof(DownloadAsync));
}

Copilot uses AI. Check for mistakes.
catch (Exception ex)
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generic catch clause.

Suggested change
catch (Exception ex)
catch (JSException ex)

Copilot uses AI. Check for mistakes.
{
logger.LogError(ex, "{DownloadAsync} throw exception: {ex}", nameof(DownloadAsync), ex.Format());
}
}

private Task<JSModule> LoadModule() => runtime.LoadModule("./_content/BootstrapBlazor.Dom2Image/dom2image.js");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// 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;

/// <summary>
/// Dom2ImageOptions 选项类
/// </summary>
public class Dom2ImageOptions
{
/// <summary>
/// Removes redundant styles. Default value is true
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? Compress { get; set; }
Comment thread
ArgoZhang marked this conversation as resolved.

/// <summary>
/// Skips idle delay for faster results. Default value is true
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? Fast { get; set; }

/// <summary>
/// Inlines fonts (icon fonts always embedded). Default value is false
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? EmbedFonts { get; set; }

/// <summary>
/// Output scale multiplier. Default value is 1
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int? Scale { get; set; }

/// <summary>
/// Device pixel ratio
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int? Dpr { get; set; }

/// <summary>
/// Output specific width size
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int? Width { get; set; }

/// <summary>
/// Output specific height size
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int? Height { get; set; }

/// <summary>
/// Fallback color for JPG/WebP. Default value is #fff
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? BackgroundColor { get; set; }

/// <summary>
/// Quality for JPG/WebP (0 to 1)
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public float? Quality { get; set; }

/// <summary>
/// Select png, jpg, webp Blob type. Default value is svg
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Type { get; set; }

/// <summary>
/// CSS selectors for elements to exclude
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string[]? Exclude { get; set; }

/// <summary>
///
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing documentation for the LocalFonts property. The XML comment is empty and should describe what this property does and how it should be used.

Suggested change
///
/// Specifies a list of local font file URLs or font names to embed in the output image.
/// Use this to include custom fonts that are not loaded by default. Provide font file paths or names as strings.

Copilot uses AI. Check for mistakes.
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string[]? LocalFonts { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/

namespace BootstrapBlazor.Components;

/// <summary>
/// IDom2ImageService 接口定义
/// </summary>
public interface IDom2ImageService
{
/// <summary>
/// 通过指定选择器获得 Html 元素返回图片数据
/// </summary>
/// <param name="selector"></param>
/// <param name="options"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<string?> GetUrlAsync(string selector, Dom2ImageOptions? options = null, CancellationToken token = default);

/// <summary>
/// 通过指定选择器获得 Html 元素返回图片数据流
/// </summary>
/// <param name="selector"></param>
/// <param name="options"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<Stream?> GetStreamAsync(string selector, Dom2ImageOptions? options = null, CancellationToken token = default);

/// <summary>
/// 通过指定选择器下载 Html 元素图片
/// </summary>
/// <param name="selector"></param>
/// <param name="fileName"></param>
/// <param name="format"></param>
/// <param name="backgroundColor"></param>
/// <param name="options"></param>
/// <returns></returns>
Task DownloadAsync(string selector, string fileName = "capture", string? format = "png", string? backgroundColor = null, Dom2ImageOptions? options = null);
}
33 changes: 33 additions & 0 deletions src/components/BootstrapBlazor.Dom2Image/wwwroot/dom2image.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { snapdom } from './lib/snapdom.min.mjs'

export async function getUrl(selector, options) {
let data = null;
const el = document.querySelector(selector);
if (el) {
const result = await snapdom(el, options || {});
data = result.url;
}
return data;
}

export async function getStream(selector, options) {
let data = null;
const el = document.querySelector(selector);
if (el) {
const result = await snapdom(el, options || {});
data = result.toBlob();
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getStream function returns result.toBlob() which is a Promise, but the function doesn't await it. This should be await result.toBlob() to properly handle the asynchronous operation.

Suggested change
data = result.toBlob();
data = await result.toBlob();

Copilot uses AI. Check for mistakes.
}
return data;
}

export async function downloadAsync(selector, filename, format, backgroundColor, options) {
const el = document.querySelector(selector);
if (el) {
const result = await snapdom(el, options || {});
await result.download({
format,
filename,
backgroundColor
});
}
}
Loading
Loading