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.6</Version>
<Version>10.0.7</Version>
</PropertyGroup>

<PropertyGroup>
Expand Down
3 changes: 2 additions & 1 deletion src/components/BootstrapBlazor.PdfReader/Locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"Print": "Print",
"TwoPageView": "Two pages on view",
"PresentationMode": "Presentation mode",
"DocumentProperty": "Document properties"
"DocumentProperty": "Document properties",
"CloseButtonText": "Close"
}
}
3 changes: 2 additions & 1 deletion src/components/BootstrapBlazor.PdfReader/Locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"Print": "打印",
"TwoPageView": "双页视图",
"PresentationMode": "演示模式",
"DocumentProperty": "文档属性"
"DocumentProperty": "文档属性",
"CloseButtonText": "关闭"
}
}
80 changes: 79 additions & 1 deletion src/components/BootstrapBlazor.PdfReader/PdfReader.razor
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@
}
@if (ShowTwoPagesOneView || ShowPresentationMode)
{
<Divider></Divider>
<div class="divider">
<div class="divider-mask"></div>
</div>
}
<div class="dropdown-item dropdown-item-doc">
<i class="@_dropdownItemDefaultIcon"></i>
Expand All @@ -96,4 +98,80 @@
</div>
</div>
</div>
<div class="bb-view-pdf-info">
<div class="bb-view-pdf-backdrop"></div>
<div class="bb-view-pdf-dialog">
<div class="bb-view-pdf-dialog-title mb-3">Document properties</div>
<div class="bb-view-pdf-dialog-item">
<div class="bb-view-pdf-dialog-label">File name:</div>
<div class="bb-view-pdf-dialog-filename"></div>
</div>
<div class="bb-view-pdf-dialog-item">
<div class="bb-view-pdf-dialog-label">File size:</div>
<div class="bb-view-pdf-dialog-filesize"></div>
</div>
<div class="divider">
Comment on lines +104 to +113
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): The same CSS class is used for the dialog header and the PDF title field, which breaks the JS metadata binding.

loadMetadata uses el.querySelector('.bb-view-pdf-dialog-title') and assumes it points to the Title metadata field, but in the markup that class is applied to both the header and the title value row. Because querySelector returns the first match (the header), the title cell never gets populated. Please give the title value a unique class (e.g. .bb-view-pdf-dialog-title-value) and update the JS selector to match.

<div class="divider-mask"></div>
</div>
<div class="bb-view-pdf-dialog-item">
<div class="bb-view-pdf-dialog-label">Title:</div>
<div class="bb-view-pdf-dialog-title"></div>
Comment on lines +104 to +118
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

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

The class name bb-view-pdf-dialog-title is used for both the dialog header (line 104) and a data field for the document title (line 118). This creates ambiguity and makes it impossible to target these elements separately. Consider renaming one of them, e.g., use bb-view-pdf-dialog-header for the dialog header at line 104.

Copilot uses AI. Check for mistakes.
</div>
<div class="bb-view-pdf-dialog-item">
<div class="bb-view-pdf-dialog-label">Author:</div>
<div class="bb-view-pdf-dialog-author"></div>
</div>
<div class="bb-view-pdf-dialog-item">
<div class="bb-view-pdf-dialog-label">Subject:</div>
<div class="bb-view-pdf-dialog-subject"></div>
</div>
<div class="bb-view-pdf-dialog-item">
<div class="bb-view-pdf-dialog-label">Keywords:</div>
<div class="bb-view-pdf-dialog-keywords"></div>
</div>
<div class="bb-view-pdf-dialog-item">
<div class="bb-view-pdf-dialog-label">Created:</div>
<div class="bb-view-pdf-dialog-created"></div>
</div>
<div class="bb-view-pdf-dialog-item">
<div class="bb-view-pdf-dialog-label">Modified:</div>
<div class="bb-view-pdf-dialog-modified"></div>
</div>
<div class="bb-view-pdf-dialog-item">
<div class="bb-view-pdf-dialog-label">Application:</div>
<div class="bb-view-pdf-dialog-application"></div>
</div>
<div class="divider">
<div class="divider-mask"></div>
</div>
<div class="bb-view-pdf-dialog-item">
<div class="bb-view-pdf-dialog-label">PDF producer:</div>
<div class="bb-view-pdf-dialog-producer"></div>
</div>
<div class="bb-view-pdf-dialog-item">
<div class="bb-view-pdf-dialog-label">PDF version:</div>
<div class="bb-view-pdf-dialog-version"></div>
</div>
<div class="bb-view-pdf-dialog-item">
<div class="bb-view-pdf-dialog-label">Page count:</div>
<div class="bb-view-pdf-dialog-count"></div>
</div>
<div class="bb-view-pdf-dialog-item">
<div class="bb-view-pdf-dialog-label">Page size:</div>
<div class="bb-view-pdf-dialog-size"></div>
</div>
<div class="divider">
<div class="divider-mask"></div>
</div>
<div class="bb-view-pdf-dialog-item">
<div class="bb-view-pdf-dialog-label">Fast web view:</div>
<div class="bb-view-pdf-dialog-webview">No</div>
</div>
<div class="bb-view-pdf-dialog-close">
<button type="button" class="btn btn-primary btn-close-doc">
<span>@Localizer["CloseButtonText"]</span>
</button>
</div>
</div>
</div>
</div>
53 changes: 53 additions & 0 deletions src/components/BootstrapBlazor.PdfReader/PdfReader.razor.css
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,56 @@
background-color: #000;
inset: 0;
}

.bb-view-pdf-info {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 5;
}

.bb-view-pdf-info:not(.show) {
display: none;
}

.bb-view-pdf-backdrop {
position: absolute;
inset: 0;
background-color: #000;
opacity: 0.6;
z-index: 1;
}

.bb-view-pdf-dialog {
background-color: #fff;
padding: 1rem;
z-index: 5;
border-radius: var(--bs-border-radius);
}

.bb-view-pdf-dialog-item {
display: flex;
flex-wrap: nowrap;
width: 344px;
}

.bb-view-pdf-dialog-item:not(:last-child) {
margin-block-start: .5rem;
}

.bb-view-pdf-dialog-label {
width: 108px;
white-space: nowrap;
overflow: hidden;
}

.bb-view-pdf-dialog-close {
text-align: right;
padding-block-start: 1rem;
}

.bb-view-pdf-dialog-close .btn {
--bs-btn-padding-x: 1rem;
}
138 changes: 134 additions & 4 deletions src/components/BootstrapBlazor.PdfReader/PdfReader.razor.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Data from '../BootstrapBlazor/modules/data.js';
import EventHandler from '../BootstrapBlazor/modules/event-handler.js';

if (pdfjsLib != null) {
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdf.worker.min.mjs';
pdfjsLib.GlobalWorkerOptions.workerSrc = '/_content/BootstrapBlazor.PdfReader/lib/pdf.worker.min.mjs';
}

export async function init(id, invoke, options) {
Expand Down Expand Up @@ -101,9 +101,120 @@ const loadPdf = async (el, invoke, options) => {
const pdfDocument = await loadingTask.promise;
pdfViewer.setDocument(pdfDocument);

pdfDocument.getMetadata().then(metadata => {
loadMetadata(el, pdfViewer, metadata);
});

return pdfViewer;
}

const loadMetadata = (el, pdfViewer, metadata) => {
console.log(metadata);
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

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

Remove the console.log statement before merging to production. Debug logging should not be left in production code.

Suggested change
console.log(metadata);

Copilot uses AI. Check for mistakes.

const filename = el.querySelector('.bb-view-pdf-dialog-filename');
const docTitle = el.querySelector('.bb-view-subject');
filename.textContent = docTitle.textContent;

const filesize = el.querySelector('.bb-view-pdf-dialog-filesize');
filesize.textContent = getFilesize(metadata);

Comment on lines +111 to +120
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): Multiple queried metadata elements are never populated, so the dialog will remain empty for those fields.

In loadMetadata, title, author, subject, and keywords are queried but never assigned from metadata, so those dialog rows will always be blank even when the PDF provides values. Please populate them from metadata (e.g. title.textContent = metadata.info.Title || '') so all available metadata is shown.

const title = el.querySelector('.bb-view-pdf-dialog-title');
const author = el.querySelector('.bb-view-pdf-dialog-author');
const subject = el.querySelector('.bb-view-pdf-dialog-subject');
const keywords = el.querySelector('.bb-view-pdf-dialog-keywords');
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

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

Variables title, author, subject, and keywords are declared but never used. They should either be populated with metadata values (e.g., title.textContent = metadata.info.Title) or removed if not needed.

Suggested change
const keywords = el.querySelector('.bb-view-pdf-dialog-keywords');
const keywords = el.querySelector('.bb-view-pdf-dialog-keywords');
if (title) title.textContent = metadata.info.Title || '';
if (author) author.textContent = metadata.info.Author || '';
if (subject) subject.textContent = metadata.info.Subject || '';
if (keywords) keywords.textContent = metadata.info.Keywords || '';

Copilot uses AI. Check for mistakes.
const created = el.querySelector('.bb-view-pdf-dialog-created');
created.textContent = parsePdfDate(metadata.info.CreationDate)?.toLocaleString();
Comment on lines +125 to +126
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: Direct access to metadata.info.* assumes all properties exist and can throw when metadata is missing.

In loadMetadata, please guard these accesses with metadata && metadata.info and use optional chaining or fallbacks when assigning to textContent, e.g. created.textContent = metadata.info?.CreationDate ? parsePdfDate(metadata.info.CreationDate)?.toLocaleString() : '' and similar for the other fields.

Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

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

The code doesn't handle cases where metadata.info.CreationDate might be undefined or null. Consider adding a null check or providing a fallback value to avoid displaying "undefined" or causing an error: created.textContent = parsePdfDate(metadata.info.CreationDate)?.toLocaleString() || 'N/A'

Suggested change
created.textContent = parsePdfDate(metadata.info.CreationDate)?.toLocaleString();
created.textContent = parsePdfDate(metadata.info.CreationDate)?.toLocaleString() || 'N/A';

Copilot uses AI. Check for mistakes.

const modified = el.querySelector('.bb-view-pdf-dialog-modified');
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

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

Variable modified is declared but never used. It should be populated with metadata.info.ModDate (e.g., modified.textContent = parsePdfDate(metadata.info.ModDate)?.toLocaleString()) or removed if not needed.

Suggested change
const modified = el.querySelector('.bb-view-pdf-dialog-modified');
const modified = el.querySelector('.bb-view-pdf-dialog-modified');
modified.textContent = parsePdfDate(metadata.info.ModDate)?.toLocaleString();

Copilot uses AI. Check for mistakes.

const application = el.querySelector('.bb-view-pdf-dialog-application');
application.textContent = metadata.info.Creator;
Copy link

Copilot AI Nov 29, 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 if metadata.info.Creator is undefined. Add a null check or use optional chaining: application.textContent = metadata.info.Creator || ''

Suggested change
application.textContent = metadata.info.Creator;
application.textContent = metadata.info?.Creator || '';

Copilot uses AI. Check for mistakes.

const producer = el.querySelector('.bb-view-pdf-dialog-producer');
producer.textContent = metadata.info.Producer;
Copy link

Copilot AI Nov 29, 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 if metadata.info.Producer is undefined. Add a null check or use optional chaining: producer.textContent = metadata.info.Producer || ''

Suggested change
producer.textContent = metadata.info.Producer;
producer.textContent = metadata.info?.Producer || '';

Copilot uses AI. Check for mistakes.

const version = el.querySelector('.bb-view-pdf-dialog-version');
version.textContent = metadata.info.PDFFormatVersion;
Comment on lines +128 to +137
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): The modified and webview elements are queried but never used, leaving the dialog fields inconsistent.

.bb-view-pdf-dialog-modified and .bb-view-pdf-dialog-webview are queried, but modified.textContent is never set and webview is never used (Razor currently hardcodes No). This leaves “Modified” always blank and makes the webview query dead code. Either populate modified (e.g. from metadata.info.ModDate via parsePdfDate) and drive webview from real document data, or remove these queries to avoid inconsistent UI and unused elements.

Suggested implementation:

    const created = el.querySelector('.bb-view-pdf-dialog-created');
    created.textContent = parsePdfDate(metadata.info.CreationDate)?.toLocaleString();

    const modified = el.querySelector('.bb-view-pdf-dialog-modified');
    modified.textContent = parsePdfDate(metadata.info.ModDate)?.toLocaleString();

    const application = el.querySelector('.bb-view-pdf-dialog-application');
  1. Locate where .bb-view-pdf-dialog-webview is queried in PdfReader.razor.js and either:
    • Bind it to a real data source (e.g. a boolean or flag in metadata that indicates whether the document was viewed via web), or
    • Remove the query and any associated unused variable if no such data is available yet.
  2. Ensure the Razor markup for the “Modified” field is present and correctly bound to .bb-view-pdf-dialog-modified so the populated value is visible.

Copy link

Copilot AI Nov 29, 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 if metadata.info.PDFFormatVersion is undefined. Add a null check or use optional chaining: version.textContent = metadata.info.PDFFormatVersion || ''

Suggested change
version.textContent = metadata.info.PDFFormatVersion;
version.textContent = metadata.info?.PDFFormatVersion || '';

Copilot uses AI. Check for mistakes.

const count = el.querySelector('.bb-view-pdf-dialog-count');
count.textContent = pdfViewer.pagesCount;

const size = el.querySelector('.bb-view-pdf-dialog-size');
pdfViewer.pdfDocument.getPage(pdfViewer.currentPageNumber).then(page => {
const viewport = page.getViewport({ scale: 1 });
size.textContent = `${(viewport.width / 72).toFixed(2)} * ${(viewport.height / 72).toFixed(2)} in (portrait)`;
});

const webview = el.querySelector('.bb-view-pdf-dialog-webview');
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

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

Variable webview is declared but never used. It should be populated with fast web view information or removed if not needed.

Suggested change
const webview = el.querySelector('.bb-view-pdf-dialog-webview');

Copilot uses AI. Check for mistakes.
}

function parsePdfDate(pdfDateString) {
if (!pdfDateString || typeof pdfDateString !== 'string') {
return null;
}

let dateStr = pdfDateString.startsWith('D:') ? pdfDateString.substring(2) : pdfDateString;

const pdfDateRegex = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})([Zz+-])(\d{2})'?(\d{2})'?$/;
const match = dateStr.match(pdfDateRegex);

if (!match) {
return null;
}

const [, year, month, day, hours, minutes, seconds, timezoneSign, timezoneHours, timezoneMinutes] = match;

const date = new Date(
parseInt(year),
parseInt(month) - 1,
parseInt(day),
parseInt(hours),
parseInt(minutes),
parseInt(seconds)
);

if (timezoneSign === 'Z' || timezoneSign === 'z') {
const utcTime = Date.UTC(
parseInt(year),
parseInt(month) - 1,
parseInt(day),
parseInt(hours),
parseInt(minutes),
parseInt(seconds)
);
date.setTime(utcTime);
}
else if (timezoneSign === '+' || timezoneSign === '-') {
const offsetHours = parseInt(timezoneHours);
const offsetMinutes = parseInt(timezoneMinutes || 0);
const totalOffsetMinutes = offsetHours * 60 + offsetMinutes;

if (timezoneSign === '+') {
date.setMinutes(date.getMinutes() - totalOffsetMinutes);
}
else {
date.setMinutes(date.getMinutes() + totalOffsetMinutes);
}
}
return date;
}

const getFilesize = metadata => {
const length = metadata.contentLength;
if (length < 1024) {
return `${Math.round(length)}B`;
}
else if (length < 1024 * 1024) {
return `${Math.round(length / 1024)}KB`;
}
else if (length < 1024 * 1024 * 1024) {
return `${length / 1024 / 1024}MB`;
Comment on lines +202 to +211
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): getFilesize can return undefined for large files and uses inconsistent rounding between units.

For metadata.contentLength >= 1024 * 1024 * 1024 * 1024, the function returns nothing, so the UI will display undefined. Also, MB/GB values are not rounded, unlike B/KB, which may produce long decimals. Please add a final fallback branch and standardize rounding across units (e.g. return ${(length / 1024 / 1024).toFixed(1)} MB;, and similarly for sizes ≥ 1 TB).

Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

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

The MB calculation should be rounded for consistency with KB formatting. Consider: return \${Math.round(length / 1024 / 1024)}MB``

Suggested change
return `${length / 1024 / 1024}MB`;
return `${Math.round(length / 1024 / 1024)}MB`;

Copilot uses AI. Check for mistakes.
}
else if (length < 1024 * 1024 * 1024 * 1024) {
return `${length / 1024 / 1024 / 1024}GB`;
Comment on lines +211 to +214
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

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

The GB calculation should be rounded for consistency with KB and MB formatting. Consider: return \${Math.round(length / 1024 / 1024 / 1024)}GB``

Suggested change
return `${length / 1024 / 1024}MB`;
}
else if (length < 1024 * 1024 * 1024 * 1024) {
return `${length / 1024 / 1024 / 1024}GB`;
return `${Math.round(length / 1024 / 1024)}MB`;
}
else if (length < 1024 * 1024 * 1024 * 1024) {
return `${Math.round(length / 1024 / 1024 / 1024)}GB`;

Copilot uses AI. Check for mistakes.
}
Comment on lines +211 to +215
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

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

The function doesn't have an explicit return statement for the GB case and beyond. Add an explicit return statement to ensure the function always returns a value, or add an else clause with a fallback.

Suggested change
return `${length / 1024 / 1024}MB`;
}
else if (length < 1024 * 1024 * 1024 * 1024) {
return `${length / 1024 / 1024 / 1024}GB`;
}
return `${Math.round(length / 1024 / 1024)}MB`;
}
else if (length < 1024 * 1024 * 1024 * 1024) {
return `${Math.round(length / 1024 / 1024 / 1024)}GB`;
}
else {
return `${Math.round(length / 1024 / 1024 / 1024 / 1024)}TB`;
}

Copilot uses AI. Check for mistakes.
}

const setObserver = el => {
const observer = new ResizeObserver(entries => {
relayoutToolbar(el);
Expand Down Expand Up @@ -325,6 +436,20 @@ const addToolbarEventHandlers = (el, pdfViewer, invoke, options) => {
// el.requestFullscreen();
//}
});
EventHandler.on(toolbar, "click", ".dropdown-item-doc", e => {
const dialog = el.querySelector(".bb-view-pdf-info");
if (dialog) {
dialog.classList.add("show");
}
});

const closeButton = el.querySelector(".btn-close-doc");
EventHandler.on(closeButton, 'click', e => {
const dialog = el.querySelector(".bb-view-pdf-info");
if (dialog) {
dialog.classList.remove("show");
}
});
}

const resetToolbarView = (el, pdfViewer) => {
Expand Down Expand Up @@ -402,7 +527,7 @@ const updateScaleValue = (el, value) => {
const scaleEl = el.querySelector(".bb-view-scale-input");

const scale = value * 100;
scaleEl.value = `${Math.round(scale, 0)}%`;
scaleEl.value = `${Math.round(scale)}%`;

if (scale === 25) {
minus.classList.add("disabled");
Expand All @@ -422,7 +547,7 @@ const updateScale = (pdfViewer, button, rate) => {
}

const scale = pdfViewer.currentScale;
const current = Math.round(parseFloat(scale * 100), 0);
const current = Math.round(parseFloat(scale * 100));
const step = [25, 33, 50, 67, 75, 80, 90, 100, 110, 125, 150, 175, 200, 250, 300, 400, 500];
const findValues = step.filter(s => rate > 0 ? current < s : current > s);
if (findValues.length === 0) {
Expand Down Expand Up @@ -462,7 +587,7 @@ const printPdf = url => {
}

iframe = document.createElement("iframe");
iframe.classList = "bb-view-print-iframe";
iframe.classList.add("bb-view-print-iframe");
iframe.style.position = "fixed";
iframe.style.right = "100%";
iframe.style.bottom = "100%";
Expand Down Expand Up @@ -506,5 +631,10 @@ export function dispose(id) {
if (iframe) {
iframe.remove();
}

const closeButton = el.querySelector(".btn-close-doc");
if (closeButton) {
EventHandler.off(closeButton, "click");
}
}
}
Loading