Get a first look at what's coming. The Developer Passport Australia Release Preview kicks off March 12. Dive in! 

📄 Displaying PDF in Service Portal Widget (Without Forcing Download)

NagaChandaE
Kilo Sage

 

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

  1. Get attachment URL (/sys_attachment.do?sys_id=...)

  2. Load it using PDF.js

  3. Convert each page into a canvas

  4. 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 functionality

  • pdfUrl → 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.


Screenshot 2026-03-18 at 2.16.14 PM.png

Screenshot 2026-03-18 at 2.15.30 PM.png

 
Screenshot 2026-03-18 at 2.50.31 PM.png
 
link : 
https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js
 
📸Widget Output :

Screenshot 2026-03-18 at 2.45.36 PM.png


⚖️Key difference

MethodBehavior
EmbedBrowser renders PDF
PDF.jsApplication 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



 

0 REPLIES 0