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
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<Version>10.0.12</Version>
<Version>10.0.13</Version>
</PropertyGroup>

<PropertyGroup>
Expand Down
137 changes: 122 additions & 15 deletions src/components/BootstrapBlazor.PdfReader/PdfReader.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public partial class PdfReader
/// </summary>
/// <remarks>优先使用 <see cref="Url"/> 未提供 <see cref="Url"/> 时会尝试调用此回调获得流进行渲染</remarks>
[Parameter]
public Func<Task<Stream>>? OnGetStreamAsync { get; set; }
public Func<Task<Stream?>>? OnGetStreamAsync { get; set; }

[Inject, NotNull]
private IStringLocalizer<PdfReader>? Localizer { get; set; }
Expand All @@ -156,6 +156,8 @@ public partial class PdfReader
private bool _enableThumbnails = true;
private bool _showToolbar = true;
private PdfReaderFitMode _fitMode;
private string _lastStreamHash = string.Empty;
private long _lastStreamLength = 0;

/// <summary>
/// <inheritdoc/>
Expand Down Expand Up @@ -188,14 +190,16 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
{
_url = Url;
_currentPage = CurrentPage;
_enableThumbnails = EnableThumbnails;
_currentRotation = CurrentRotation;
_showToolbar = ShowToolbar;
_enableThumbnails = EnableThumbnails;
_fitMode = FitMode;
}

if (_url != Url)
{
_url = Url;
_lastStreamHash = string.Empty;
await InvokeVoidAsync("setUrl", Id, _url);
}
if (_currentPage != CurrentPage)
Expand Down Expand Up @@ -229,8 +233,124 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
_fitMode = FitMode;
await SetFitMode(_fitMode);
}
if (string.IsNullOrEmpty(Url))
{
Stream? stream = null;
if (OnGetStreamAsync != null)
{
stream = await OnGetStreamAsync();
}

await SetPdfStream(stream);
}
}

private async Task SetPdfStream(Stream? stream)
{
if (stream == null || stream == Stream.Null)
{
_lastStreamHash = string.Empty;
_lastStreamLength = 0;
await InvokeVoidAsync("setData", Id, null);
return;
}

Comment on lines +248 to +257
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Hashing the original stream in the non-NET6 path is fragile for non-seekable streams and unnecessarily re-reads the stream.

In SetPdfStream, the non-NET6 branch hashes the original stream after GetBytes has fully read it. For non-seekable streams this means ComputerHash(stream) runs at end-of-stream and produces an incorrect hash, and for seekable streams it forces a second pass. Since you already have pdfBytes, consider hashing pdfBytes in both NET6 and non-NET6 builds to avoid dependence on stream seekability and double-reading.

byte[] pdfBytes = await GetBytes(stream);

var currentLength = pdfBytes.Length;
if (_lastStreamLength != currentLength)
{
_lastStreamLength = currentLength;
await InvokeVoidAsync("setData", Id, pdfBytes);
return;
}

#if NET6_0
var currentHash = ComputerHash(pdfBytes);
#else
var currentHash = await ComputerHash(stream);
Comment on lines +269 to +271
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

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

Method name typo: ComputerHash should be ComputeHash.

Suggested change
var currentHash = ComputerHash(pdfBytes);
#else
var currentHash = await ComputerHash(stream);
var currentHash = ComputeHash(pdfBytes);
#else
var currentHash = await ComputeHash(stream);

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

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

Method name typo: ComputerHash should be ComputeHash.

Copilot uses AI. Check for mistakes.
#endif
Comment on lines +268 to +272
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

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

The hash computation logic is incorrect for non-NET6_0 targets. On line 271, you're trying to compute the hash of stream after it has already been fully read and converted to bytes on line 258. The stream position will be at the end, making the hash computation inaccurate or computing the hash of an already-consumed stream.

Since you already have pdfBytes available, you should compute the hash from the byte array instead. You can create a synchronous overload of ComputerHash that accepts byte[] for non-NET6_0 targets, or convert the conditional compilation to use the same byte-based approach for both targets.

Copilot uses AI. Check for mistakes.
if (_lastStreamHash != currentHash)
{
_lastStreamHash = currentHash;
await InvokeVoidAsync("setData", Id, pdfBytes);
}
}

private async Task<byte[]?> GetPdfStreamDataAsync()
{
byte[]? pdfBytes = null;
if (OnGetStreamAsync != null)
{
var stream = await OnGetStreamAsync();
if (stream == null || stream == Stream.Null)
{
_lastStreamHash = string.Empty;
_lastStreamLength = 0;
}
else
{
pdfBytes = await GetBytes(stream);
}
}
return pdfBytes;
}

private static async Task<byte[]> GetBytes(Stream stream)
{
using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream);
return memoryStream.ToArray();
}

/// <summary>
/// 设置 PDF 流数据方法
/// </summary>
/// <param name="stream"></param>
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

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

The XML documentation for the stream parameter is missing. Public API methods should have complete documentation including parameter descriptions. Add a <param name="stream"> tag to describe what the stream parameter represents (e.g., "The PDF file stream to load").

Suggested change
/// <param name="stream"></param>
/// <param name="stream">The PDF file stream to load.</param>

Copilot uses AI. Check for mistakes.
/// <returns></returns>
public async Task SetPdfStreamAsync(Stream stream)
{
using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream);
var pdfBytes = memoryStream.ToArray();
_lastStreamLength = pdfBytes.Length;
#if NET6_0
_lastStreamHash = ComputerHash(pdfBytes);
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

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

Method name typo: ComputerHash should be ComputeHash.

Copilot uses AI. Check for mistakes.
#else
_lastStreamHash = await ComputerHash(stream);
Comment on lines +311 to +320
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Using the original stream for hashing after copying introduces similar seekability issues and inconsistency with the byte-based path.

In SetPdfStreamAsync, you copy stream into memoryStream but, for non-NET6, you still compute _lastStreamHash from the original stream. As with SetPdfStream, this fails for non-seekable streams (you’ll hash only the remaining tail, often empty) and the hash won’t match the rendered pdfBytes. To keep behavior correct and consistent, compute the hash from pdfBytes for both NET6 and non-NET6 instead of using the original stream.

Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

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

Method name typo: ComputerHash should be ComputeHash.

Copilot uses AI. Check for mistakes.
#endif
await InvokeVoidAsync("setData", Id, pdfBytes);
Comment on lines +311 to +322
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

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

The hash computation logic is incorrect for non-NET6_0 targets. On line 320, you're trying to compute the hash of the original stream parameter after it has already been fully read and copied to memoryStream on line 314. At this point, the stream position is at the end, and even if it's seekable, it will compute the hash of an empty or already-consumed stream.

You should either:

  1. Compute the hash from pdfBytes instead of stream (like you do for NET6_0)
  2. Pass memoryStream to the hash function and ensure it's seeked back to the beginning

The simplest fix is to use ComputerHash(pdfBytes) for the non-NET6_0 case as well, since you already have the byte array.

Copilot uses AI. Check for mistakes.
}

/// <summary>
/// 设置 Pdf Base64 数据方法
/// </summary>
/// <param name="base64Data"></param>
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

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

The XML documentation for the base64Data parameter is missing. Public API methods should have complete documentation including parameter descriptions. Add a <param name="base64Data"> tag to describe what the parameter represents (e.g., "The Base64-encoded PDF data").

Suggested change
/// <param name="base64Data"></param>
/// <param name="base64Data">The Base64-encoded PDF data.</param>

Copilot uses AI. Check for mistakes.
/// <returns></returns>
public async Task SetPdfBase64DataAsync(string base64Data)
{
var pdfBytes = Convert.FromBase64String(base64Data);
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

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

The SetPdfBase64DataAsync method doesn't update the _lastStreamHash and _lastStreamLength fields like SetPdfStreamAsync does. This inconsistency means that if the component state tracking logic depends on these fields, calling this method could lead to unexpected behavior or redundant updates. Consider whether these fields should be updated here for consistency, or document why they don't need to be.

Suggested change
var pdfBytes = Convert.FromBase64String(base64Data);
var pdfBytes = Convert.FromBase64String(base64Data);
_lastStreamLength = pdfBytes.Length;
#if NET6_0
_lastStreamHash = ComputerHash(pdfBytes);
#else
using (var memoryStream = new MemoryStream(pdfBytes))
{
_lastStreamHash = await ComputerHash(memoryStream);
}
#endif

Copilot uses AI. Check for mistakes.
await InvokeVoidAsync("setData", Id, pdfBytes);
}

#if NET6_0
private static string ComputerHash(byte[] data)
{
var hashBytes = System.Security.Cryptography.SHA256.HashData(data);
return Convert.ToBase64String(hashBytes);
}
Comment on lines +337 to +341
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

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

The method name has a typo: ComputerHash should be ComputeHash. "Computer" is a noun (the machine), while "Compute" is the verb (to calculate). This typo appears in all three occurrences of the method (lines 337, 343, and their usages).

Copilot uses AI. Check for mistakes.
#else
private static async Task<string> ComputerHash(Stream stream)
{
if (stream.CanSeek)
{
stream.Seek(0, SeekOrigin.Begin);
}
var hashBytes = await System.Security.Cryptography.SHA256.HashDataAsync(stream);
return Convert.ToBase64String(hashBytes);
}
Comment on lines +343 to +351
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

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

The method name has a typo: ComputerHash should be ComputeHash. This is the async overload version of the same incorrectly named method.

Copilot uses AI. Check for mistakes.
#endif

/// <summary>
/// <inheritdoc/>
/// </summary>
Expand Down Expand Up @@ -278,19 +398,6 @@ protected override async Task InvokeInitAsync()
/// <returns></returns>
public Task RotateRight() => InvokeVoidAsync("rotate", Id, 90);

private async Task<byte[]?> GetPdfStreamDataAsync()
{
byte[]? pdfBytes = null;
if (OnGetStreamAsync != null)
{
using var memoryStream = new MemoryStream();
var stream = await OnGetStreamAsync();
await stream.CopyToAsync(memoryStream);
pdfBytes = memoryStream.ToArray();
}
return pdfBytes;
}

/// <summary>
/// 页面开始初始化时回调方法
/// </summary>
Expand Down
28 changes: 24 additions & 4 deletions src/components/BootstrapBlazor.PdfReader/PdfReader.razor.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export async function setUrl(id, url) {

const { options } = pdf;
options.url = url;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Switching from a blob-based data source to a URL does not revoke any existing object URL, which can leak memory.

setData revokes any existing pdf.objectUrl before creating a new blob URL, but setUrl only updates options.url and leaves a previous objectUrl intact. If a consumer calls setData then setUrl, the old object URL is never revoked, causing a memory leak. Consider mirroring the cleanup in setUrl by revoking pdf.objectUrl (if set) before assigning the new URL.

options.data = null;
await loadPdf(pdf);
}

Expand All @@ -48,12 +47,33 @@ export async function setData(id, data) {
return;
}

const { options } = pdf;
options.url = null;
options.data = data;
const { options, objectUrl } = pdf;
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
}

options.url = createObjectURLFromByte(data);
options.data = null;
pdf.objectUrl = options.url;
Comment on lines +50 to +57
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

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

Memory leak: The objectUrl cleanup in the dispose function (line 737) doesn't revoke object URLs. When the component is disposed, you should revoke any object URLs created with URL.createObjectURL to prevent memory leaks. Add if (pdf.objectUrl) { URL.revokeObjectURL(pdf.objectUrl); } in the disposePdf function.

Copilot uses AI. Check for mistakes.
await loadPdf(pdf);
}

const createObjectURLFromBase64 = base64Data => {
const binaryString = atob(base64Data);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}

const blob = new Blob([bytes], { type: 'application/pdf' });
return URL.createObjectURL(blob);
}
Comment on lines +61 to +70
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

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

The function createObjectURLFromBase64 is defined but not used anywhere in this file. Given that SetPdfBase64DataAsync in the C# code converts the base64 to bytes and passes it to setData, which then uses createObjectURLFromByte, this function appears to be unused. Consider removing it if it's not needed, or export it if it's intended for future use or external consumption.

Suggested change
const createObjectURLFromBase64 = base64Data => {
const binaryString = atob(base64Data);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
const blob = new Blob([bytes], { type: 'application/pdf' });
return URL.createObjectURL(blob);
}

Copilot uses AI. Check for mistakes.

const createObjectURLFromByte = bytes => {
const blob = new Blob([bytes], { type: 'application/pdf' });
return URL.createObjectURL(blob);
}

export function setScaleValue(id, value) {
const { pdfViewer } = Data.get(id);
if (pdfViewer) {
Expand Down