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
51 changes: 27 additions & 24 deletions src/components/BootstrapBlazor.PdfReader/PdfReader.razor
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,34 @@
@inherits BootstrapModuleComponentBase

<div @attributes="@AdditionalAttributes" id="@Id" class="@ClassString" style="@StyleString">
<div class="bb-view-toolbar">
<div class="bb-view-title">
<div class="bb-view-icon bb-view-bar"><i class="fa-solid fa-bars"></i></div>
<span class="bb-view-subject">@_docTitle</span>
@if (Options.ShowToolbar)
{
<div class="bb-view-toolbar init">
<div class="bb-view-title">
<div class="bb-view-icon bb-view-bar"><i class="fa-solid fa-bars"></i></div>
<span class="bb-view-subject">@_docTitle</span>
</div>
<div class="@ViewBodyString">
<input type="text" class="bb-view-num" @bind="CurrentPageString" /><span class="bb-view-slash">/</span><div class="bb-view-pagesCount"></div>
<div class="bb-view-divider"></div>
<div class="bb-view-icon"><i class="fa-solid fa-minus"></i></div>
<input type="text" class="bb-view-scale" value="100%" />
<div class="bb-view-icon"><i class="fa-solid fa-plus"></i></div>
<div class="bb-view-divider"></div>
<div class="bb-view-icon bb-view-fit-page" @onclick="FitToPage"><i class="fa-solid fa-arrows-left-right-to-line"></i></div>
<div class="bb-view-icon bb-view-fit-width" @onclick="FitToWidth"><i class="fa-solid fa-arrows-left-right"></i></div>
<div class="bb-view-icon bb-view-fit-rotate" @onclick="RotateLeft"><i class="fa-solid fa-rotate-left"></i></div>
<div class="bb-view-icon bb-view-fit-rotate" @onclick="RotateRight"><i class="fa-solid fa-rotate-right"></i></div>
<div class="bb-view-divider"></div>
<div class="bb-view-icon bb-view-draw"><i class="fa-solid fa-pen-to-square"></i></div>
</div>
<div class="bb-view-controls">
<div class="bb-view-icon bb-view-download"><i class="fa-solid fa-arrow-right-to-bracket fa-rotate-90"></i></div>
<div class="bb-view-icon bb-view-print"><i class="fa-solid fa-print"></i></div>
<div class="bb-view-icon bb-view-home"><i class="fa-solid fa-flag"></i></div>
</div>
</div>
<div class="@ViewBodyString">
<input type="text" class="bb-view-num" value="1" /><span class="bb-view-slash">/</span><div>14</div>
<div class="bb-view-divider"></div>
<div class="bb-view-icon"><i class="fa-solid fa-minus"></i></div>
<input type="text" class="bb-view-scale" value="100%" />
<div class="bb-view-icon"><i class="fa-solid fa-plus"></i></div>
<div class="bb-view-divider"></div>
<div class="bb-view-icon bb-view-fit-page" @onclick="FitToPage"><i class="fa-solid fa-arrows-left-right-to-line"></i></div>
<div class="bb-view-icon bb-view-fit-width" @onclick="FitToWidth"><i class="fa-solid fa-arrows-left-right"></i></div>
<div class="bb-view-icon bb-view-fit-rotate" @onclick="RotateLeft"><i class="fa-solid fa-rotate-left"></i></div>
<div class="bb-view-icon bb-view-fit-rotate" @onclick="RotateRight"><i class="fa-solid fa-rotate-right"></i></div>
<div class="bb-view-divider"></div>
<div class="bb-view-icon bb-view-draw"><i class="fa-solid fa-pen-to-square"></i></div>
</div>
<div class="bb-view-controls">
<div class="bb-view-icon bb-view-download"><i class="fa-solid fa-arrow-right-to-bracket fa-rotate-90"></i></div>
<div class="bb-view-icon bb-view-print"><i class="fa-solid fa-print"></i></div>
<div class="bb-view-icon bb-view-home"><i class="fa-solid fa-flag"></i></div>
</div>
</div>
}
<div class="bb-view-container">
<div class="pdfViewer"></div>
</div>
Expand Down
98 changes: 69 additions & 29 deletions src/components/BootstrapBlazor.PdfReader/PdfReader.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Website: https://www.blazor.zone or https://argozhang.github.io/

using Microsoft.AspNetCore.Components;
using System.Globalization;

namespace BootstrapBlazor.Components;

Expand All @@ -13,38 +14,43 @@ namespace BootstrapBlazor.Components;
public partial class PdfReader
{
/// <summary>
/// 获得/设置 PDF 文档路径
/// 获得/设置 <see cref="PdfReaderOptions"/> 配置项实例
/// </summary>
[Parameter]
public string? Url { get; set; }

/// <summary>
/// 获得/设置 PDF 组件高度 默认 600px
/// </summary>
[Parameter]
public string? ViewHeight { get; set; }

/// <summary>
/// 获得/设置 是否适配当前页面宽度 默认 false
/// </summary>
[Parameter]
public bool IsFitToPage { get; set; }
[NotNull]
public PdfReaderOptions? Options { get; set; }

private string? ClassString => CssBuilder.Default("bb-pdf-reader")
.AddClassFromAttributes(AdditionalAttributes)
.Build();

private string? StyleString => CssBuilder.Default()
.AddClass($"--bb-pdf-view-height: {ViewHeight};", !string.IsNullOrEmpty(ViewHeight))
.AddClass($"--bb-pdf-view-height: {Options.ViewHeight};", !string.IsNullOrEmpty(Options.ViewHeight))
.AddClassFromAttributes(AdditionalAttributes)
.Build();

private string? ViewBodyString => CssBuilder.Default("bb-view-body")
.AddClass("fit-page", IsFitToPage)
.AddClass("fit-page", Options.IsFitToPage)
.Build();

private string? _docTitle;
private bool _isFitToPage;
private uint _currentPage;
private string? _url;

private string CurrentPageString
{
get => Options.CurrentPage.ToString(CultureInfo.InvariantCulture);
set => SetCurrentPage(value);
}

private void SetCurrentPage(string value)
{
if (uint.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
{
Options.CurrentPage = num;
}
}
Comment on lines +47 to +53
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

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

The SetCurrentPage method silently ignores invalid input without providing feedback. Consider adding validation error handling or at least logging when the parse fails, so developers and users understand why page navigation might not work as expected.

Copilot uses AI. Check for mistakes.

/// <summary>
/// <inheritdoc/>
Expand All @@ -53,7 +59,13 @@ protected override void OnParametersSet()
{
base.OnParametersSet();

_docTitle = Path.GetFileName(Url);
Options ??= new PdfReaderOptions();

if (Options.CurrentPage == 0)
{
Options.CurrentPage = 1;
}
_docTitle = Path.GetFileName(Options.Url);
}

/// <summary>
Expand All @@ -67,38 +79,57 @@ protected override async Task OnAfterRenderAsync(bool firstRender)

if (firstRender)
{
_isFitToPage = IsFitToPage;
_isFitToPage = Options.IsFitToPage;
_currentPage = Options.CurrentPage;
_url = Options.Url;
}

if (_isFitToPage != IsFitToPage)
if (_url != Options.Url)
{
_isFitToPage = IsFitToPage;
await TriggerFit(IsFitToPage ? "fitToPage" : "fitToWidth");
_url = Options.Url;
await InvokeInitAsync();
}

if (_isFitToPage != Options.IsFitToPage)
{
_isFitToPage = Options.IsFitToPage;
await TriggerFit(_isFitToPage ? "fitToPage" : "fitToWidth");
}
if (_currentPage != Options.CurrentPage)
{
_currentPage = Options.CurrentPage;
await NavigateToPageAsync(_currentPage);
}
Comment on lines +98 to 102
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

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

There's a potential race condition here. If NavigateToPageAsync is called while the JavaScript pageChanged callback is executing, the page state could become inconsistent. Consider adding a flag to prevent updating _currentPage and Options.CurrentPage in the PageChanged method when the navigation was triggered from C# code rather than user interaction.

Copilot uses AI. Check for mistakes.
}

/// <summary>
/// <inheritdoc/>
/// </summary>
/// <returns></returns>
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, new { Url, IsFitToPage });
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, new
{
Options.Url,
Options.IsFitToPage,
TriggerPagesInit = Options.OnInitAsync != null,
TriggerPageChanged = Options.OnPageChangedAsync != null
});

/// <summary>
/// 跳转到指定页码方法
/// </summary>
/// <param name="pageNumber"></param>
/// <returns></returns>
public Task NavigateToPageAsync(int pageNumber) => InvokeVoidAsync("navigateToPage", Id, pageNumber);
public Task NavigateToPageAsync(uint pageNumber) => InvokeVoidAsync("navigateToPage", Id, pageNumber);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): CurrentPage/initial page support is only applied after changes, so the initial page value on first load is effectively ignored.

Because _currentPage is set from Options.CurrentPage only on first render and no navigation is triggered then, the viewer never actually jumps to a non-default initial page. To honor an initial CurrentPage, either pass it to JS and set pdfViewer.currentPageNumber in pagesinit, or, once initialization completes, call NavigateToPageAsync(Options.CurrentPage) when Options.CurrentPage > 1.

Suggested implementation:

        TriggerPagesInit = Options.OnInitAsync != null,
        TriggerPageChanged = Options.OnPageChangedAsync != null
    });

    // Honor an initial non-default CurrentPage by navigating after initialization completes
    if (Options is not null && Options.CurrentPage > 1)
    {
        await NavigateToPageAsync((uint)Options.CurrentPage);
    }

I assumed this block is inside an async initialization method (likely OnAfterRenderAsync or a similar lifecycle/initialization method) where:

  • "InvokeVoidAsync" initializes the JS PDF viewer, and
  • "Options.CurrentPage" is an int or similar numeric type.

You may need to:

  1. Ensure the enclosing method is async and can await the new NavigateToPageAsync call.
  2. Adjust the cast to uint if Options.CurrentPage is already an unsigned type.
  3. If the initialization can be called multiple times, consider guarding this navigation with a "firstRender" or similar flag so the initial navigation only happens once.
  4. If you prefer the "pass to JS and handle in pagesinit" approach instead, add "InitialPage = Options.CurrentPage" to the object passed to JS here and then update the JS side to set "pdfViewer.currentPageNumber" in the "pagesinit" handler using that value.


/// <summary>
/// 适应页面宽度
/// </summary>
public void FitToPage() => IsFitToPage = true;
public void FitToPage() => Options.IsFitToPage = true;

/// <summary>
/// 适应文档宽度
/// </summary>
public void FitToWidth() => IsFitToPage = false;
public void FitToWidth() => Options.IsFitToPage = false;

/// <summary>
/// 旋转页面方法
Expand All @@ -125,18 +156,27 @@ public async Task RotateRight()
/// </summary>
/// <returns></returns>
[JSInvokable]
public Task PagesInit()
public async Task PagesInit(int pagesCount)
{
return Task.CompletedTask;
if (Options.OnInitAsync != null)
{
await Options.OnInitAsync(pagesCount);
}
}

/// <summary>
/// 改变页码时回调方法
/// </summary>
/// <returns></returns>
[JSInvokable]
public Task PageChanging()
public async Task PageChanged(uint pageIndex)
{
return Task.CompletedTask;
_currentPage = pageIndex;
Options.CurrentPage = pageIndex;

if (Options.OnPageChangedAsync != null)
{
await Options.OnPageChangedAsync(pageIndex);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
@import url('pdf_viewer.css');

.bb-pdf-reader {
--bb-pdf-view-height: 600px;
--bb-pdf-toolbar-height: 60px;
Expand All @@ -16,6 +14,10 @@
color: #fff;
}

.bb-view-toolbar.init > div {
visibility: hidden;
}

.bb-view-title {
display: flex;
align-items: center;
Expand Down
85 changes: 48 additions & 37 deletions src/components/BootstrapBlazor.PdfReader/PdfReader.razor.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,28 @@ if (pdfjsLib != null) {
}

export async function init(id, invoke, options) {
await addLink('./_content/BootstrapBlazor.PdfReader/css/pdf_viewer.css');

const el = document.getElementById(id);
if (el === null) {
return;
}

await addLink('./_content/BootstrapBlazor.PdfReader/css/pdf.css');
if (options.url === null) {
return;
}

const loadingTask = pdfjsLib.getDocument(options);
loadingTask.onProgress = function (progressData) {
console.log(progressData.loaded, progressData.total);
};

// handle password only when required (optional password support)
loadingTask.onPassword = function (updatePassword, reason) {
if (reason === pdfjsLib.PasswordResponses.NEED_PASSWORD) {
// only prompt if PDF actually requires password
const password = prompt("This PDF is password protected. Enter password:");
updatePassword(password);
} else if (reason === pdfjsLib.PasswordResponses.INCORRECT_PASSWORD) {
}
else if (reason === pdfjsLib.PasswordResponses.INCORRECT_PASSWORD) {
const password = prompt("Incorrect password. Please try again:");
updatePassword(password);
}
Expand All @@ -39,43 +42,11 @@ export async function init(id, invoke, options) {
eventBus
});

addEventListener(pdfViewer, eventBus, invoke, options);

eventBus.on("pagesinit", function () {
if (options.isFitToPage) {
pdfViewer.currentScaleValue = "page-width";
}
else {
pdfViewer.currentScaleValue = 1.0;
}
});

// handle the promise
const pdfDocument = await loadingTask.promise;
pdfViewer.setDocument(pdfDocument);

// pdfDocument.then(function (doc) {
// pdf.pdfDoc = doc;
// pdf.pagesCount = doc.numPages;
// renderPage(pdf, pdf.pageNum);

// // notify .NET side that document is loaded
// invoke.invokeMethodAsync('DocumentLoaded', {
// pagesCount: pdf.pagesCount,
// pageNumber: pdf.pageNum
// });
// })
// .catch(function (error) {
// console.error("PDF loading error:", error);

// // handle password exceptions specifically
// if (error.name === "PasswordException") {
// console.error("Password required but not provided");
// }

// // notify .NET side that document loading failed
// invoke.invokeMethodAsync('DocumentLoadError', error.message);
// });

Data.set(id, pdfViewer);
}

Expand Down Expand Up @@ -106,6 +77,46 @@ export function dispose(id) {
Data.get(id);
}

const addEventListener = (pdfViewer, eventBus, invoke, options) => {
eventBus.on("pagesinit", async () => {
if (options.isFitToPage) {
pdfViewer.currentScaleValue = "page-width";
}
else {
pdfViewer.currentScaleValue = 1.0;
}

const el = pdfViewer.container.parentElement;
const numPages = pdfViewer.pagesCount;
const countEl = el.querySelector(".bb-view-pagesCount");
if (countEl) {
countEl.innerHTML = numPages;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security (javascript.browser.security.insecure-document-method): User controlled data in methods like innerHTML, outerHTML or document.write is an anti-pattern that can lead to XSS vulnerabilities

Source: opengrep

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security (javascript.browser.security.insecure-innerhtml): User controlled data in a countEl.innerHTML is an anti-pattern that can lead to XSS vulnerabilities

Source: opengrep

}

const toolbarEl = el.querySelector(".bb-view-toolbar");
if (toolbarEl) {
toolbarEl.classList.remove("init");
}

if (options.triggerPagesInit === true) {
await invoke.invokeMethodAsync("pagesInit", numPages);
}
});

eventBus.on("pagechanging", async evt => {
const page = evt.pageNumber;
const el = evt.source.container.parentElement;
const pageNumberEl = el.querySelector(".bb-view-num");
if (pageNumberEl) {
pageNumberEl.value = page;
}

if (options.triggerPageChanged === true) {
await invoke.invokeMethodAsync("pageChanged", page);
}
}, true);
}

function getCanvas(item) {
if (isDomSupported() && typeof item === 'string') {
item = document.getElementById(item);
Expand Down
Loading
Loading