đ Displaying PDF in Service Portal Widget (Without Forcing Download)
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
2 hours ago - last edited 2 hours ago
By default, ServiceNow does not render PDF files inside Service Portal widgets.
Instead, when accessing an attachment, the platform forces the file to download.
đWhy this happens
This behavior is controlled by the system property:
đglide.ui.attachment.force_download_all_mime_types
Default value:
true
What it does:
Forces all attachments to download
Prevents files from opening inside the browser
Protects against malicious content execution
â ď¸Common misunderstanding
Many assume:
âServiceNow does not support PDF previewâ
Thatâs not accurate.
đThe real issue is:
Browser rendering is blocked, not PDF usage itself
âSimple approach (but not ideal)
You can set:
glide.ui.attachment.force_download_all_mime_types = falseâ
And use:
<embed src="/sys_attachment.do?sys_id={{data.sys_id}}&view=true"
type="application/pdf"
width="100%"
height="600px">Problem with this approach
Affects entire instance
Reduces security
Allows browser-based rendering of all file types
Example 1
HTML attachment might render instead of downloading â security issue
Example 2
SVG or script-based files may execute in browser
đThis is why this approach is not recommended in production.
â Correct approach (Rendering PDF inside widget)
Instead of relying on browser rendering, we can use PDF.js
This allows us to:
Fetch the PDF file
Render it manually inside a widget
Display pages using
<canvas>
đ§ How it works
Get attachment URL (
/sys_attachment.do?sys_id=...)Load it using PDF.js
Convert each page into a canvas
Display inside widget container
Example 1
User opens record â widget loads â PDF pages rendered inside portal
Example 2
Large PDF â only visible pages rendered (lazy loading)
đĄAdvantages
â No system property change
â Works within Service Portal
â Full UI control (navigation, zoom, lazy load)
â Better performance for large files
Below is the widget code
đ§ą HTML :
<div class="pdf-viewer-wrapper">
<!-- Top toolbar (navigation + file info + download) -->
<div class="pdf-toolbar">
<!-- LEFT: Navigation controls -->
<div class="toolbar-left">
<!-- Go to previous page -->
<button
ng-click="c.prevPage()"
ng-disabled="c.currentPage <= 1"
class="btn btn-sm">
â Prev
</button>
<!-- Current page indicator -->
<span class="page-info">
Page <strong>{{c.currentPage}}</strong> of <strong>{{c.totalPages}}</strong>
</span>
<!-- Go to next page -->
<button
ng-click="c.nextPage()"
ng-disabled="c.currentPage >= c.totalPages"
class="btn btn-sm">
Next âś
</button>
</div>
<!-- CENTER: File name (always centered using absolute positioning) -->
<div class="toolbar-center">
<strong>{{data.fileName}}</strong>
</div>
<!-- RIGHT: Download link -->
<div class="toolbar-right">
<a href="/sys_attachment.do?sys_id={{data.pdfSysId}}">
Download
</a>
</div>
</div>
<!-- Scrollable area where all PDF pages live -->
<div id="pdfPageArea" class="pdf-page-area">
<!-- Container where page wrappers are dynamically inserted -->
<div id="pdfContainer" class="pdf-container">
<!-- JS will inject page-wrapper divs + canvas here -->
</div>
</div>
</div>
đ¨CSS :
/* Outer wrapper for entire PDF viewer */
.pdf-viewer-wrapper {
font-family: Arial, sans-serif;
border: 1px solid #ccc;
border-radius: 4px;
overflow: hidden; /* prevents content overflow */
background: #525659; /* gray background like PDF viewers */
}
/* Top toolbar styling */
.pdf-toolbar {
display: flex;
justify-content: space-between; /* left - center - right */
align-items: center;
background: #323232;
color: #fff;
padding: 8px 14px;
flex-wrap: wrap; /* prevents breaking on small screens */
gap: 8px;
}
/* Generic toolbar container (not used directly but structured layout) */
.toolbar {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
}
/* Left & right sections aligned horizontally */
.toolbar-left,
.toolbar-right {
display: flex;
align-items: center;
gap: 8px;
}
/* Center section forced to stay in middle */
.toolbar-center {
position: absolute;
left: 50%;
transform: translateX(-50%); /* perfect center alignment */
font-size: 14px;
font-weight: 600;
color: #ddd;
pointer-events: none; /* prevents blocking clicks */
}
/* Page text spacing */
.page-info {
min-width: 120px;
}
/* Button styling */
.pdf-toolbar .btn {
background: #555;
color: #fff;
border: none;
border-radius: 3px;
padding: 4px 10px;
cursor: pointer;
font-size: 13px;
}
/* Hover effect (only if enabled) */
.pdf-toolbar .btn:hover:not([disabled]) {
background: #1890ff;
}
/* Disabled button state */
.pdf-toolbar .btn[disabled] {
opacity: 0.4;
cursor: not-allowed;
}
/* Scrollable PDF area */
.pdf-page-area {
overflow-y: auto; /* vertical scroll */
height: 800px; /* fixed height viewport */
background: #525659;
padding: 20px 0;
}
/* Container holding all pages stacked vertically */
.pdf-container {
display: flex;
flex-direction: column; /* stack pages */
align-items: center; /* center horizontally */
gap: 12px; /* space between pages */
}
/* Each page box */
.page-wrapper {
box-shadow: 0 4px 20px rgba(0,0,0,0.5); /* depth effect */
background: white;
position: relative; /* needed for badge positioning */
}
/* Canvas (actual PDF page) */
.page-wrapper canvas {
display: block; /* removes inline spacing issues */
}
/* Page number badge (bottom-right corner) */
.page-number-badge {
position: absolute;
bottom: 8px;
right: 10px;
background: rgba(0,0,0,0.4);
color: #fff;
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
}
/* Download link styling */
a {
color: white;
}
âď¸Client Script (Navigation + Cleanup) :
function($scope) {
var c = this;
// âââââââââââââââââââââââââââââ
// BASIC STATE (what we track)
// âââââââââââââââââââââââââââââ
c.currentPage = 1; // Which page user is currently seeing
c.totalPages = 0; // Total number of pages in PDF
c.pdfDoc = null; // PDF document object from PDF.js
c.pageWrappers = []; // All page container elements (empty placeholders)
c.renderedPages = {}; // Keeps track of which pages are already rendered
c.scale = 1.5; // Zoom level (1 = normal, >1 = zoom in)
c.renderWindow = 2; // Keep only nearby pages (current Âą2 pages)
var observer; // Used to detect which page is visible
// âââââââââââââââââââââââââââââ
// CHECK IF PDF LIBRARY EXISTS
// âââââââââââââââââââââââââââââ
if (typeof pdfjsLib === 'undefined') {
console.error('pdfjsLib not loaded');
return;
}
// Set worker file for PDF.js (required)
pdfjsLib.GlobalWorkerOptions.workersrc='https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
// âââââââââââââââââââââââââââââ
// LOAD PDF FILE
// âââââââââââââââââââââââââââââ
pdfjsLib.getDocument(c.data.pdfUrl).promise
.then(function(pdf) {
c.pdfDoc = pdf; // Save PDF object
c.totalPages = pdf.numPages; // Get total pages
// Update UI safely (Angular digest)
if (!$scope.$$phase) $scope.$apply();
buildPages(); // Create empty page containers
})
.catch(function(err) {
console.error('PDF load failed:', err);
});
// âââââââââââââââââââââââââââââ
// CREATE EMPTY PAGE BOXES
// (no content yet, just placeholders)
// âââââââââââââââââââââââââââââ
function buildPages() {
var container = document.getElementById('pdfContainer');
container.innerHTML = ''; // Clear old content
c.pageWrappers = [];
c.renderedPages = {};
// Use first page to get width/height
c.pdfDoc.getPage(1).then(function(page) {
var viewport = page.getViewport({ scale: c.scale });
for (let i = 1; i <= c.totalPages; i++) {
// Create empty container for each page
var wrapper = document.createElement('div');
wrapper.className = 'page-wrapper';
wrapper.id = 'page-' + i;
// Set size same as PDF page
wrapper.style.width = viewport.width + 'px';
wrapper.style.height = viewport.height + 'px';
// Show page number on top-right
var badge = document.createElement('div');
badge.className = 'page-number-badge';
badge.innerText = i + ' / ' + c.totalPages;
wrapper.appendChild(badge);
container.appendChild(wrapper);
c.pageWrappers.push(wrapper);
}
initObserver(); // Start tracking visible page
maintainPages(1); // Render first few pages initially
});
}
// âââââââââââââââââââââââââââââ
// OBSERVER: detects which page is visible
// âââââââââââââââââââââââââââââ
function initObserver() {
var pageArea = document.getElementById('pdfPageArea');
observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
// If page is visible on screen
if (entry.isIntersecting) {
var pageNum = parseInt(entry.target.id.split('-')[1], 10);
// Update only if page actually changed
if (c.currentPage !== pageNum) {
c.currentPage = pageNum;
// Render nearby pages, remove far ones
maintainPages(pageNum);
if (!$scope.$$phase) $scope.$apply();
}
}
});
}, {
root: pageArea, // scrolling container
threshold: 0.6 // page considered visible if 60% visible
});
// Observe all page containers
c.pageWrappers.forEach(function(wrapper) {
observer.observe(wrapper);
});
}
// âââââââââââââââââââââââââââââ
// MAIN LOGIC
// Decide which pages to keep/remove
// âââââââââââââââââââââââââââââ
function maintainPages(currentPage) {
var start = Math.max(1, currentPage - c.renderWindow);
var end = Math.min(c.totalPages, currentPage + c.renderWindow);
for (var i = 1; i <= c.totalPages; i++) {
// If page is near current â render it
if (i >= start && i <= end) {
renderPage(i);
}
// If far â remove it from DOM (save memory)
else {
unrenderPage(i);
}
}
}
// âââââââââââââââââââââââââââââ
// RENDER PAGE (draw PDF on canvas)
// âââââââââââââââââââââââââââââ
function renderPage(pageNum) {
// Skip if already rendered
if (c.renderedPages[pageNum]) return;
c.renderedPages[pageNum] = true;
var wrapper = document.getElementById('page-' + pageNum);
if (!wrapper) return;
// Show loading text
var loader = document.createElement('div');
loader.className = 'pdf-loader';
loader.innerText = 'Loading...';
wrapper.appendChild(loader);
// Get page and draw it
c.pdfDoc.getPage(pageNum)
.then(function(page) {
var viewport = page.getViewport({ scale: c.scale });
var canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
wrapper.insertBefore(canvas, wrapper.firstChild);
return page.render({
canvasContext: canvas.getContext('2d'),
viewport: viewport
}).promise;
})
.then(function() {
loader.remove(); // remove loader after render
})
.catch(function(err) {
console.error('Render failed:', err);
loader.innerText = 'Failed';
});
}
// âââââââââââââââââââââââââââââ
// REMOVE PAGE (free memory)
// âââââââââââââââââââââââââââââ
function unrenderPage(pageNum) {
if (!c.renderedPages[pageNum]) return;
var wrapper = document.getElementById('page-' + pageNum);
if (!wrapper) return;
var canvas = wrapper.querySelector('canvas');
if (canvas) canvas.remove(); // remove drawn page
delete c.renderedPages[pageNum];
}
// âââââââââââââââââââââââââââââ
// NAVIGATION (buttons)
// âââââââââââââââââââââââââââââ
c.scrollToPage = function(pageNum) {
var target = document.getElementById('page-' + pageNum);
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
c.prevPage = function() {
if (c.currentPage > 1) {
c.scrollToPage(c.currentPage - 1);
}
};
c.nextPage = function() {
if (c.currentPage < c.totalPages) {
c.scrollToPage(c.currentPage + 1);
}
};
// âââââââââââââââââââââââââââââ
// CLEANUP (important when widget destroyed)
// âââââââââââââââââââââââââââââ
$scope.$on('$destroy', function() {
if (observer) observer.disconnect();
});
}
âď¸Data Required from Server
This implementation does not depend on a specific way of fetching attachments.
You can retrieve the attachment record using any approach (GlideRecord, input, context, etc.).
đThe widget only requires two variables from the server:
data.pdfSysId = '<attachment_sys_id>';
data.pdfUrl = '/sys_attachment.do?sys_id=' + '<attachment_sys_id>';âđ§ Why only these two?
pdfSysIdâ used for download functionalitypdfUrlâ used by PDF.js to load and render the file
đWidget Dependency Setup
Before using PDF.js, you need to add it as a dependency.
https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.jsâď¸Key difference
| Method | Behavior |
| Embed | Browser renders PDF |
| PDF.js | Application renders PDF |
đĽFinal takeaway
ServiceNow is not blocking PDFs
It is blocking direct browser rendering
Using PDF.js, we can render PDFs inside widgets safely and efficiently
