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.1-beta04</Version>
<Version>10.0.1-beta05</Version>
</PropertyGroup>

<PropertyGroup>
Expand Down
14 changes: 11 additions & 3 deletions src/components/BootstrapBlazor.PdfReader/PdfReader.razor
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<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>
<span class="bb-view-subject d-none d-sm-block">@_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>
Expand Down Expand Up @@ -39,7 +39,15 @@
</div>
</div>
}
<div class="bb-view-container">
<div class="pdfViewer"></div>
<div class="bb-view-main">
@if (Options.EnableThumbnails)
{
<div class="bb-view-thumbnails"></div>
}
<div class="bb-view-content">
<div class="bb-view-container">
<div class="pdfViewer"></div>
</div>
</div>
</div>
</div>
35 changes: 24 additions & 11 deletions src/components/BootstrapBlazor.PdfReader/PdfReader.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
namespace BootstrapBlazor.Components;

/// <summary>
/// Blazor Pdf Reader PDF 阅读器 组件
/// Blazor Pdf Reader PDF 阅读器 组件
/// </summary>
[JSModuleAutoLoader("./_content/BootstrapBlazor.PdfReader/PdfReader.razor.js", JSObjectReference = true)]
public partial class PdfReader
Expand Down Expand Up @@ -77,14 +77,12 @@ private void SetCurrentScale(string value)
}
else if (float.TryParse(value.TrimEnd("%"), out var v))
{
if (v > 500)
v = v switch
{
v = 500;
}
else if (v < 25)
{
v = 25;
}
> 500 => 500,
< 25 => 25,
_ => v
};

Options.CurrentScale = v.ToString(CultureInfo.InvariantCulture);
}
Expand Down Expand Up @@ -171,7 +169,9 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
{
Options.Url,
Options.IsFitToPage,
TriggerPagesInit = Options.OnInitAsync != null,
Options.EnableThumbnails,
TriggerPagesInit = Options.OnPagesInitAsync != null,
TriggerPagesLoaded = Options.OnPagesLoadedAsync != null,
TriggerPageChanged = Options.OnPageChangedAsync != null,
TriggerTowPagesOnViewChanged = Options.OnTwoPagesOneViewAsync != null
});
Expand Down Expand Up @@ -220,9 +220,22 @@ public async Task RotateRight()
[JSInvokable]
public async Task PagesInit(int pagesCount)
{
if (Options.OnInitAsync != null)
if (Options.OnPagesInitAsync != null)
{
await Options.OnPagesInitAsync(pagesCount);
}
}

/// <summary>
/// 页面加载完毕时回调方法
/// </summary>
/// <returns></returns>
[JSInvokable]
public async Task PagesLoaded(int pagesCount)
{
if (Options.OnPagesLoadedAsync != null)
{
await Options.OnInitAsync(pagesCount);
await Options.OnPagesLoadedAsync(pagesCount);
}
}

Expand Down
56 changes: 50 additions & 6 deletions src/components/BootstrapBlazor.PdfReader/PdfReader.razor.css
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,6 @@
color: #6c757d;
}

.bb-view-bar {
margin-inline-end: 2rem;
}

.bb-view-subject {
white-space: nowrap;
display: block;
Expand All @@ -60,8 +56,10 @@
min-width: 0;
width: 1%;
display: flex;
flex-wrap: nowrap;
align-items: center;
justify-content: center;
overflow: hidden;
}

.bb-view-body.fit-page .bb-view-fit-page {
Expand Down Expand Up @@ -106,10 +104,56 @@
padding: 0 1rem;
}

.bb-view-main {
display: flex;
width: 100%;
height: var(--bb-pdf-view-height);
overflow: hidden;
}

.bb-view-thumbnails {
flex-basis: 0;
overflow: auto;
background-color: #28292a;
padding: 1rem 0;
transition: flex-basis .3s linear;
}

.bb-view-thumbnails.show {
flex-basis: 300px;
}

::deep .bb-view-thumbnail-item {
display: block;
text-align: center;
}

::deep .bb-view-thumbnail-item:not(:last-child) {
margin-block-end: 1rem;
}

::deep .bb-view-thumbnail-item.active img {
border: 2px solid #0d6efd;
}

::deep .bb-view-thumbnail-item img {
width: 128px;
padding: 1px;
cursor: pointer;
border: 2px solid #28292a;
}

.bb-view-content {
position: relative;
flex: 1 1 auto;
min-width: 0;
width: 1%;
height: var(--bb-pdf-view-height);
}

.bb-view-container {
overflow: auto;
position: absolute;
background-color: #000;
width: 100%;
height: var(--bb-pdf-view-height);
inset: 0;
}
117 changes: 101 additions & 16 deletions src/components/BootstrapBlazor.PdfReader/PdfReader.razor.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,23 +82,9 @@ export function navigateToPage(id, pageNumber) {
}

export function scale(id, scale) {
const { el, pdfViewer } = Data.get(id);
const { pdfViewer } = Data.get(id);
if (pdfViewer) {
pdfViewer.currentScaleValue = scale / 100;

const minus = el.querySelector(".bb-page-minus");
const plus = el.querySelector(".bb-page-plus");

if (scale === "25") {
minus.classList.add("disabled");
}
else if (scale === "500") {
plus.classList.add("disabled");
}
else {
minus.classList.remove("disabled");
plus.classList.remove("disabled");
}
}
}

Expand Down Expand Up @@ -139,13 +125,63 @@ const addEventListener = (el, pdfViewer, eventBus, invoke, options) => {
}
});

eventBus.on("pagesloaded", async e => {
if (options.enableThumbnails) {
const thumbnailsContainer = el.querySelector(".bb-view-thumbnails");
pdfViewer.getPagesOverview().map(async (p, i) => {
const item = document.createElement("div");
item.classList.add("bb-view-thumbnail-item");
if (pdfViewer.currentPageNumber === i + 1) {
item.classList.add("active");
}
item.setAttribute("data-bb-page", `${i + 1}`);
thumbnailsContainer.appendChild(item);

const page = await pdfViewer.pdfDocument.getPage(i + 1);
const canvas = await makeThumb(page);
const img = document.createElement("img");
img.src = canvas.toDataURL();
item.appendChild(img);
});
Comment on lines +131 to +145
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

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

Performance issue: Using .map() with async callbacks doesn't wait for the promises to complete. This can cause race conditions where thumbnails are added to the DOM out of order or the event handler is registered before all thumbnails are rendered. Use await Promise.all(pdfViewer.getPagesOverview().map(async (p, i) => { ... })) or a for...of loop to ensure proper sequencing.

Copilot uses AI. Check for mistakes.

EventHandler.on(thumbnailsContainer, "click", ".bb-view-thumbnail-item", e => {
const active = thumbnailsContainer.querySelector('.active');
if (active) {
active.classList.remove('active');
}

const item = e.delegateTarget;
item.classList.add("active");

const index = parseInt(item.getAttribute("data-bb-page")) || 1;
pdfViewer.currentPageNumber = index;
})
Comment on lines +131 to +158
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

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

Missing null check: thumbnailsContainer could be null if the element doesn't exist in the DOM. Add a null check before attempting to use it: if (thumbnailsContainer) { ... } to prevent potential runtime errors.

Suggested change
pdfViewer.getPagesOverview().map(async (p, i) => {
const item = document.createElement("div");
item.classList.add("bb-view-thumbnail-item");
if (pdfViewer.currentPageNumber === i + 1) {
item.classList.add("active");
}
item.setAttribute("data-bb-page", `${i + 1}`);
thumbnailsContainer.appendChild(item);
const page = await pdfViewer.pdfDocument.getPage(i + 1);
const canvas = await makeThumb(page);
const img = document.createElement("img");
img.src = canvas.toDataURL();
item.appendChild(img);
});
EventHandler.on(thumbnailsContainer, "click", ".bb-view-thumbnail-item", e => {
const active = thumbnailsContainer.querySelector('.active');
if (active) {
active.classList.remove('active');
}
const item = e.delegateTarget;
item.classList.add("active");
const index = parseInt(item.getAttribute("data-bb-page")) || 1;
pdfViewer.currentPageNumber = index;
})
if (thumbnailsContainer) {
pdfViewer.getPagesOverview().map(async (p, i) => {
const item = document.createElement("div");
item.classList.add("bb-view-thumbnail-item");
if (pdfViewer.currentPageNumber === i + 1) {
item.classList.add("active");
}
item.setAttribute("data-bb-page", `${i + 1}`);
thumbnailsContainer.appendChild(item);
const page = await pdfViewer.pdfDocument.getPage(i + 1);
const canvas = await makeThumb(page);
const img = document.createElement("img");
img.src = canvas.toDataURL();
item.appendChild(img);
});
EventHandler.on(thumbnailsContainer, "click", ".bb-view-thumbnail-item", e => {
const active = thumbnailsContainer.querySelector('.active');
if (active) {
active.classList.remove('active');
}
const item = e.delegateTarget;
item.classList.add("active");
const index = parseInt(item.getAttribute("data-bb-page")) || 1;
pdfViewer.currentPageNumber = index;
})
}

Copilot uses AI. Check for mistakes.
}

if (options.triggerPagesLoaded === true) {
await invoke.invokeMethodAsync("PagesLoaded", e.pagesCount);
}
})

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

if (options.enableThumbnails) {
const thumbnailsContainer = el.querySelector(".bb-view-thumbnails");
if (thumbnailsContainer) {
const active = thumbnailsContainer.querySelector('.active');
active.classList.remove('active');
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

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

Potential null reference error: The code assumes active element exists without null checking before calling classList.remove(). If no active element is found (e.g., on first page change or if thumbnails were dynamically removed), this will throw an error. Add a null check: if (active) { active.classList.remove('active'); }

Suggested change
active.classList.remove('active');
if (active) {
active.classList.remove('active');
}

Copilot uses AI. Check for mistakes.

const item = thumbnailsContainer.querySelector(`[data-bb-page='${page}']`);
item.classList.add("active");
item.scrollIntoView({ behavior: 'smooth', block: "nearest", inline: "start" });
Comment on lines +180 to +181
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

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

Potential null reference error: The code assumes the thumbnail item element exists without null checking before calling classList.add() and scrollIntoView(). If the thumbnail for the page doesn't exist yet (e.g., still loading), this will throw an error. Add a null check: if (item) { item.classList.add("active"); item.scrollIntoView(...); }

Suggested change
item.classList.add("active");
item.scrollIntoView({ behavior: 'smooth', block: "nearest", inline: "start" });
if (item) {
item.classList.add("active");
item.scrollIntoView({ behavior: 'smooth', block: "nearest", inline: "start" });
}

Copilot uses AI. Check for mistakes.
}
}

if (options.triggerPageChanged === true) {
await invoke.invokeMethodAsync("pageChanged", page);
}
Expand All @@ -156,7 +192,22 @@ const addEventListener = (el, pdfViewer, eventBus, invoke, options) => {
const scaleEl = el.querySelector(".bb-view-scale");

eventBus.on("scalechanging", evt => {
scaleEl.value = `${Math.round(evt.scale * 100, 0)}%`;
const scale = evt.scale * 100;
scaleEl.value = `${Math.round(scale, 0)}%`;

const minus = el.querySelector(".bb-page-minus");
const plus = el.querySelector(".bb-page-plus");

if (scale === 25) {
minus.classList.add("disabled");
}
else if (scale === 500) {
Comment on lines +201 to +204
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

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

Floating-point comparison issue: Comparing scale === 25 and scale === 500 with exact equality is unreliable for floating-point numbers. Due to precision issues with multiplication (e.g., evt.scale * 100), the comparison may fail to match even when the value is logically 25 or 500. Use threshold checks instead: if (scale <= 25) and if (scale >= 500) to ensure the buttons are properly disabled at the boundaries.

Suggested change
if (scale === 25) {
minus.classList.add("disabled");
}
else if (scale === 500) {
if (scale <= 25) {
minus.classList.add("disabled");
}
else if (scale >= 500) {

Copilot uses AI. Check for mistakes.
plus.classList.add("disabled");
}
else {
minus.classList.remove("disabled");
plus.classList.remove("disabled");
}
})

EventHandler.on(minus, "click", e => updateScale(pdfViewer, e.target, -1));
Expand All @@ -173,6 +224,14 @@ const addEventListener = (el, pdfViewer, eventBus, invoke, options) => {
}
});
}

const thumbnailsToggle = el.querySelector(".bb-view-bar");
if (thumbnailsToggle) {
EventHandler.on(thumbnailsToggle, "click", e => {
const thumbnailsEl = el.querySelector(".bb-view-thumbnails");
Comment on lines +228 to +231
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): Toggling thumbnails will throw when thumbnails are disabled.

The click handler is always attached to .bb-view-bar, but .bb-view-thumbnails only exists when Options.EnableThumbnails is true. When thumbnails are disabled, thumbnailsEl will be null and thumbnailsEl.classList.toggle("show") will throw. Add a null check (or avoid registering the handler when thumbnails are disabled).

thumbnailsEl.classList.toggle("show");
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

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

Potential null reference error: The code assumes thumbnailsEl exists without null checking before calling classList.toggle(). If EnableThumbnails is false or the element doesn't exist for any reason, this will throw an error. Add a null check: if (thumbnailsEl) { thumbnailsEl.classList.toggle("show"); }

Suggested change
thumbnailsEl.classList.toggle("show");
if (thumbnailsEl) {
thumbnailsEl.classList.toggle("show");
}

Copilot uses AI. Check for mistakes.
});
}
}

const updateScale = (pdfViewer, button, rate) => {
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 (performance): Thumbnail rendering may be overkill in resolution relative to displayed size.

Here you render the thumbnail canvas at devicePixelRatio * viewport with scaleSize = 1, but CSS constrains the thumbnail to width: 128px. On large PDFs and high-DPI displays this can cause unnecessary memory and CPU use. Please consider basing the render scale on the intended thumbnail width (e.g., 128px * devicePixelRatio) or reducing scaleSize accordingly to keep quality acceptable while lowering rendering cost.

Suggested change
const updateScale = (pdfViewer, button, rate) => {
const updateScale = (pdfViewer, button, rate) => {
pdfViewer.currentScaleValue = v / 100;
}
const makeThumb = async page => {
const outputScale = window.devicePixelRatio || 1;
const THUMBNAIL_CSS_WIDTH = 128;
// First get an unscaled viewport to inspect the page width in CSS pixels.
const baseViewport = page.getViewport({ scale: 1 });
// Compute a scale so that the rendered thumbnail is ~THUMBNAIL_CSS_WIDTH wide in CSS pixels,
// then multiply by devicePixelRatio so the backing buffer is high-DPI without being excessive.
const targetScale = Math.min(
1,
(THUMBNAIL_CSS_WIDTH * outputScale) / baseViewport.width
);
const viewport = page.getViewport({ scale: targetScale });
const canvas = document.createElement("canvas");
canvas.width = viewport.width;
canvas.height = viewport.height;
await page.render({
canvasContext: canvas.getContext("2d"),
viewport
}).promise;
return canvas;
}

Expand All @@ -194,6 +253,22 @@ const updateScale = (pdfViewer, button, rate) => {
pdfViewer.currentScaleValue = v / 100;
}

const makeThumb = async page => {
const outputScale = window.devicePixelRatio || 1;
const vp = page.getViewport({ scale: 1 });
const canvas = document.createElement("canvas");
const scaleSize = 1;
canvas.width = vp.width * scaleSize * outputScale;
canvas.height = vp.height * scaleSize * outputScale;

await page.render({
canvasContext: canvas.getContext("2d"),
viewport: page.getViewport({ scale: scaleSize * outputScale })
}).promise;

return canvas;
}

export function dispose(id) {
Data.remove(id);

Expand All @@ -212,5 +287,15 @@ export function dispose(id) {
if (towPagesOneView) {
EventHandler.off(towPagesOneView, "click");
}

const thumbnailsToggle = el.querySelector(".bb-view-bar");
if (thumbnailsToggle) {
EventHandler.off(thumbnailsToggle, "click");
}

const thumbnailsContainer = el.querySelector(".bb-view-thumbnails");
if (thumbnailsContainer) {
EventHandler.off(thumbnailsContainer, "click");
}
}
}
12 changes: 11 additions & 1 deletion src/components/BootstrapBlazor.PdfReader/PdfReaderOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ public class PdfReaderOptions
/// </summary>
public bool ShowToolbar { get; set; } = true;

/// <summary>
/// 获得/设置 是否显示缩略图 默认 true 显示
/// </summary>
public bool EnableThumbnails { get; set; } = true;

/// <summary>
/// 获得/设置 PDF 文档路径
/// </summary>
Expand Down Expand Up @@ -52,7 +57,12 @@ public class PdfReaderOptions
/// <summary>
/// 页面初始化回调方法
/// </summary>
public Func<int, Task>? OnInitAsync { get; set; }
public Func<int, Task>? OnPagesInitAsync { get; set; }
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

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

Breaking change: Renaming OnInitAsync to OnPagesInitAsync will break existing code that uses this callback. Consider deprecating the old property rather than removing it immediately, or document this as a breaking change in the release notes.

Copilot uses AI. Check for mistakes.

/// <summary>
/// 页面加载完毕回调方法
/// </summary>
public Func<int, Task>? OnPagesLoadedAsync { get; set; }

/// <summary>
/// 页面初始化回调方法
Expand Down
Loading