Skip to content

Commit a2e8c02

Browse files
authored
feat(PdfReader): add thumbnails function (#723)
* feat: 增加缩略图功能 * refactor: 增加缩略图功能 * feat: 支持缩略图响应页码变化 * doc: 格式化文档 * refactor: 修复缩略图排序问题 * refactor: 增加响应式 * fix: 修复比例禁用逻辑 * chore: bump version 10.0.1-beta05
1 parent 492184f commit a2e8c02

6 files changed

Lines changed: 198 additions & 38 deletions

File tree

src/components/BootstrapBlazor.PdfReader/BootstrapBlazor.PdfReader.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk.Razor">
22

33
<PropertyGroup>
4-
<Version>10.0.1-beta04</Version>
4+
<Version>10.0.1-beta05</Version>
55
</PropertyGroup>
66

77
<PropertyGroup>

src/components/BootstrapBlazor.PdfReader/PdfReader.razor

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<div class="bb-view-toolbar init">
99
<div class="bb-view-title">
1010
<div class="bb-view-icon bb-view-bar"><i class="fa-solid fa-bars"></i></div>
11-
<span class="bb-view-subject">@_docTitle</span>
11+
<span class="bb-view-subject d-none d-sm-block">@_docTitle</span>
1212
</div>
1313
<div class="@ViewBodyString">
1414
<input type="text" class="bb-view-num" @bind="CurrentPageString" /><span class="bb-view-slash">/</span><div class="bb-view-pagesCount"></div>
@@ -39,7 +39,15 @@
3939
</div>
4040
</div>
4141
}
42-
<div class="bb-view-container">
43-
<div class="pdfViewer"></div>
42+
<div class="bb-view-main">
43+
@if (Options.EnableThumbnails)
44+
{
45+
<div class="bb-view-thumbnails"></div>
46+
}
47+
<div class="bb-view-content">
48+
<div class="bb-view-container">
49+
<div class="pdfViewer"></div>
50+
</div>
51+
</div>
4452
</div>
4553
</div>

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

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
namespace BootstrapBlazor.Components;
99

1010
/// <summary>
11-
/// Blazor Pdf Reader PDF 阅读器 组件
11+
/// Blazor Pdf Reader PDF 阅读器 组件
1212
/// </summary>
1313
[JSModuleAutoLoader("./_content/BootstrapBlazor.PdfReader/PdfReader.razor.js", JSObjectReference = true)]
1414
public partial class PdfReader
@@ -77,14 +77,12 @@ private void SetCurrentScale(string value)
7777
}
7878
else if (float.TryParse(value.TrimEnd("%"), out var v))
7979
{
80-
if (v > 500)
80+
v = v switch
8181
{
82-
v = 500;
83-
}
84-
else if (v < 25)
85-
{
86-
v = 25;
87-
}
82+
> 500 => 500,
83+
< 25 => 25,
84+
_ => v
85+
};
8886

8987
Options.CurrentScale = v.ToString(CultureInfo.InvariantCulture);
9088
}
@@ -171,7 +169,9 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
171169
{
172170
Options.Url,
173171
Options.IsFitToPage,
174-
TriggerPagesInit = Options.OnInitAsync != null,
172+
Options.EnableThumbnails,
173+
TriggerPagesInit = Options.OnPagesInitAsync != null,
174+
TriggerPagesLoaded = Options.OnPagesLoadedAsync != null,
175175
TriggerPageChanged = Options.OnPageChangedAsync != null,
176176
TriggerTowPagesOnViewChanged = Options.OnTwoPagesOneViewAsync != null
177177
});
@@ -220,9 +220,22 @@ public async Task RotateRight()
220220
[JSInvokable]
221221
public async Task PagesInit(int pagesCount)
222222
{
223-
if (Options.OnInitAsync != null)
223+
if (Options.OnPagesInitAsync != null)
224+
{
225+
await Options.OnPagesInitAsync(pagesCount);
226+
}
227+
}
228+
229+
/// <summary>
230+
/// 页面加载完毕时回调方法
231+
/// </summary>
232+
/// <returns></returns>
233+
[JSInvokable]
234+
public async Task PagesLoaded(int pagesCount)
235+
{
236+
if (Options.OnPagesLoadedAsync != null)
224237
{
225-
await Options.OnInitAsync(pagesCount);
238+
await Options.OnPagesLoadedAsync(pagesCount);
226239
}
227240
}
228241

src/components/BootstrapBlazor.PdfReader/PdfReader.razor.css

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,6 @@
4343
color: #6c757d;
4444
}
4545

46-
.bb-view-bar {
47-
margin-inline-end: 2rem;
48-
}
49-
5046
.bb-view-subject {
5147
white-space: nowrap;
5248
display: block;
@@ -60,8 +56,10 @@
6056
min-width: 0;
6157
width: 1%;
6258
display: flex;
59+
flex-wrap: nowrap;
6360
align-items: center;
6461
justify-content: center;
62+
overflow: hidden;
6563
}
6664

6765
.bb-view-body.fit-page .bb-view-fit-page {
@@ -106,10 +104,56 @@
106104
padding: 0 1rem;
107105
}
108106

107+
.bb-view-main {
108+
display: flex;
109+
width: 100%;
110+
height: var(--bb-pdf-view-height);
111+
overflow: hidden;
112+
}
113+
114+
.bb-view-thumbnails {
115+
flex-basis: 0;
116+
overflow: auto;
117+
background-color: #28292a;
118+
padding: 1rem 0;
119+
transition: flex-basis .3s linear;
120+
}
121+
122+
.bb-view-thumbnails.show {
123+
flex-basis: 300px;
124+
}
125+
126+
::deep .bb-view-thumbnail-item {
127+
display: block;
128+
text-align: center;
129+
}
130+
131+
::deep .bb-view-thumbnail-item:not(:last-child) {
132+
margin-block-end: 1rem;
133+
}
134+
135+
::deep .bb-view-thumbnail-item.active img {
136+
border: 2px solid #0d6efd;
137+
}
138+
139+
::deep .bb-view-thumbnail-item img {
140+
width: 128px;
141+
padding: 1px;
142+
cursor: pointer;
143+
border: 2px solid #28292a;
144+
}
145+
146+
.bb-view-content {
147+
position: relative;
148+
flex: 1 1 auto;
149+
min-width: 0;
150+
width: 1%;
151+
height: var(--bb-pdf-view-height);
152+
}
153+
109154
.bb-view-container {
110155
overflow: auto;
111156
position: absolute;
112157
background-color: #000;
113-
width: 100%;
114-
height: var(--bb-pdf-view-height);
158+
inset: 0;
115159
}

src/components/BootstrapBlazor.PdfReader/PdfReader.razor.js

Lines changed: 101 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -82,23 +82,9 @@ export function navigateToPage(id, pageNumber) {
8282
}
8383

8484
export function scale(id, scale) {
85-
const { el, pdfViewer } = Data.get(id);
85+
const { pdfViewer } = Data.get(id);
8686
if (pdfViewer) {
8787
pdfViewer.currentScaleValue = scale / 100;
88-
89-
const minus = el.querySelector(".bb-page-minus");
90-
const plus = el.querySelector(".bb-page-plus");
91-
92-
if (scale === "25") {
93-
minus.classList.add("disabled");
94-
}
95-
else if (scale === "500") {
96-
plus.classList.add("disabled");
97-
}
98-
else {
99-
minus.classList.remove("disabled");
100-
plus.classList.remove("disabled");
101-
}
10288
}
10389
}
10490

@@ -139,13 +125,63 @@ const addEventListener = (el, pdfViewer, eventBus, invoke, options) => {
139125
}
140126
});
141127

128+
eventBus.on("pagesloaded", async e => {
129+
if (options.enableThumbnails) {
130+
const thumbnailsContainer = el.querySelector(".bb-view-thumbnails");
131+
pdfViewer.getPagesOverview().map(async (p, i) => {
132+
const item = document.createElement("div");
133+
item.classList.add("bb-view-thumbnail-item");
134+
if (pdfViewer.currentPageNumber === i + 1) {
135+
item.classList.add("active");
136+
}
137+
item.setAttribute("data-bb-page", `${i + 1}`);
138+
thumbnailsContainer.appendChild(item);
139+
140+
const page = await pdfViewer.pdfDocument.getPage(i + 1);
141+
const canvas = await makeThumb(page);
142+
const img = document.createElement("img");
143+
img.src = canvas.toDataURL();
144+
item.appendChild(img);
145+
});
146+
147+
EventHandler.on(thumbnailsContainer, "click", ".bb-view-thumbnail-item", e => {
148+
const active = thumbnailsContainer.querySelector('.active');
149+
if (active) {
150+
active.classList.remove('active');
151+
}
152+
153+
const item = e.delegateTarget;
154+
item.classList.add("active");
155+
156+
const index = parseInt(item.getAttribute("data-bb-page")) || 1;
157+
pdfViewer.currentPageNumber = index;
158+
})
159+
}
160+
161+
if (options.triggerPagesLoaded === true) {
162+
await invoke.invokeMethodAsync("PagesLoaded", e.pagesCount);
163+
}
164+
})
165+
142166
eventBus.on("pagechanging", async evt => {
143167
const page = evt.pageNumber;
144168
const pageNumberEl = el.querySelector(".bb-view-num");
145169
if (pageNumberEl) {
146170
pageNumberEl.value = page;
147171
}
148172

173+
if (options.enableThumbnails) {
174+
const thumbnailsContainer = el.querySelector(".bb-view-thumbnails");
175+
if (thumbnailsContainer) {
176+
const active = thumbnailsContainer.querySelector('.active');
177+
active.classList.remove('active');
178+
179+
const item = thumbnailsContainer.querySelector(`[data-bb-page='${page}']`);
180+
item.classList.add("active");
181+
item.scrollIntoView({ behavior: 'smooth', block: "nearest", inline: "start" });
182+
}
183+
}
184+
149185
if (options.triggerPageChanged === true) {
150186
await invoke.invokeMethodAsync("pageChanged", page);
151187
}
@@ -156,7 +192,22 @@ const addEventListener = (el, pdfViewer, eventBus, invoke, options) => {
156192
const scaleEl = el.querySelector(".bb-view-scale");
157193

158194
eventBus.on("scalechanging", evt => {
159-
scaleEl.value = `${Math.round(evt.scale * 100, 0)}%`;
195+
const scale = evt.scale * 100;
196+
scaleEl.value = `${Math.round(scale, 0)}%`;
197+
198+
const minus = el.querySelector(".bb-page-minus");
199+
const plus = el.querySelector(".bb-page-plus");
200+
201+
if (scale === 25) {
202+
minus.classList.add("disabled");
203+
}
204+
else if (scale === 500) {
205+
plus.classList.add("disabled");
206+
}
207+
else {
208+
minus.classList.remove("disabled");
209+
plus.classList.remove("disabled");
210+
}
160211
})
161212

162213
EventHandler.on(minus, "click", e => updateScale(pdfViewer, e.target, -1));
@@ -173,6 +224,14 @@ const addEventListener = (el, pdfViewer, eventBus, invoke, options) => {
173224
}
174225
});
175226
}
227+
228+
const thumbnailsToggle = el.querySelector(".bb-view-bar");
229+
if (thumbnailsToggle) {
230+
EventHandler.on(thumbnailsToggle, "click", e => {
231+
const thumbnailsEl = el.querySelector(".bb-view-thumbnails");
232+
thumbnailsEl.classList.toggle("show");
233+
});
234+
}
176235
}
177236

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

256+
const makeThumb = async page => {
257+
const outputScale = window.devicePixelRatio || 1;
258+
const vp = page.getViewport({ scale: 1 });
259+
const canvas = document.createElement("canvas");
260+
const scaleSize = 1;
261+
canvas.width = vp.width * scaleSize * outputScale;
262+
canvas.height = vp.height * scaleSize * outputScale;
263+
264+
await page.render({
265+
canvasContext: canvas.getContext("2d"),
266+
viewport: page.getViewport({ scale: scaleSize * outputScale })
267+
}).promise;
268+
269+
return canvas;
270+
}
271+
197272
export function dispose(id) {
198273
Data.remove(id);
199274

@@ -212,5 +287,15 @@ export function dispose(id) {
212287
if (towPagesOneView) {
213288
EventHandler.off(towPagesOneView, "click");
214289
}
290+
291+
const thumbnailsToggle = el.querySelector(".bb-view-bar");
292+
if (thumbnailsToggle) {
293+
EventHandler.off(thumbnailsToggle, "click");
294+
}
295+
296+
const thumbnailsContainer = el.querySelector(".bb-view-thumbnails");
297+
if (thumbnailsContainer) {
298+
EventHandler.off(thumbnailsContainer, "click");
299+
}
215300
}
216301
}

src/components/BootstrapBlazor.PdfReader/PdfReaderOptions.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ public class PdfReaderOptions
1414
/// </summary>
1515
public bool ShowToolbar { get; set; } = true;
1616

17+
/// <summary>
18+
/// 获得/设置 是否显示缩略图 默认 true 显示
19+
/// </summary>
20+
public bool EnableThumbnails { get; set; } = true;
21+
1722
/// <summary>
1823
/// 获得/设置 PDF 文档路径
1924
/// </summary>
@@ -52,7 +57,12 @@ public class PdfReaderOptions
5257
/// <summary>
5358
/// 页面初始化回调方法
5459
/// </summary>
55-
public Func<int, Task>? OnInitAsync { get; set; }
60+
public Func<int, Task>? OnPagesInitAsync { get; set; }
61+
62+
/// <summary>
63+
/// 页面加载完毕回调方法
64+
/// </summary>
65+
public Func<int, Task>? OnPagesLoadedAsync { get; set; }
5666

5767
/// <summary>
5868
/// 页面初始化回调方法

0 commit comments

Comments
 (0)