Skip to content

feat(PdfReader): add OnGetStreamAsync parameter#813

Merged
ArgoZhang merged 2 commits intomasterfrom
feat-pdf
Dec 12, 2025
Merged

feat(PdfReader): add OnGetStreamAsync parameter#813
ArgoZhang merged 2 commits intomasterfrom
feat-pdf

Conversation

@ArgoZhang
Copy link
Copy Markdown
Member

@ArgoZhang ArgoZhang commented Dec 12, 2025

Link issues

fixes #812

Summary By Copilot

Regression?

  • Yes
  • No

Risk

  • High
  • Medium
  • Low

Verification

  • Manual (required)
  • Automated

Packaging changes reviewed?

  • Yes
  • No
  • N/A

☑️ Self Check before Merge

⚠️ Please check all items below before review. ⚠️

  • Doc is updated/provided or not needed
  • Demo is updated/provided or not needed
  • Merge the latest code from the main branch

Summary by Sourcery

Add support for initializing PdfReader with a PDF data stream in addition to a URL and pass the resolved PDF bytes into the JS init call.

New Features:

  • Introduce an OnGetStreamAsync callback parameter on PdfReader to provide a PDF stream when no Url is supplied.

Enhancements:

  • Update PdfReader initialization to optionally load PDF content from a provided stream and send the resulting byte data to the client-side initializer.

Copilot AI review requested due to automatic review settings December 12, 2025 01:48
@bb-auto bb-auto Bot added the enhancement New feature or request label Dec 12, 2025
@bb-auto bb-auto Bot added this to the v9.2.0 milestone Dec 12, 2025
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Dec 12, 2025

Reviewer's Guide

Adds a new OnGetStreamAsync callback to PdfReader to support loading PDF content from a stream, and updates initialization to optionally pass the PDF bytes to the JS interop instead of only using a URL.

Sequence diagram for PdfReader initialization with optional stream

sequenceDiagram
    participant BlazorRuntime
    participant PdfReader
    participant OnGetStreamAsync as OnGetStreamAsyncDelegate
    participant JSInterop as JavaScriptInterop

    BlazorRuntime->>PdfReader: InvokeInitAsync()
    activate PdfReader
    PdfReader->>PdfReader: GetPdfStreamDataAsync()
    activate PdfReader
    alt OnGetStreamAsync is set
        PdfReader->>OnGetStreamAsync: Invoke()
        activate OnGetStreamAsync
        OnGetStreamAsync-->>PdfReader: Stream
        deactivate OnGetStreamAsync
        PdfReader->>PdfReader: CopyToAsync(memoryStream)
        PdfReader-->>PdfReader: pdfBytes
    else OnGetStreamAsync is null
        PdfReader-->>PdfReader: pdfBytes is null
    end
    deactivate PdfReader
    PdfReader->>JSInterop: init(Id, Interop, { Url, Data = pdfBytes, ... })
    deactivate PdfReader
Loading

Class diagram for updated PdfReader component

classDiagram
    class PdfReader {
        +string? Url
        +object? FitMode
        +bool EnableThumbnails
        +int CurrentPage
        +Func~Task~? OnPrintingAsync
        +Func~Task`1~? OnGetStreamAsync
        +Task InvokeInitAsync()
        +Task RotateLeft()
        +Task RotateRight()
        -Task`1 GetPdfStreamDataAsync()
    }

    class OnGetStreamAsyncDelegate {
        <<delegate>>
        +Task StreamInvoker()
    }

    PdfReader --> OnGetStreamAsyncDelegate : uses
Loading

Flow diagram for GetPdfStreamDataAsync logic

flowchart TD
    A[GetPdfStreamDataAsync start] --> B{OnGetStreamAsync is not null}
    B -- Yes --> C[Create MemoryStream]
    C --> D[Invoke OnGetStreamAsync]
    D --> E[Get Stream result]
    E --> F[Copy stream to MemoryStream]
    F --> G[Convert MemoryStream to byte array]
    G --> H[Return pdfBytes]
    B -- No --> I[Set pdfBytes to null]
    I --> H[Return pdfBytes]
Loading

File-Level Changes

Change Details Files
Introduce OnGetStreamAsync parameter to allow loading PDF content from a stream when Url is not provided.
  • Add nullable Func<Task> OnGetStreamAsync Blazor parameter with documentation comments describing precedence relative to Url
  • Inject the parameter into the PdfReader component API for consumers to provide a stream-based source
src/components/BootstrapBlazor.PdfReader/PdfReader.razor.cs
Update initialization flow to optionally load PDF bytes from OnGetStreamAsync and pass them to JS interop.
  • Change InvokeInitAsync from expression-bodied synchronous Task to async method
  • Add private GetPdfStreamDataAsync helper that invokes OnGetStreamAsync, copies the stream to a MemoryStream, and returns the PDF bytes
  • Extend the JS init call payload to include Data = _data while still passing Url and existing configuration/trigger flags
src/components/BootstrapBlazor.PdfReader/PdfReader.razor.cs

Assessment against linked issues

Issue Objective Addressed Explanation
#812 Add an OnGetStreamAsync parameter to the PdfReader component that allows loading a PDF from a provided stream (used during initialization, alongside the existing Url parameter).

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@ArgoZhang ArgoZhang merged commit 30f142a into master Dec 12, 2025
5 of 6 checks passed
@ArgoZhang ArgoZhang deleted the feat-pdf branch December 12, 2025 01:49
Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • The XML remarks say the stream callback is only used when Url is not provided, but GetPdfStreamDataAsync currently calls OnGetStreamAsync unconditionally; consider short-circuiting when Url is set to avoid unnecessary stream work and keep behavior aligned with the docs.
  • In GetPdfStreamDataAsync, the stream returned from OnGetStreamAsync is never disposed; wrapping it in a using/await using (or allowing the callback to handle disposal explicitly by convention) would avoid potential resource leaks.
  • You can simplify GetPdfStreamDataAsync by returning directly from the happy path (e.g., return memoryStream.ToArray();) and eliminating the separate pdfBytes variable and trailing return, which makes the method slightly clearer.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The XML remarks say the stream callback is only used when `Url` is not provided, but `GetPdfStreamDataAsync` currently calls `OnGetStreamAsync` unconditionally; consider short-circuiting when `Url` is set to avoid unnecessary stream work and keep behavior aligned with the docs.
- In `GetPdfStreamDataAsync`, the stream returned from `OnGetStreamAsync` is never disposed; wrapping it in a `using`/`await using` (or allowing the callback to handle disposal explicitly by convention) would avoid potential resource leaks.
- You can simplify `GetPdfStreamDataAsync` by returning directly from the happy path (e.g., `return memoryStream.ToArray();`) and eliminating the separate `pdfBytes` variable and trailing `return`, which makes the method slightly clearer.

## Individual Comments

### Comment 1
<location> `src/components/BootstrapBlazor.PdfReader/PdfReader.razor.cs:284-287` </location>
<code_context>
+    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();
</code_context>

<issue_to_address>
**issue (bug_risk):** Dispose the stream returned by OnGetStreamAsync to avoid resource leaks.

The stream returned from OnGetStreamAsync is never disposed. If it’s a FileStream or similar unmanaged resource, this can leak handles until finalization. Wrap the returned stream in a using/await using block, or clearly document that the delegate must return a non-resource stream (e.g., MemoryStream). For example:

```csharp
if (OnGetStreamAsync != null)
{
    await using var stream = await OnGetStreamAsync();
    using var memoryStream = new MemoryStream();
    await stream.CopyToAsync(memoryStream);
    pdfBytes = memoryStream.ToArray();
}
```
</issue_to_address>

### Comment 2
<location> `src/components/BootstrapBlazor.PdfReader/PdfReader.razor.cs:240-244` </location>
<code_context>
-        TriggerScaleChanged = OnScaleChangedAsync != null,
-        TriggerRotationChanged = OnRotationChanged != null,
-    });
+        var _data = await GetPdfStreamDataAsync();
+        await InvokeVoidAsync("init", Id, Interop, new
+        {
+            Url,
+            Data = _data,
+            FitMode,
+            EnableThumbnails,
</code_context>

<issue_to_address>
**suggestion (performance):** Avoid calling OnGetStreamAsync when Url is provided to align with the documented precedence and reduce unnecessary work.

Per the XML remarks, Url should take precedence and the stream callback should only be used when Url is not provided. The current code always calls GetPdfStreamDataAsync, which can incur unnecessary I/O when Url is set and ultimately used by the JS layer.

You could short-circuit the stream loading:

```csharp
protected override async Task InvokeInitAsync()
{
    byte[]? data = null;
    if (string.IsNullOrEmpty(Url))
    {
        data = await GetPdfStreamDataAsync();
    }

    await InvokeVoidAsync("init", Id, Interop, new
    {
        Url,
        Data = data,
        // ...
    });
}
```

This aligns with the documented precedence and avoids unnecessary work when Url is provided.
</issue_to_address>

### Comment 3
<location> `src/components/BootstrapBlazor.PdfReader/PdfReader.razor.cs:281-292` </location>
<code_context>
+        if (OnGetStreamAsync != null)
+        {
+            using var memoryStream = new MemoryStream();
+            var stream = await OnGetStreamAsync();
+            await stream.CopyToAsync(memoryStream);
+            pdfBytes = memoryStream.ToArray();
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Guard against OnGetStreamAsync returning null to avoid a possible NullReferenceException.

Since `OnGetStreamAsync` is an extension point, an implementation might legitimately (or accidentally) return null, causing `await stream.CopyToAsync(memoryStream)` to throw. Consider handling the null case explicitly (e.g. return null or throw a descriptive exception) before calling `CopyToAsync` to avoid an opaque NRE and make callback issues easier to diagnose.

```suggestion
    private async Task<byte[]?> GetPdfStreamDataAsync()
    {
        byte[]? pdfBytes = null;
        if (OnGetStreamAsync != null)
        {
            var stream = await OnGetStreamAsync();
            if (stream is null)
            {
                throw new InvalidOperationException("OnGetStreamAsync returned null. A non-null stream is required to read PDF data.");
            }

            using var memoryStream = new MemoryStream();
            await stream.CopyToAsync(memoryStream);
            pdfBytes = memoryStream.ToArray();
        }
        return pdfBytes;
    }
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds support for loading PDF documents via stream data in the PdfReader component, providing an alternative to URL-based loading. The implementation adds a new OnGetStreamAsync callback parameter that can be used to supply PDF data as a stream when a URL is not available.

Key Changes:

  • Added OnGetStreamAsync parameter to enable PDF loading from streams
  • Modified InvokeInitAsync to retrieve PDF data from the callback and pass it to JavaScript
  • Incremented package version from 10.0.11 to 10.0.12

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
PdfReader.razor.cs Added OnGetStreamAsync parameter, refactored InvokeInitAsync to support stream-based PDF loading, and implemented GetPdfStreamDataAsync helper method
BootstrapBlazor.PdfReader.csproj Bumped version to 10.0.12
Comments suppressed due to low confidence (1)

src/components/BootstrapBlazor.PdfReader/PdfReader.razor.cs:206

  • Equality checks on floating point values can yield unexpected results.
        if (_currentRotation != CurrentRotation)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +281 to +292
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;
}
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The stream returned by OnGetStreamAsync is not being disposed. When a callback returns a Stream, the caller is typically responsible for disposing it. The stream should be wrapped in a using statement to ensure proper resource cleanup and prevent memory leaks.

Copilot uses AI. Check for mistakes.
TriggerScaleChanged = OnScaleChangedAsync != null,
TriggerRotationChanged = OnRotationChanged != null,
});
var _data = await GetPdfStreamDataAsync();
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The GetPdfStreamDataAsync method is called unconditionally and will execute even when Url is provided. According to the documentation comment on OnGetStreamAsync (line 133), the callback should only be invoked when Url is not provided. This wastes resources and doesn't align with the documented behavior. The method should only be called when Url is null or empty.

Suggested change
var _data = await GetPdfStreamDataAsync();
object? _data = null;
if (string.IsNullOrEmpty(Url))
{
_data = await GetPdfStreamDataAsync();
}

Copilot uses AI. Check for mistakes.
Comment on lines +238 to +255
protected override async Task InvokeInitAsync()
{
Url,
FitMode,
EnableThumbnails,
CurrentPage,
TriggerPagesInit = OnPagesInitAsync != null,
TriggerPagesLoaded = OnPagesLoadedAsync != null,
TriggerPageChanged = OnPageChangedAsync != null,
TriggerTowPagesOnViewChanged = OnTwoPagesOneViewAsync != null,
TriggerScaleChanged = OnScaleChangedAsync != null,
TriggerRotationChanged = OnRotationChanged != null,
});
var _data = await GetPdfStreamDataAsync();
await InvokeVoidAsync("init", Id, Interop, new
{
Url,
Data = _data,
FitMode,
EnableThumbnails,
CurrentPage,
TriggerPagesInit = OnPagesInitAsync != null,
TriggerPagesLoaded = OnPagesLoadedAsync != null,
TriggerPageChanged = OnPageChangedAsync != null,
TriggerTowPagesOnViewChanged = OnTwoPagesOneViewAsync != null,
TriggerScaleChanged = OnScaleChangedAsync != null,
TriggerRotationChanged = OnRotationChanged != null,
});
}
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The JavaScript code (PdfReader.razor.js line 19-21) checks if options.url is null and returns early, which means when only Data is provided without a Url, the PDF will not load. The JavaScript code needs to be updated to handle the case where options.data is provided instead of options.url. Currently, this feature will not work as intended.

Copilot uses AI. Check for mistakes.
if (OnGetStreamAsync != null)
{
using var memoryStream = new MemoryStream();
var stream = await OnGetStreamAsync();
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The stream returned by OnGetStreamAsync is not null-checked before use. If the callback returns null, this will cause a NullReferenceException when calling CopyToAsync. Add a null check after awaiting OnGetStreamAsync to handle this case gracefully.

Suggested change
var stream = await OnGetStreamAsync();
var stream = await OnGetStreamAsync();
if (stream == null)
{
return null;
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(PdfReader): add OnGetStreamAsync parameter

2 participants