Easily create a visualization tool for AutoCAD's DWG and DXF files. View, edit, and annotate those files as PDF files from the browser.
This demo allows you to:
Implementation steps
To add CAD Viewer capability with WebViewer:
Step 1: Choose your preferred web stack
Step 2: Download required modules listed in the Demo Dependencies section below
Step 3: Add the ES6 JavaScript sample code provided in this guide
Demo Dependencies
This sample uses the following:
1// ES6 Compliant Syntax
2// GitHub Copilot v1.0, GPT-4.1, September 29, 2025
3// File: index.js
4
5import WebViewer from '@pdftron/webviewer';
6
7// CAD Viewer Demo
8//
9// This code demonstrates how to view CAD files directly in the browser with the Apryse JavaScript document SDK.
10// It shows how to create a display and visualization tool for AutoCAD floor plans and drawings.
11// Allowing users to view, edit, and annotate AutoCAD DWG CAD files from the browser using the WebViewer Server.
12//
13// **Important**
14// 1. You must get a license key from Apryse for the server to run.
15// A trial key can be obtained from:
16// https://docs.apryse.com/core/guides/get-started/trial-key
17//
18// 2. You need to also run the `npm install` command at /cad-viewer/server/ location to install the `@pdftron/pdfnet-node` and `@pdftron/cad` packages.
19
20// Initialize WebViewer with the specified settings
21function initializeWebViewer() {
22
23 // This code initializes the WebViewer with the basic settings
24 WebViewer({
25 path: '/lib',
26 licenseKey: 'YOUR_LICENSE_KEY',
27 enableFilePicker: false,
28 }, element).then((instance) => {
29 // Enable the measurement toolbar so it appears with all the other tools, and disable Cloudy rectangular tool
30 const cloudyTools = [
31 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT,
32 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT2,
33 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT3,
34 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT4,
35 ];
36 instance.UI.enableFeatures([instance.UI.Feature.Measurement, instance.UI.Feature.Initials]);
37 instance.UI.disableTools(cloudyTools);
38
39 // Set default toolbar group to Annotate
40 instance.UI.setToolbarGroup('toolbarGroup-Annotate');
41
42 // Set default tool on mobile devices to Pan.
43 if (UIElements.isMobileDevice()) {
44 instance.UI.setToolMode(instance.Core.Tools.ToolNames.PAN);
45 }
46
47 instance.Core.documentViewer.addEventListener('documentUnloaded', () => {
48 if (searchParams.has('file')) {
49 searchParams.delete('file');
50 history.replaceState(null, '', '?' + searchParams.toString());
51 }
52 });
53
54 instance.Core.annotationManager.enableAnnotationNumbering();
55 instance.UI.NotesPanel.enableAttachmentPreview();
56
57 // Add the demo-specific functionality
58 customizeUI(instance).then(() => {
59 // Create UI controls after demo is initialized
60 UIElements.createUIControls(instance);
61 });
62 });
63}
64
65// CAD files, Key is the file name, value is the URL
66const CAD_FILES = {
67 'DWG': 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/construction%20drawings%20color.dwg',
68 'DXF': 'PROVIDE_URL_HERE/*.dxf',
69};
70
71const defaultDoc = CAD_FILES['DWG']; // Default CAD file to load
72
73// Default styles for measurement tools
74const DEFAULT_FONT_SIZE = 16;
75window.DEFAULT_FONT_SIZE = DEFAULT_FONT_SIZE;
76const DEFAULT_STROKE_THICKNESS = 2;
77window.DEFAULT_STROKE_THICKNESS = DEFAULT_STROKE_THICKNESS;
78const MEASUREMENT_TOOLS = [
79 'AnnotationCreateDistanceMeasurement',
80 'AnnotationCreatePerimeterMeasurement',
81 'AnnotationCreateAreaMeasurement',
82];
83
84window.MEASUREMENT_TOOLS = MEASUREMENT_TOOLS;
85
86const customizeUI = async (instance) => {
87 const { Feature } = instance.UI;
88 const { Annotations, documentViewer } = instance.Core;
89
90 instance.UI.enableFeatures([Feature.Measurement]);
91 instance.UI.enableTools(MEASUREMENT_TOOLS);
92 instance.UI.openElements(['tabPanel']);
93 instance.UI.setActiveLeftPanel('layersPanel');
94
95 // Update default tool styles
96 MEASUREMENT_TOOLS.forEach((tool, index) => {
97 documentViewer.getTool(tool).setStyles({
98 StrokeThickness: DEFAULT_STROKE_THICKNESS / documentViewer.getZoomLevel(),
99 StrokeColor: new Annotations.Color(
100 255 * Number(index === 0),
101 255 * Number(index === 1),
102 255 * Number(index === 2),
103 ),
104 });
105 });
106
107 // Update font size to be larger
108 Annotations.LineAnnotation.prototype['constant']['FONT_SIZE'] = DEFAULT_FONT_SIZE / documentViewer.getZoomLevel() + 'px';
109 Annotations.LineAnnotation.prototype['constant']['TEXT_COLOR'] = '#FF0000';
110 documentViewer.addEventListener('zoomUpdated', (zoom) => UIElements.zoomUpdated(instance, zoom));
111
112 // Using the Apryse CAD Module, Convert and Load default document
113 const cadUrl = defaultDoc;
114 const cadFilename = cadUrl.split('/').pop();
115 const response = await fetch(defaultDoc);
116 if (!response.ok) {
117 throw new Error(`Failed to fetch CAD: ${response.status}`);
118 }
119 const cadBuffer = await response.arrayBuffer();
120 const pdfBuffer = await convertCadtoPdf(cadBuffer, cadFilename);
121 instance.UI.loadDocument(pdfBuffer, {
122 extension: 'pdf',
123 });
124};
125
126const convertCadtoPdf = async (cadBuffer, cadFilename) => {
127 // Send the CAD to the server to be converted to PDF
128 console.log('Sending CAD to server for conversion...');
129 const cadBlob = new Blob([cadBuffer]);
130 const formData = new FormData();
131 formData.append('cadfile', cadBlob, cadFilename);
132
133 const postResponse = await fetch('http://localhost:5050/server/handler.js', {
134 method: 'POST',
135 body: formData,
136 });
137
138 if (postResponse.status !== 200) {
139 throw new Error(`Server error during CAD upload: ${postResponse.status}`);
140 }
141 const buffer = await postResponse.arrayBuffer();
142 return buffer;
143};
144
145// Make convertCadtoPdf globally available
146window.convertCadtoPdf = convertCadtoPdf;
147const searchParams = new URLSearchParams(window.location.search);
148const history = window.history || window.parent.history || window.top.history;
149const element = document.getElementById('viewer');
150
151// Cleanup function for when the demo is closed or page is unloaded
152const cleanup = (instance) => {
153 const { Feature } = instance.UI;
154
155 if (typeof instance !== 'undefined' && instance.UI) {
156 instance.UI.disableTools(MEASUREMENT_TOOLS);
157 instance.UI.disableFeatures([Feature.Measurement]);
158 instance.UI.closeElements(['leftPanel']);
159 console.log('Cleaning up cad-viewer demo');
160 }
161};
162
163// Register cleanup for page unload
164window.addEventListener('beforeunload', () => cleanup(instance));
165window.addEventListener('unload', () => cleanup(instance));
166
167//helper function to load the ui-elements.js script
168function loadUIElementsScript() {
169 return new Promise((resolve, reject) => {
170 if (window.UIElements) {
171 console.log('UIElements already loaded');
172 resolve();
173 return;
174 }
175
176 const script = document.createElement('script');
177 script.src = '/showcase-demos/cad-viewer/client/ui-elements.js';
178 script.onload = function () {
179 console.log('✅ UIElements script loaded successfully');
180 resolve();
181 };
182 script.onerror = function () {
183 console.error('Failed to load UIElements script');
184 reject(new Error('Failed to load ui-elements.js'));
185 };
186 document.head.appendChild(script);
187 });
188}
189
190// Load UIElements script first, then initialize WebViewer
191loadUIElementsScript().then(() => {
192 initializeWebViewer();
193}).catch((error) => {
194 console.error('Failed to load UIElements:', error);
195});
196
1// UI Elements class to create and manage custom UI controls
2//
3// Helper code to add controls to the viewer holding the buttons
4// This code creates a container for the buttons, styles them, and adds them to the viewer
5//
6class UIElements {
7
8 // Function to check if the user is on a mobile device
9 static isMobileDevice = () => {
10 return (
11 /(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(
12 window.navigator.userAgent
13 ) ||
14 /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(
15 window.navigator.userAgent.substring(0, 4)
16 )
17 );
18 }
19
20 // Choose File button
21 static filePicker = (instance) => {
22 const button = document.createElement('button');
23 button.className = 'btn-filepicker';
24 button.textContent = 'Choose File';
25 button.onclick = () => {
26 const input = document.createElement('input');
27 input.type = 'file';
28 input.accept = '.dwg,.dxf';
29 input.onchange = async (event) => {
30 try {
31 const file = event.target.files[0];
32 if (file) {
33 const arrayBuffer = await file.arrayBuffer();
34 const pdfBuffer = await window.convertCadtoPdf(arrayBuffer, file.name);
35 instance.UI.loadDocument(pdfBuffer, {
36 extension: 'pdf',
37 });
38 }
39 } catch (e) {
40 console.error(e);
41 }
42 };
43 input.click();
44 };
45
46 return button;
47 }
48
49 static createUIControls = (instance) => {
50 // Create a container for all controls
51 const controlsContainer = document.createElement('div');
52 controlsContainer.className = 'button-container';
53
54 // Add the file picker and Import/Export buttons to the controls container
55 controlsContainer.appendChild(this.filePicker(instance));
56
57 // Add the controls container to the viewer element
58 const element = document.getElementById('viewer');
59 element.insertBefore(controlsContainer, element.firstChild);
60 };
61
62 static zoomUpdated = (instance, zoom) => {
63 const { Annotations, documentViewer } = instance.Core;
64 Annotations.LineAnnotation.prototype['constant']['FONT_SIZE'] = window.DEFAULT_FONT_SIZE / zoom + 'px';
65
66 window.MEASUREMENT_TOOLS.forEach((tool) => {
67 documentViewer.getTool(tool).setStyles({
68 StrokeThickness: window.DEFAULT_STROKE_THICKNESS / zoom,
69 });
70 });
71 };
72}
1/* CSS standard Compliant Syntax */
2/* GitHub Copilot v1.0, GPT-4.1, September 29, 2025 */
3/* File: index.css */
4
5.btn-filepicker {
6 background-color: #007bff;
7 margin: 10px;
8 padding: 5px 10px;
9 border: 1px solid #ccc;
10 border-radius: 4px;
11 cursor: pointer;
12 font-size: 14px;
13 font-weight: bold;
14 transition: all 0.2s ease;
15 box-shadow: 0 2px 4px rgba(0,0,0,0.1);
16 color: white;
17}
18
19.btn-filepicker:hover {
20 background-color: #0056b3;
21 transform: translateY(-1px);
22 box-shadow: 0 4px 8px rgba(0,0,0,0.2);
23}
24
25.btn-filepicker:active {
26 transform: translateY(1px);
27 box-shadow: 0 1px 2px rgba(0,0,0,0.2);
28}
29
30/* Button Container */
31.button-container {
32 display: flex;
33 flex-direction: row;
34 align-items: center;
35 gap: 15px;
36 margin: 5px 0;
37 padding: 16px;
38 padding-bottom: 5px;
39 border-bottom: 1px solid #DFE1E6;
40 background-color: rgba(112, 198, 255, 0.2);
41}
1// ES6 Compliant Syntax
2// GitHub Copilot v1.0, GPT-4.1, September 29, 2025
3// File: handler.js
4// This file will handle CAD file conversion requests.
5
6const fs = require('fs');
7const { PDFNet } = require('@pdftron/pdfnet-node');
8
9// **Important**
10// 1. You must get a license key from Apryse for the server to run.
11// A trial key can be obtained from:
12// https://docs.apryse.com/core/guides/get-started/trial-key
13//
14// 2. You need to also run the `npm install` command at /cad-viewer/server/ location to install the `@pdftron/pdfnet-node` and `@pdftron/cad` packages.
15const licenseKey = 'YOUR_LICENSE_KEY';
16const multer = require('multer');
17const upload = multer({ dest: 'sentCads/' });
18const { response } = require('express');
19const serverFolder = 'server';
20const sentCads = 'sentCads';
21const serverHandler = `/${serverFolder}/handler.js`;
22
23module.exports = async (app) => {
24
25 async function initializePDFNet() {
26 // Create folder sentCads that will hold the sent CAD format files, if it doesn't exist
27 if (!fs.existsSync(sentCads))
28 fs.mkdirSync(sentCads);
29
30 // Initialize PDFNet
31 await PDFNet.initialize(licenseKey);
32
33 // Specify the CAD library path
34 await PDFNet.addResourceSearchPath('./node_modules/@pdftron/cad/lib/');
35
36 // Check if the Apryse SDK CAD module is available.
37 if (await PDFNet.CADModule.isModuleAvailable())
38 console.log('Apryse SDK CAD module is available.');
39 else
40 console.log('Unable to run: Apryse SDK CAD module not available.');
41 }
42
43 // Handle POST request sent to '/server/handler.js'
44 // This endpoint receives the CAD file URL to be loaded in the Apryse webviewer, then saves it to the server
45 app.post(serverHandler, upload.single('cadfile'), async (request, response) => {
46 try {
47 const cadPath = request.file.path;
48 const cadFilename = request.file.originalname;
49
50 console.log(`Received CAD file ${cadFilename}, processing...`);
51
52 // Convert the CAD file to PDF and get the buffer
53 const buffer = await convertCadToPdfBuffer(cadPath);
54
55 console.log(`Conversion complete, sending PDF back to client...`);
56
57 // Set headers to indicate a PDF file attachment and send the buffer
58 await response.setHeader('Content-Type', 'application/pdf');
59 await response.setHeader('Content-Disposition', `attachment; filename="${cadFilename.replace(/\.[^/.]+$/, ".pdf")}"`);
60 response.status(200).send(buffer);
61 } catch (e) {
62 response.status(500).send(`Error processing CAD file: ${e.message}`);
63 } finally {
64 // Cleanup: remove the sent CAD file
65 const cadPath = request.file.path;
66 fs.unlink(cadPath, (err) => {
67 if (err) {
68 console.error(`Error removing CAD file ${cadPath}: ${err.message}`);
69 }
70 });
71 }
72 });
73
74 const convertCadToPdfBuffer = async (cad) => {
75 try {
76 // Create a new PDF document and convert the CAD file to PDF
77 const doc = await PDFNet.PDFDoc.create();
78 await PDFNet.Convert.fromCAD(doc, cad);
79
80 // Initialize security handler and lock the document
81 doc.initSecurityHandler();
82 doc.lock();
83
84 // Save the PDF document to a memory buffer
85 const uint8Array = await doc.saveMemoryBuffer(PDFNet.SDFDoc.SaveOptions.e_linearized);
86 const buffer = Buffer.from(uint8Array);
87
88 // Unlock the document
89 doc.unlock();
90
91 // Return the PDF buffer
92 return buffer;
93 }
94 catch (err) {
95 console.log(err);
96 throw new Error(err);
97 }
98 };
99
100 // Initialize PDFNet
101 PDFNet.runWithoutCleanup(initializePDFNet, licenseKey).then(
102 function onFulfilled() {
103 response.status(200);
104 },
105 function onRejected(error) {
106 // log error and close response
107 console.error('Error initializing PDFNet', error);
108 response.status(503).send();
109 }
110 );
111};
112
1// ES6 Compliant Syntax
2// GitHub Copilot v1.0, GPT-4.1, September 29, 2025
3// File: server.js
4// This file is to run a server in localhost.
5
6const express = require('express');
7const fs = require('fs');
8const bodyParser = require('body-parser');
9const handler = require('./handler.js');
10const port = process.env.PORT || 5050;
11const app = express();
12const sentPdfs = 'sentPdfs';
13
14// CORS middleware to allow cross-origin requests from the playground
15app.use((req, res, next) => {
16 res.header('Access-Control-Allow-Origin', '*');
17 res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
18 res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
19
20 // Handle preflight OPTIONS requests
21 if (req.method === 'OPTIONS') {
22 res.sendStatus(200);
23 } else {
24 next();
25 }
26});
27
28app.use(bodyParser.text());
29app.use('/client', express.static('../client')); // For statically serving 'client' folder at '/'
30
31handler(app);
32
33// Run server
34const server = app.listen(port, 'localhost', (err) => {
35 if (err) {
36 console.error(err);
37 } else {
38 console.info(`Server is listening at http://localhost:${port}`);
39
40 }
41});
42
43// Server shutdown and cleanup
44function shutdown() {
45 console.log('Cleanup started...');
46
47 // Example: Close server
48 server.close(() => {
49 console.log('Server closed.');
50
51 // Removes sent PDFs folder
52 if (fs.existsSync(sentPdfs))
53 fs.rmdirSync(sentPdfs, { recursive: true });
54
55 // If no async cleanup, exit directly
56 process.exit(0);
57 });
58}
59
60// Handle shutdown signals
61process.on('SIGINT', shutdown); // Ctrl+C
62process.on('SIGTERM', shutdown); // kill command or Docker stop
63process.on('uncaughtException', (err) => {
64 console.error('Uncaught Exception:', err);
65 shutdown();
66});
1{
2 "name": "cad-viewer-server",
3 "version": "1.0.0",
4 "description": "CAD Viewer Demo Server Component",
5 "main": "server.js",
6 "scripts": {
7 "start": "node server.js",
8 "dev": "node server.js"
9 },
10 "dependencies": {
11 "@pdftron/cad": "^11.7.0",
12 "@pdftron/pdfnet-node": "^11.7.0",
13 "body-parser": "^1.20.2",
14 "express": "^4.18.2",
15 "multer": "^1.4.4",
16 "open": "^9.1.0"
17 },
18 "keywords": [
19 "cad-viewer",
20 "pdf",
21 "server",
22 "pdftron",
23 "webviewer"
24 ],
25 "author": "Apryse",
26 "license": "MIT"
27}
28
Did you find this helpful?
Trial setup questions?
Ask experts on DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales