Skip to content

Instantly share code, notes, and snippets.

@crockwave
Last active April 8, 2022 23:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save crockwave/d44ec31b0c05f94e6593e68262025b6c to your computer and use it in GitHub Desktop.
Save crockwave/d44ec31b0c05f94e6593e68262025b6c to your computer and use it in GitHub Desktop.
PDF rendering using pdf.js 2.13.216 build. Render base canvas, text layer, and annotation layer. Text layer selectable by setting `z-index: 1` in CSS. Includes annotation via pdf-lib.js 1.17.1 and re-rendering on each annotation event.
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale = 1.0, maximum-scale = 1.0, user-scalable=no">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/pdfjs-dist@2.13.216/build/pdf.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/pdfjs-dist@2.13.216/build/pdf.worker.js"></script>
<script src="https://unpkg.com/pdf-lib@1.17.1"></script>
<script src="https://unpkg.com/downloadjs@1.4.7"></script>
<style type="text/css">
#upload-button {
width: 150px;
display: block;
margin: 20px auto;
}
#file-to-upload {
display: none;
}
#pdf-main-container {
width: 800px;
margin: 20px auto;
}
#pdf-loader {
display: none;
text-align: center;
color: #999999;
font-size: 13px;
line-height: 100px;
height: 100px;
}
#pdf-contents {
display: none;
}
#pdf-meta {
overflow: hidden;
margin: 0 0 20px 0;
}
#pdf-buttons {
float: left;
}
#page-count-container {
float: right;
}
#pdf-current-page {
display: inline;
}
#pdf-total-pages {
display: inline;
}
#pdf-canvas {
border: 1px solid rgba(0,0,0,0.2);
box-sizing: border-box;
}
#page-loader {
height: 100px;
line-height: 100px;
text-align: center;
display: none;
color: #999999;
font-size: 13px;
}
#annotation-layer {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
overflow: hidden;
opacity: 0.2;
line-height: 1.0;
}
#annotation-layer > section {
color: transparent;
position: absolute;
white-space: pre;
cursor: text;
transform-origin: 0% 0%;
}
#annotation-layer > .linkAnnotation > a {
position: absolute;
font-size: 1em;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.textLayer {
position: absolute;
text-align: initial;
left: 0;
top: 0;
right: 0;
bottom: 0;
overflow: hidden;
opacity: 0.2;
line-height: 1;
-webkit-text-size-adjust: none;
-moz-text-size-adjust: none;
text-size-adjust: none;
forced-color-adjust: none;
z-index: 1;
}
.textLayer span,
.textLayer br {
color: transparent;
position: absolute;
white-space: pre;
cursor: text;
transform-origin: 0% 0%;
}
/* Only necessary in Google Chrome, see issue 14205, and most unfortunately
* the problem doesn't show up in "text" reference tests. */
.textLayer span.markedContent {
top: 0;
height: 0;
}
.textLayer .highlight {
margin: -1px;
padding: 1px;
background-color: rgba(180, 0, 170, 1);
border-radius: 4px;
}
.textLayer .highlight.appended {
position: initial;
}
.textLayer .highlight.begin {
border-radius: 4px 0 0 4px;
}
.textLayer .highlight.end {
border-radius: 0 4px 4px 0;
}
.textLayer .highlight.middle {
border-radius: 0;
}
.textLayer .highlight.selected {
background-color: rgba(0, 100, 0, 1);
}
.textLayer ::-moz-selection {
background: rgba(0, 0, 255, 1);
}
.textLayer ::selection {
background: rgba(0, 0, 255, 1);
}
/* Avoids https://github.com/mozilla/pdf.js/issues/13840 in Chrome */
.textLayer br::-moz-selection {
background: transparent;
}
.textLayer br::selection {
background: transparent;
}
.textLayer .endOfContent {
display: block;
position: absolute;
left: 0;
top: 100%;
right: 0;
bottom: 0;
z-index: -1;
cursor: default;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.textLayer .endOfContent.active {
top: 0;
}
</style>
</head>
<body>
<button id="upload-button">Select PDF</button>
<input type="file" id="file-to-upload" accept="application/pdf" />
<div id="pdf-main-container">
<div id="pdf-loader">Loading document ...</div>
<div id="pdf-contents">
<div id="pdf-meta">
<div id="pdf-buttons">
<button id="pdf-prev">Previous</button>
<button id="pdf-next">Next</button>
<span id="spacer"></span>
<button id="new-upload-button">Select PDF</button>
<button id="first-page-button">First Page</button>
<button id="annotate-pdf-button">Annotate PDF</button>
<button id="save-pdf-button">Save PDF</button>
</div>
<div id="page-count-container">Page <div id="pdf-current-page"></div> of <div id="pdf-total-pages"></div></div>
</div>
<canvas id="pdf-canvas" width="800"></canvas>
<div id="text-layer" class="textLayer"></div>
<div id="annotation-layer"></div>
<div id="page-loader">Loading page ...</div>
</div>
</div>
<script>
const { degrees, PDFDocument, rgb, StandardFonts } = PDFLib
async function savePdf() {
// Trigger the browser to download the PDF document
download(__EXISTING_PDF_BYTES, "pdf-lib_modification_example.pdf", "application/pdf");
}
async function modifyPdf() {
// Load a PDFDocument from the existing PDF bytes
const pdfDoc = await PDFDocument.load(__EXISTING_PDF_BYTES)
// Embed the Helvetica font
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica)
// Get the first page of the document
const pages = pdfDoc.getPages()
const firstPage = pages[0]
// Get the width and height of the first page
const { width, height } = firstPage.getSize()
// Draw a timestamped string of text in random location
const d = new Date();
let h = addZero(d.getHours());
let m = addZero(d.getMinutes());
let s = addZero(d.getSeconds());
let time = h + ":" + m + ":" + s;
firstPage.drawText('PDF-Lib Annotation: ' + time, {
x: getRandomIntInclusive(5, 200),
y: getRandomIntInclusive(5, height - 100),
size: 20,
font: helveticaFont,
color: rgb(0.95, 0.1, 0.1),
})
__EXISTING_PDF_BYTES = await pdfDoc.save();
showUpdatedPDF();
}
var __PDF_DOC,
__CURRENT_PAGE,
__TOTAL_PAGES,
__PAGE_RENDERING_IN_PROGRESS = 0,
__CANVAS = $('#pdf-canvas').get(0),
__CANVAS_CTX = __CANVAS.getContext('2d'),
__EXISTING_PDF_BYTES;
function getRandomIntInclusive(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1) + min);
}
function addZero(i) {
if (i < 10) {i = "0" + i}
return i;
}
function showUpdatedPDF() {
// $("#pdf-loader").show();
pdfjsLib.getDocument({ data: __EXISTING_PDF_BYTES }).promise.then(function(pdf_doc) {
__PDF_DOC = pdf_doc;
__TOTAL_PAGES = __PDF_DOC.numPages;
// Hide the pdf loader and show pdf container in HTML
$("#pdf-loader").hide();
$("#pdf-contents").show();
$("#pdf-total-pages").text(__TOTAL_PAGES);
// Show the first page
showPage(1);
}).catch(function(error) {
// If error re-show the upload button
$("#pdf-loader").hide();
$("#upload-button").show();
alert(error.message);
});;
}
function showSelectedPDF(pdf_url) {
$("#pdf-loader").show();
pdfjsLib.getDocument({ url: pdf_url }).promise.then(function(pdf_doc) {
__PDF_DOC = pdf_doc;
__TOTAL_PAGES = __PDF_DOC.numPages;
__PDF_DOC.getData().then(function(pdf_data) {
__EXISTING_PDF_BYTES = pdf_data;
});
// Hide the pdf loader and show pdf container in HTML
$("#pdf-loader").hide();
$("#pdf-contents").show();
$("#pdf-total-pages").text(__TOTAL_PAGES);
// Show the first page
showPage(1);
}).catch(function(error) {
// If error re-show the upload button
$("#pdf-loader").hide();
$("#upload-button").show();
alert(error.message);
});;
}
function showPage(page_no) {
__PAGE_RENDERING_IN_PROGRESS = 1;
__CURRENT_PAGE = page_no;
// Disable Prev & Next buttons while page is being loaded
$("#pdf-next, #pdf-prev").attr('disabled', 'disabled');
// While page is being rendered hide the canvas & annotayion layer and show a loading message
$("#pdf-canvas").hide();
$("#annotation-layer").hide();
$("text-layer").hide();
$("#page-loader").show();
// Update current page in HTML
$("#pdf-current-page").text(page_no);
// Fetch the page
__PDF_DOC.getPage(page_no).then(function(page) {
// As the canvas is of a fixed width we need to set the scale of the viewport accordingly
var scale_required = __CANVAS.width / page.getViewport({ scale: 1 }).width;
// Get viewport of the page at required scale
var viewport = page.getViewport({ scale: scale_required });
// Set canvas height
__CANVAS.height = viewport.height;
var renderContext = {
canvasContext: __CANVAS_CTX,
viewport: viewport
};
// Render the page contents in the canvas
page.render(renderContext).promise.then(function() {
__PAGE_RENDERING_IN_PROGRESS = 0;
// Re-enable Prev & Next buttons
$("#pdf-next, #pdf-prev").removeAttr('disabled');
// Show the canvas and hide the page loader
$("#pdf-canvas").show();
$("#page-loader").hide();
return page.getTextContent();
}).then(function(textContent) {
// if(textContent.length == 0)
// return;
// console.log("Text content: " + JSON.stringify(textContent));
var textLayer = new pdfjsLib.renderTextLayer({
container: $("#text-layer").get(0),
pageIndex: page.pageIndex,
viewport: viewport,
textContent: textContent
});
// textLayer.setTextContent(textContent);
// textLayer.render();
// Get canvas offset
var canvas_offset = $("#pdf-canvas").offset();
// Clear HTML for text layer and show
$("#text-layer").html('').show();
// Assign the CSS created to the text-layer element
$("#text-layer").css({ left: canvas_offset.left + 'px', top: canvas_offset.top + 'px', height: __CANVAS.height + 'px', width: __CANVAS.width + 'px' });
// pdfjsLib.renderTextLayer({
// viewport: viewport,
// textDivs: [],
// container: $("#text-layer").get(0),
// textContent: textContent
// });
// Return annotation data of the page after the pdf has been rendered in the canvas
return page.getAnnotations();
}).then(function(annotationData) {
if(annotationData.length == 0)
return;
console.log("Annotations: " + JSON.stringify(annotationData, null, 1));
// Get canvas offset
var canvas_offset = $("#pdf-canvas").offset();
// Clear HTML for annotation layer and show
$("#annotation-layer").html('').show();
// Assign the CSS created to the annotation-layer element
$("#annotation-layer").css({ left: canvas_offset.left + 'px', top: canvas_offset.top + 'px', height: __CANVAS.height + 'px', width: __CANVAS.width + 'px' });
pdfjsLib.AnnotationLayer.render({
viewport: viewport.clone({ dontFlip: true }),
div: $("#annotation-layer").get(0),
annotations: annotationData,
page: page
});
});
});
}
// Upon click this should should trigger click on the #file-to-upload file input element
// This is better than showing the not-good-looking file input element
$("#upload-button").on('click', function() {
$("#file-to-upload").trigger('click');
});
$("#new-upload-button").on('click', function() {
$("#file-to-upload").trigger('click');
});
// When user chooses Annotate PDF button
$("#annotate-pdf-button").on('click', function() {
modifyPdf();
});
// When user chooses Save PDF button
$("#save-pdf-button").on('click', function() {
savePdf();
});
// When user chooses a PDF file
$("#file-to-upload").on('change', function() {
// Validate whether PDF
if(['application/pdf'].indexOf($("#file-to-upload").get(0).files[0].type) == -1) {
alert('Error : Not a PDF');
return;
}
$("#upload-button").hide();
// Send the object url of the pdf
showSelectedPDF(URL.createObjectURL($("#file-to-upload").get(0).files[0]));
// Show the first page
});
$("#first-page-button").on('click', function() {
showPage(1);
});
// Previous page of the PDF
$("#pdf-prev").on('click', function() {
if(__CURRENT_PAGE != 1)
showPage(--__CURRENT_PAGE);
});
// Next page of the PDF
$("#pdf-next").on('click', function() {
if(__CURRENT_PAGE != __TOTAL_PAGES)
showPage(++__CURRENT_PAGE);
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment