Skip to content

Commit 109c6cf

Browse files
authored
PdfViewer: Password support (#68)
* PDF Viewer - Password support #67
1 parent ec74e8b commit 109c6cf

9 files changed

Lines changed: 146 additions & 12 deletions

File tree

BlazorExpress.Bulma.Demo.RCL/Constants/DemoStringConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public static class DemoStringConstants
1010
public const string PageTitle_Suffix = " | Blazor Bulma: An enterprise-grade open-source component library from the Blazor Express team.";
1111

1212
public const string StaticAssets_RootPath = "_content/BlazorExpress.Bulma.Demo.RCL";
13+
public const string StaticAssets_Docs_Path = StaticAssets_RootPath + "/docs";
1314
public const string StaticAssets_Icons_Path = StaticAssets_RootPath + "/icons";
1415
public const string StaticAssets_Icons_Logo_png = StaticAssets_Icons_Path + "/logo.png";
1516

BlazorExpress.Bulma.Demo.RCL/Pages/Demos/PdfViewer/PdfViewerDocumentation.razor

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,42 @@
6262
<Demo Type="typeof(PdfViewer_Demo_03_Base64String)" Tabs="true" />
6363
</Section>
6464

65+
<Section Class="p-0" Size="HeadingSize.H3" Name="Password Protected PDFs (Fixed Password)" PageUrl="@pageUrl" Link="password-protected-pdfs-fixed-password">
66+
<Block>
67+
The <strong>PdfViewer</strong> component supports opening password-protected PDF files by supplying a fixed password via the <code>Password</code> parameter.
68+
<br /><br />
69+
<strong>How to use:</strong>
70+
<div class="content mb-2">
71+
<ol>
72+
<li>Add the <code>PdfViewer</code> component to your page.</li>
73+
<li>Set the <code>Url</code> parameter to the protected PDF file (static URL or data URI).</li>
74+
<li>Provide the document password using the <code>Password</code> parameter (example: <code>Password="12345"</code>).</li>
75+
<li>Optionally handle <code>OnDocumentLoaded</code> and <code>OnPageChanged</code> to react after the document opens or when the user navigates pages.</li>
76+
</ol>
77+
</div>
78+
The demo loads <code>pdf_password_protected.pdf</code> from static assets and demonstrates a fixed-password scenario: <code>Password="12345"</code>. Security note: do not hard-code production secrets into client-side WebAssembly. If the PDF password must remain secret, validate or supply it server-side or use a user prompt instead (see the "Prompt for Password" demo).
79+
</Block>
80+
<Demo Type="typeof(PdfViewer_Demo_04_Password_Protected_A)" Tabs="true" />
81+
</Section>
82+
83+
<Section Class="p-0" Size="HeadingSize.H3" Name="password Protected PDFs (Prompt for Password)" PageUrl="@pageUrl" Link="password-protected-pdfs-prompt-for-password">
84+
<Block>
85+
The <strong>PdfViewer</strong> component can also be used in scenarios where you want the user to provide the password at runtime. Instead of setting a fixed <code>Password</code>, the component will prompt for credentials when a protected document is loaded (or you can implement your own prompt and pass the value to <code>Password</code>).
86+
<br /><br />
87+
<strong>How to use:</strong>
88+
<div class="content mb-2">
89+
<ol>
90+
<li>Add the <code>PdfViewer</code> component to your page without setting the <code>Password</code> parameter.</li>
91+
<li>Set the <code>Url</code> parameter to the protected PDF file.</li>
92+
<li>Optionally delay or control loading (for example, show a <code>Button</code> that creates the viewer on demand) so the prompt appears only when appropriate.</li>
93+
<li>Optionally implement a custom UI to collect a password and supply it to the component via the <code>Password</code> parameter if you need a tailored UX.</li>
94+
</ol>
95+
</div>
96+
The demo demonstrates a lazy-load approach: a button toggles <code>showPdfViewer</code> and instantiates the viewer which then prompts for the password. This is useful when you want to avoid showing the prompt until the user explicitly requests the document.
97+
</Block>
98+
<Demo Type="typeof(PdfViewer_Demo_05_Password_Protected_B_Prompt_For_Password)" Tabs="true" />
99+
</Section>
100+
65101
@code {
66102
private const string componentName = nameof(PdfViewer);
67103
private const string pageUrl = DemoRouteConstants.Demos_PdfViewer_Documentation;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<PdfViewer Class="mb-3"
2+
Url="@($"/{DemoStringConstants.StaticAssets_Docs_Path}/pdf_password_protected.pdf")"
3+
Password="12345" />
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
@if (showPdfViewer)
2+
{
3+
<PdfViewer Class="mb-3"
4+
Url="@($"/{DemoStringConstants.StaticAssets_Docs_Path}/pdf_password_protected.pdf")" />
5+
}
6+
else
7+
{
8+
<Button Class="btn btn-secondary mb-3" @onclick="() => showPdfViewer = true">Load Password Protected PDF (Prompt for Password)</Button>
9+
}
10+
11+
@code {
12+
private bool showPdfViewer = false;
13+
}

BlazorExpress.Bulma.Demo.RCL/Pages/Docs/PdfViewer/PdfViewer_Doc_01_Documentation.razor

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919
<DocxTable TItem="PdfViewer" DocType="DocType.Parameters" />
2020
</Section>
2121

22-
<Section Class="p-0" Size="HeadingSize.H4" Name="Methods" PageUrl="@pageUrl" Link="methods">
22+
@* <Section Class="p-0" Size="HeadingSize.H4" Name="Methods" PageUrl="@pageUrl" Link="methods">
2323
<DocxTable TItem="PdfViewer" DocType="DocType.Methods" />
24-
</Section>
24+
</Section> *@
2525

2626
<Section Class="p-0" Size="HeadingSize.H4" Name="Events" PageUrl="@pageUrl" Link="events">
2727
<DocxTable TItem="PdfViewer" DocType="DocType.Events" />
Binary file not shown.

BlazorExpress.Bulma/Components/PdfViewer/PdfViewer.razor.cs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public partial class PdfViewer : BulmaComponentBase
3232
protected override async Task OnAfterRenderAsync(bool firstRender)
3333
{
3434
if (firstRender)
35-
await PdfViewerJsInterop.InitializeAsync(objRef!, Id!, scale, rotation, Url!);
35+
await PdfViewerJsInterop.InitializeAsync(objRef!, Id!, scale, rotation, Url!, Password!);
3636

3737
await base.OnAfterRenderAsync(firstRender);
3838
}
@@ -73,6 +73,15 @@ public void DocumentLoaded(PdfViewerModel pdfViewerModel)
7373
OnDocumentLoaded.InvokeAsync(new PdfViewerEventArgs(pageNumber, pagesCount));
7474
}
7575

76+
[JSInvokable]
77+
public void DocumentLoadError(string errorMessage)
78+
{
79+
if (string.IsNullOrEmpty(errorMessage)) return;
80+
81+
if (OnDocumentLoadError.HasDelegate)
82+
OnDocumentLoadError.InvokeAsync(errorMessage);
83+
}
84+
7685
[JSInvokable]
7786
public void SetPdfViewerMetaData(PdfViewerModel pdfViewerModel)
7887
{
@@ -228,12 +237,24 @@ private async Task ZoomOutAsync()
228237
/// <summary>
229238
/// This event fires immediately after the PDF document is loaded.
230239
/// </summary>
240+
[AddedVersion("1.0.0")]
241+
[Description("This event fires immediately after the PDF document is loaded.")]
231242
[Parameter]
232243
public EventCallback<PdfViewerEventArgs> OnDocumentLoaded { get; set; }
233244

245+
/// <summary>
246+
/// This event fires if there is an error loading the PDF document.
247+
/// </summary>
248+
[AddedVersion("1.1.0")]
249+
[Description("This event fires if there is an error loading the PDF document.")]
250+
[Parameter]
251+
public EventCallback<string> OnDocumentLoadError { get; set; }
252+
234253
/// <summary>
235254
/// This event fires immediately after the page is changed.
236255
/// </summary>
256+
[AddedVersion("1.0.0")]
257+
[Description("This event fires immediately after the page is changed.")]
237258
[Parameter]
238259
public EventCallback<PdfViewerEventArgs> OnPageChanged { get; set; }
239260

@@ -243,9 +264,25 @@ private async Task ZoomOutAsync()
243264
/// <remarks>
244265
/// Default value is <see cref="Orientation.Portrait" />.
245266
/// </remarks>
267+
[AddedVersion("1.0.0")]
268+
[DefaultValue(Orientation.Portrait)]
269+
[Description("Gets or sets the preferred orientation for the PDF viewer.")]
246270
[Parameter]
247271
public Orientation Orientation { get; set; } = Orientation.Portrait;
248272

273+
/// <summary>
274+
/// Gets or sets the password used for the PDF document if it is password-protected.
275+
/// </summary>
276+
/// <remarks>
277+
/// Default value is <see langword="null"/>.
278+
/// </remarks>
279+
[AddedVersion("1.1.0")]
280+
[DefaultValue(null)]
281+
[Description("Gets or sets the password used for the PDF document if it is password-protected.")]
282+
[Parameter]
283+
[ParameterTypeName("string?")]
284+
public string? Password { get; set; }
285+
249286
/// <summary>
250287
/// Provides JavaScript interop functionality for the PDF viewer.
251288
/// </summary>
@@ -259,7 +296,11 @@ private async Task ZoomOutAsync()
259296
/// <remarks>
260297
/// Default value is null.
261298
/// </remarks>
299+
[AddedVersion("1.0.0")]
300+
[DefaultValue(null)]
301+
[Description("Gets or sets the URL of the PDF document to be displayed. PDF Viewer component supports base64 string as a URL.")]
262302
[Parameter]
303+
[ParameterTypeName("string?")]
263304
public string? Url { get; set; }
264305

265306
#endregion

BlazorExpress.Bulma/Components/PdfViewer/PdfViewerJsInterop.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ public async Task GotoPageAsync(object objRef, string elementId, int gotoPageNum
4141
await module.InvokeVoidAsync("gotoPage", objRef, elementId, gotoPageNum);
4242
}
4343

44-
public async Task InitializeAsync(object objRef, string elementId, double scale, double rotation, string url)
44+
public async Task InitializeAsync(object objRef, string elementId, double scale, double rotation, string url, string password)
4545
{
4646
var module = await moduleTask.Value;
47-
await module.InvokeVoidAsync("initialize", objRef, elementId, scale, rotation, url);
47+
await module.InvokeVoidAsync("initialize", objRef, elementId, scale, rotation, url, password);
4848
}
4949

5050
public async Task LastPageAsync(object objRef, string elementId)

BlazorExpress.Bulma/wwwroot/js/blazorexpress.bulma.pdf.js

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -194,17 +194,57 @@ pageRotateCwButton.disabled = this.pagesCount === 0;
194194
pageRotateCcwButton.disabled = this.pagesCount === 0;
195195
*/
196196

197-
export function initialize(dotNetHelper, elementId, scale, rotation, url) {
197+
export function initialize(dotNetHelper, elementId, scale, rotation, url, password = null) {
198198
const pdf = new Pdf(elementId);
199199
pdf.scale = scale;
200200
pdf.rotation = rotation;
201201

202-
pdfJS.getDocument(url).promise.then(function (doc) {
203-
pdf.pdfDoc = doc;
204-
pdf.pagesCount = doc.numPages;
205-
renderPage(pdf, pdf.pageNum);
206-
dotNetHelper.invokeMethodAsync('DocumentLoaded', { pagesCount: pdf.pagesCount, pageNumber: pdf.pageNum });
207-
});
202+
// prepare loading options (optional password allowed)
203+
const options = { url };
204+
if (password) {
205+
options.password = password; // pre-supply if user already has it
206+
}
207+
208+
// begin loading document
209+
const loadingTask = pdfJS.getDocument(options);
210+
211+
// handle password only when required (optional password support)
212+
loadingTask.onPassword = function (updatePassword, reason) {
213+
if (reason === pdfJS.PasswordResponses.NEED_PASSWORD) {
214+
// only prompt if PDF actually requires password
215+
const password = prompt("This PDF is password protected. Enter password:");
216+
updatePassword(password);
217+
} else if (reason === pdfJS.PasswordResponses.INCORRECT_PASSWORD) {
218+
const password = prompt("Incorrect password. Please try again:");
219+
updatePassword(password);
220+
}
221+
};
222+
223+
// handle the promise
224+
loadingTask
225+
.promise
226+
.then(function (doc) {
227+
pdf.pdfDoc = doc;
228+
pdf.pagesCount = doc.numPages;
229+
renderPage(pdf, pdf.pageNum);
230+
231+
// notify .NET side that document is loaded
232+
dotNetHelper.invokeMethodAsync('DocumentLoaded', {
233+
pagesCount: pdf.pagesCount,
234+
pageNumber: pdf.pageNum
235+
});
236+
})
237+
.catch(function (error) {
238+
console.error("PDF loading error:", error);
239+
240+
// handle password exceptions specifically
241+
if (error.name === "PasswordException") {
242+
console.error("Password required but not provided");
243+
}
244+
245+
// notify .NET side that document loading failed
246+
dotNetHelper.invokeMethodAsync('DocumentLoadError', error.message);
247+
});
208248
}
209249

210250
function isDomSupported() {

0 commit comments

Comments
 (0)