Public
Edited
Feb 19, 2023
Importers
11 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
pdfView(await FileAttachment("sample.pdf").url(), {
backgroundColor: "#776EA7",
fontColor: "#BDC6FF",

scale: 0.5
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
pdfView = async (url, opts) => {
const options = Object.assign(
{},
{
startPage: 1,
scale: 1,
backgroundColor: "#f4f4f4",
fontColor: "#6e6e6e"
},
opts
);
const { startPage } = options;
let rootEl;
let prevButton;
let nextButton;
let currentPage = startPage || 1;
let renderWidth;

const uid = initializeStyles(options);
const ns = uid.id;

// Download PDF
const pdfDoc = await pdfjsLib.getDocument(url).promise;
const totalPages = pdfDoc.numPages;

// Initialise canvas and context
const canvas = html`<canvas class="canvas"></canvas>`;
const pagePicker = pagePickerInput(currentPage, totalPages);

// Scale width based on the device pixel ratio
function scaleWidth(width) {
return Math.floor(width / dpr);
}

function updateUI(width) {
pagePicker.value = currentPage;

if (rootEl && width) {
rootEl.style.maxWidth = `${scaleWidth(width)}px`;
}

if (currentPage <= 1) {
prevButton[0].disabled = true;
} else {
prevButton[0].disabled = false;
}

if (currentPage >= totalPages) {
nextButton[0].disabled = true;
} else {
nextButton[0].disabled = false;
}
}

async function renderCurrentPage() {
return renderPageToCanvas(canvas, pdfDoc, currentPage, options);
}

async function showPage(num) {
if (num >= 1 && num <= totalPages) {
currentPage = num;
const { width } = await renderCurrentPage();
updateUI(width);
}
}

function showNextPage() {
const nextPage =
currentPage + 1 > totalPages ? currentPage : currentPage + 1;
showPage(nextPage);
}

function showPreviousPage() {
const nextPage = currentPage - 1 < 1 ? currentPage : currentPage - 1;
showPage(nextPage);
}

const pageChangeHander = (e) => {
const target = e.currentTarget;
const nextPage = +target.value;
showPage(nextPage);
};
pagePicker.addEventListener("change", pageChangeHander);
invalidation.then(() =>
pagePicker.removeEventListener("change", pageChangeHander)
);

prevButton = Inputs.button("Previous", {
reduce: () => showPreviousPage()
});
nextButton = Inputs.button("Next", {
reduce: () => showNextPage()
});

const { width } = await renderCurrentPage();
updateUI();

const paginationEl =
totalPages > 1
? html`<div class="${ns}__pagination">
<div class="${ns}__actions">
<div>${prevButton}</div>
<div>${nextButton}</div>
</div>
<div class="${ns}__info">
${pagePicker} of <span>${totalPages}</span>
</div>
</div>`
: null;

rootEl = html`<div class="${ns}" style="max-width: ${scaleWidth(width)}px;">
<div class="${ns}__canvas-wrapper">
${canvas}
</div>
${paginationEl}
</div>`;

return rootEl;
}
Insert cell
pagePickerInput = (startPage, pages) => {
const options = Array.from({ length: pages }).map((_, i) => {
const index = i + 1;
return html`<option value="${index}" selected="${index === startPage}">${index}</option>`;
});

const input = html`<select>${options}</select>`;
return input;
}
Insert cell
initializeStyles = (options) => {
let initialized;
const uid = DOM.uid("pdfView");
const ns = uid.id;
if (initialized) return uid;

initialized = true;

const { backgroundColor, fontColor } = options;
const inputsNs = Inputs.text().classList[0];

const styles = html`<style name="${ns}">
.${ns} form.${inputsNs} {
width: auto;
}

.${ns} {
--background-color: ${backgroundColor};
--font-color: ${fontColor};

padding: 0.5rem;
background-color: var(--background-color);
border-radius: 0.5rem;
overflow: hidden;
}

.${ns} > * + * {
margin-top: 0.5rem;
}

.${ns}__pagination {
display: flex;
justify-content: space-between;
align-items: center;
}

.${ns}__actions {
display: flex;
}

.${ns}__actions > * + * {
margin-left: 0.5rem;
}

.${ns}__info {
font-size: 0.875rem;
color: var(--font-color);
}

.${ns}__canvas-wrapper canvas {
max-width: 100%;
border-radius: 0.25rem;
}

.${ns} button[disabled] {
cursor: not-allowed;
}
</style>`;

document.querySelector("head").append(styles);

invalidation.then(
() => styles.parentNode && styles.parentNode.removeChild(styles)
);
return uid;
}
Insert cell
renderPageToCanvas = async (canvas, pdf, pageNum, options) => {
const { scale } = options;
const ctx = canvas.getContext("2d");

const page = await pdf.getPage(pageNum);
const viewport = page.getViewport({ scale: scale * dpr });
canvas.height = viewport.height;
canvas.width = viewport.width;

await page.render({
canvasContext: ctx,
viewport
});

return {
height: viewport.height,
width: viewport.width
};
}
Insert cell
dpr = window.devicePixelRatio
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more