Easily create high resolution thumbnail images from selected document pages.
This demo allows you to:
Implementation steps
To add Thumbnail Creation capability with WebViewer:
Step 1: Get started with WebViewer in your preferred web stack
Step 2: Add the ES6 JavaScript sample code provided in this guide
1// ES6 Compliant Syntax
2// GitHub Copilot v1.0, GPT-4.1, October 15, 2025
3// File: index.js
4
5import WebViewer from '@pdftron/webviewer';
6
7// Create Thumbnail Demo
8// This code demonstrates how to create a high resolution JPG or PNG thumbnail for a PDF document using the loadCanvas API.
9
10function initializeWebViewer() {
11
12 // This code initializes the WebViewer with the basic settings
13 WebViewer({
14 path: '/lib',
15 licenseKey: 'YOUR_LICENSE_KEY',
16 enableFilePicker: true,
17 loadAsPDF: true, // Ensure files are loaded as PDF documents for best thumbnail quality
18 }, document.getElementById('viewer')).then((instance) => {
19 // Enable the measurement toolbar so it appears with all the other tools, and disable Cloudy rectangular tool
20 const cloudyTools = [
21 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT,
22 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT2,
23 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT3,
24 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT4,
25 ];
26 instance.UI.enableFeatures([instance.UI.Feature.Measurement, instance.UI.Feature.Initials]);
27 instance.UI.disableTools(cloudyTools);
28 // Set default toolbar group to Annotate
29 instance.UI.setToolbarGroup('toolbarGroup-Annotate');
30 // Set default tool on mobile devices to Pan.
31 if (UIElements.isMobileDevice()) {
32 instance.UI.setToolMode(instance.Core.Tools.ToolNames.PAN);
33 }
34
35 instance.Core.documentViewer.addEventListener('documentUnloaded', () => {
36 if (searchParams.has('file')) {
37 searchParams.delete('file');
38 history.replaceState(null, '', '?' + searchParams.toString());
39 }
40 });
41
42 instance.Core.annotationManager.enableAnnotationNumbering();
43 instance.UI.NotesPanel.enableAttachmentPreview();
44 // Add the demo-specific functionality
45 customizeUI(instance).then(() => {
46 // Create UI controls after demo is initialized
47 UIElements.createUIControls(instance);
48 });
49 });
50}
51
52const searchParams = new URLSearchParams(window.location.search);
53const history = window.history || window.parent.history || window.top.history;
54
55window.pageNum = 1;
56window.scaleNum = 1.0;
57window.thumbnailName = 'thumbnail';
58window.thumbnailType = 'png';
59
60window.thumbnailOptions = ['PNG', 'JPEG'];
61
62
63const customizeUI = async (instance) => {
64 // Load the default document
65 await instance.UI.loadDocument('https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/WebviewerDemoDoc.pdf');
66};
67
68// Function to handle the Create Thumbnail button click
69window.onThumbnailButtonClick = (instance) => {
70 // Get the document
71 const doc = instance.Core.documentViewer.getDocument();
72
73 // Get the page number from user input
74 let pageNumber = parseInt(window.pageNum, 10);
75
76 // Validate page number
77 if (isNaN(pageNumber) || pageNumber < 1 || pageNumber > doc.getPageCount()) {
78 alert('Please enter a valid page number between 1 and ' + doc.getPageCount());
79 return; // Exit if page number is invalid
80 }
81
82 // Clamp the page number to valid range
83 pageNumber = isNaN(pageNumber) ? 1 : Math.min(Math.max(pageNumber, 1), doc.getPageCount());
84
85 // Get the scale (zoom level) from user input
86 const zoom = parseFloat(window.scaleNum);
87 if (isNaN(zoom) || zoom <= 0 || zoom > 10) {
88 alert('Please enter a valid zoom level between 0.1 and 10');
89 return; // Exit if zoom is invalid
90 }
91
92 // Compensate for device pixel ratio to ensure consistent output across devices
93 // Normalize to standard DPR of 1.0 by dividing by actual DPR
94 const devicePixelRatio = window.devicePixelRatio || 1;
95 const adjustedZoom = zoom / devicePixelRatio;
96
97 console.log(`Device Pixel Ratio: ${devicePixelRatio}`);
98 console.log(`Original zoom: ${zoom}, Adjusted zoom: ${adjustedZoom}`);
99 console.log(`This should produce thumbnails equivalent to DPR=1.0 environment`);
100
101 // Get the file name and type from user input
102 const name = window.thumbnailName;
103 const type = window.thumbnailType;
104
105 // Save to blob using the loadCanvas API
106 doc.loadCanvas({
107 pageNumber,
108 zoom: adjustedZoom, // Use DPI-adjusted zoom for consistent output
109 drawComplete: async (thumbnail) => {
110 // Optionally, comment out "drawAnnotations" to exclude annotations
111 await instance.Core.documentViewer
112 .getAnnotationManager()
113 .drawAnnotations(pageNumber, thumbnail);
114 // thumbnail is a HTMLCanvasElement or HTMLImageElement
115 thumbnail.toBlob(
116 (blob) => {
117 saveAs(blob, name + '.' + type);
118 },
119 'image/' + type,
120 1
121 );
122 },
123 });
124};
125
126// Cleanup function for when the demo is closed or page is unloaded
127const cleanup = (instance) => {
128 if (typeof instance !== 'undefined' && instance.UI) {
129 if (instance.Core.documentViewer.getDocument()) {
130 // Insert any other cleanup code here
131 }
132 console.log('Cleaning up demo');
133 }
134};
135
136// Register cleanup for page unload
137window.addEventListener('beforeunload', () => cleanup(instance));
138window.addEventListener('unload', () => cleanup(instance));
139
140// UI Elements Script Loader
141// Loads the ui-elements.js script
142function loadUIElementsScript() {
143 return new Promise((resolve, reject) => {
144 if (window.UIElements) {
145 console.log('UIElements already loaded');
146 resolve();
147 return;
148 }
149 const script = document.createElement('script');
150 script.src = '/showcase-demos/create-thumbnail/ui-elements.js';
151 script.onload = function () {
152 console.log('✅ UIElements script loaded successfully');
153 resolve();
154 };
155 script.onerror = function () {
156 console.error('Failed to load UIElements script');
157 reject(new Error('Failed to load ui-elements.js'));
158 };
159 document.head.appendChild(script);
160 });
161}
162
163// Load UIElements script first, then initialize WebViewer
164loadUIElementsScript().then(() => {
165 initializeWebViewer();
166}).catch((error) => {
167 console.error('Failed to load UIElements:', error);
168});
169
1// ES6 Compliant Syntax
2// GitHub Copilot v1.0, GPT-4.1, October 15, 2025
3// File: ui-elements.js
4
5// UI Elements class to create and manage custom UI controls
6//
7// Helper code to add controls to the viewer holding the buttons
8// This code creates a container for the buttons, styles them, and adds them to the viewer
9//
10class UIElements {
11 // Function to check if the user is on a mobile device
12 static isMobileDevice = () => {
13 return (
14 /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(
15 window.navigator.userAgent
16 ) ||
17 /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test(
18 window.navigator.userAgent.substring(0, 4)
19 )
20 );
21 }
22
23 // Create and return the page number input field
24 static pageNumberInputField = () => {
25 // Create a container for the input field
26 const container = document.createElement('div');
27 container.className = 'input-container';
28 container.style.display = 'flex';
29
30 // Create and append the label
31 const label = document.createElement('label');
32 label.className = 'input-label';
33 label.textContent = 'Page Number';
34 container.appendChild(label);
35
36 // Create and append the input field
37 const input = document.createElement('input');
38 input.className = 'number-input-field';
39 input.type = 'number';
40 input.value = window.pageNum;
41 input.onchange = (e) => {
42 window.pageNum = e.target.value;
43 };
44 input.min = 1;
45 input.pattern = "\d+";
46 container.appendChild(input);
47
48 return container;
49 }
50
51 // Create and return the scale factor input field
52 static scaleFactorInputField = () => {
53 // Create a container for the input field
54 const container = document.createElement('div');
55 container.className = 'input-container';
56
57 // Create and append the label
58 const label = document.createElement('label');
59 label.className = 'input-label';
60 label.textContent = 'Scale Factor';
61 container.appendChild(label);
62
63 // Create and append the input field
64 const input = document.createElement('input');
65 input.className = 'number-input-field';
66 input.type = 'number';
67 input.value = window.scaleNum;
68 input.onchange = (e) => {
69 window.scaleNum = e.target.value;
70 }
71 input.min = 0.1;
72 input.step = 0.1;
73 input.max = 10;
74 container.appendChild(input);
75
76 return container;
77 }
78
79 // Create and return the file name input field
80 static fileNameInputField = () => {
81 // Create a container for the input field
82 const container = document.createElement('div');
83 container.className = 'input-container';
84
85 // Create and append the label
86 const label = document.createElement('label');
87 label.className = 'input-label';
88 label.textContent = 'File Name';
89 container.appendChild(label);
90
91 // Create and append the input field
92 const input = document.createElement('input');
93 input.className = 'text-input-field';
94 input.type = 'text';
95 input.value = window.thumbnailName;
96 input.onchange = (e) => {
97 window.thumbnailName = e.target.value;
98 };
99 input.required = true;
100 input.minLength = 1;
101 container.appendChild(input);
102
103 return container;
104 }
105
106 // Create and return the image type dropdown field
107 static imageTypeInputField = () => {
108 // Create a container for the dropdown
109 const container = document.createElement('div');
110 container.className = 'input-container';
111
112 // Create and append the label
113 const label = document.createElement('label');
114 label.className = 'input-label';
115 label.textContent = 'Image Type';
116 container.appendChild(label);
117
118 // Create and append the dropdown
119 const select = document.createElement('select');
120 select.className = 'dropdown-input-field';
121 select.value = window.thumbnailType;
122 select.onchange = (e) => {
123 window.thumbnailType = e.target.value;
124 };
125
126 // Add options to the dropdown
127 window.thumbnailOptions.forEach((option) => {
128 const optionElement = document.createElement('option');
129 optionElement.value = option.toLowerCase();
130 optionElement.textContent = option;
131 select.appendChild(optionElement);
132 });
133 container.appendChild(select);
134
135 return container;
136 }
137
138 // Create and return the download button
139 static downloadButton = (instance) => {
140 // Create the button element
141 const button = document.createElement('button');
142 button.className = 'button';
143 button.textContent = 'Download Thumbnail';
144 button.onclick = () => window.onThumbnailButtonClick(instance);
145
146 return button;
147 }
148
149 static createUIControls = (instance) => {
150 // Create a container for all controls
151 const controlsContainer = document.createElement('div');
152 controlsContainer.className = 'controls-container';
153
154 controlsContainer.appendChild(this.pageNumberInputField());
155 controlsContainer.appendChild(this.scaleFactorInputField());
156 controlsContainer.appendChild(this.fileNameInputField());
157 controlsContainer.appendChild(this.imageTypeInputField());
158 controlsContainer.appendChild(this.downloadButton(instance));
159
160 // Add the controls container to the viewer element
161 const element = document.getElementById('viewer');
162 element.insertBefore(controlsContainer, element.firstChild);
163
164 console.log('✅ UI controls created');
165 };
166}
167
168// Make UIElements globally available
169window.UIElements = UIElements;
1/* CSS standard Compliant Syntax */
2/* GitHub Copilot v1.0, GPT-4.1, October 15, 2025 */
3/* File: index.css */
4
5/* Button Styles */
6.button {
7 background-color: #007bff;
8 margin: 0 10px;
9 padding: 5px 10px;
10 border: 1px solid #ccc;
11 border-radius: 4px;
12 cursor: pointer;
13 font-size: 14px;
14 font-weight: bold;
15 transition: all 0.2s ease;
16 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
17 color: white;
18}
19
20.button:hover {
21 background-color: #0056b3;
22 transform: translateY(-1px);
23 box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
24}
25
26.button:active {
27 transform: translateY(1px);
28 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
29}
30
31/* Text Inputs Container */
32.text-inputs-container {
33 display: flex;
34 flex-direction: row;
35 flex-wrap: wrap;
36 gap: 10px;
37}
38
39/* Input Controls */
40.input-container {
41 display: flex;
42 flex-direction: column;
43 align-items: flex-start;
44 margin-bottom: 15px;
45}
46
47.input-label {
48 display: block;
49 text-align: start;
50 font-size: 14px;
51 margin-inline-end: 12px;
52 margin-bottom: 8px;
53 font-weight: bold;
54 transition: background-color border-color color fill stroke opacity box-shadow transform;
55 transition-duration: 200ms;
56 opacity: 1;
57 color: #485056;
58 line-height: 1.5;
59 width: 50%;
60 white-space: nowrap;
61}
62
63.number-input-field,
64.text-input-field,
65.dropdown-input-field {
66 width: 100%;
67 min-width: 0px;
68 outline: transparent solid 2px;
69 outline-offset: 2px;
70 position: relative;
71 appearance: none;
72 transition: background-color border-color color fill stroke opacity box-shadow transform;
73 transition-duration: 200ms;
74 border-radius: 6px;
75 background-color: white;
76 color: #485056;
77 border-width: 1px;
78 border-style: solid;
79 border-image: initial;
80 border-color: #cfd4da;
81 box-sizing: border-box;
82 font-size: 14px;
83 padding-inline-start: 16px;
84 padding-inline-end: 16px;
85 height: 40px;
86}
87
88.number-input-field:focus,
89.text-input-field:focus,
90.dropdown-input-field:focus {
91 border-color: #007bff;
92 box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
93}
94
95/* Controls Container */
96.controls-container {
97 display: flex;
98 flex-direction: row;
99 align-items: center;
100 gap: 15px;
101 margin: 5px 0;
102 padding: 16px;
103 padding-bottom: 5px;
104 border-bottom: 1px solid #DFE1E6;
105 background-color: rgba(112, 198, 255, 0.2);
106}
107
108
Did you find this helpful?
Trial setup questions?
Ask experts on DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales