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.
Once you generate your license key, it will automatically be included in your sample code below.
Apryse collects some data regarding your usage of the SDK for product improvement.
The data that Apryse collects include:
For clarity, no other data is collected by the SDK and Apryse has no access to the contents of your documents.
If you wish to continue without data collection, contact us and we will email you a no-tracking trial key for you to get started.
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 the 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 an 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