Extracts key-value pairs from CAD drawings in PDF format with well-structured title blocks. Outputs the data as JSON and adds visual annotations to the document to illustrate the relationships.
This demo allows you to:
Implementation steps
To add CAD Title Block Data Extraction capability with WebViewer:
Step 1: Choose 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, Claude Sonnet 4, October 22, 2025
3// File: index.js
4
5import WebViewer from '@pdftron/webviewer';
6
7// CAD Title Block Data Extraction Demo
8//
9// This code demonstrates how to extract key-value data pairs from CAD drawings with well formed title blocks
10//
11// **Important**
12// 1. You must get a license key from Apryse for the server to run.
13// A trial key can be obtained from:
14// https://docs.apryse.com/core/guides/get-started/trial-key
15//
16// 2. You need to also run the `npm install` command at /title-block-data-extraction/server/ location to install the `@pdftron/pdfnet-node`, `@pdftron/cad`, and `@pdftron/data-extraction` packages.
17
18function initializeWebViewer() {
19
20 // This code initializes the WebViewer with the basic settings
21 WebViewer({
22 path: '/lib',
23 licenseKey: 'YOUR_LICENSE_KEY',
24 }, document.getElementById('viewer')).then((instance) => {
25 // Enable the measurement toolbar so it appears with all the other tools, and disable Cloudy rectangular tool
26 const cloudyTools = [
27 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT,
28 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT2,
29 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT3,
30 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT4,
31 ];
32 instance.UI.enableFeatures([instance.UI.Feature.Measurement, instance.UI.Feature.Initials]);
33 instance.UI.disableTools(cloudyTools);
34 // Set default toolbar group to Annotate
35 instance.UI.setToolbarGroup('toolbarGroup-Annotate');
36 // Set default tool on mobile devices to Pan.
37 if (UIElements.isMobileDevice()) {
38 instance.UI.setToolMode(instance.Core.Tools.ToolNames.PAN);
39 }
40
41 instance.Core.documentViewer.addEventListener('documentUnloaded', () => {
42 if (searchParams.has('file')) {
43 searchParams.delete('file');
44 history.replaceState(null, '', '?' + searchParams.toString());
45 }
46 });
47
48 instance.Core.annotationManager.enableAnnotationNumbering();
49 instance.UI.NotesPanel.enableAttachmentPreview();
50 // Add the demo-specific functionality
51 customizeUI(instance).then(() => {
52 // Create UI controls after demo is initialized
53 UIElements.createUIControls(instance);
54 });
55 });
56}
57
58const searchParams = new URLSearchParams(window.location.search);
59const history = window.history || window.parent.history || window.top.history;
60
61// Starting page for extraction
62let startPage = 1;
63
64// Global variable to hold result data
65window.resultData = null;
66
67const defaultDoc = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/cad_floor_plan.pdf';
68
69const customizeUI = async (instance) => {
70 // Customize the UI for the title-block-data-extraction demo
71 instance.UI.setToolbarGroup('toolbarGroup-View');
72 instance.UI.disableElements(['thumbnailControl']);
73
74 // Reset variables when new document is loaded
75 instance.Core.documentViewer.addEventListener('documentLoaded', async () => {
76 window.resultData = null;
77 startPage = 1;
78
79 // Reset the JSON display area and Color Legend
80 UIElements.resetUI(instance);
81 });
82
83 // Load the default CAD file for demonstration
84 if (defaultDoc) {
85 await loadCadDocument(instance, defaultDoc);
86 }
87};
88
89// Load a CAD document, converting to PDF if necessary
90const loadCadDocument = async (instance, cadUrl) => {
91 // Get the file name and extension
92 const cadFilename = cadUrl.split('/').pop();
93 const extension = cadFilename.split('.').pop().toLowerCase();
94
95 console.log(`Preparing to load document: ${cadFilename}`);
96 console.log(`File extension detected: ${extension}`);
97
98 // If the file is a CAD format, convert it to PDF first
99 if (cadUrl && extension) {
100 if (['dwg', 'dxf', 'dgn', 'rvt'].includes(extension)) {
101 console.log(`Loading CAD file: ${cadFilename}`);
102 const response = await fetch(cadUrl);
103 if (!response.ok) {
104 throw new Error(`Failed to fetch CAD: ${response.status}`);
105 }
106 const cadBuffer = await response.arrayBuffer();
107 const pdfBuffer = await convertCadtoPdf(cadBuffer, cadFilename);
108 instance.UI.loadDocument(pdfBuffer, {
109 extension: 'pdf',
110 });
111 } else {
112 console.log(`Loading document: ${cadFilename}`);
113 instance.UI.loadDocument(cadUrl);
114 }
115 }
116};
117
118// Function to convert CAD ArrayBuffer to PDF ArrayBuffer via server
119const convertCadtoPdf = async (cadBuffer, cadFilename) => {
120 // Send the CAD to the server to be converted to PDF
121 console.log('Sending CAD to server for conversion...');
122 const cadBlob = new Blob([cadBuffer]);
123 const formData = new FormData();
124 formData.append('cadfile', cadBlob, cadFilename);
125
126 const postResponse = await fetch('http://localhost:5050/server/handler.js', {
127 method: 'POST',
128 body: formData,
129 });
130
131 if (postResponse.status !== 200) {
132 throw new Error(`Server error during CAD upload: ${postResponse.status}`);
133 }
134 const buffer = await postResponse.arrayBuffer();
135 return buffer;
136};
137window.convertCadtoPdf = convertCadtoPdf; // Make convertCadtoPdf globally available so that the UIElements module can access it
138
139// Function to extract key-value pairs from title block via server
140const extractKeyValuePairs = async (instance) => {
141 const doc = instance.Core.documentViewer.getDocument();
142 if (doc) {
143 const pdfBuffer = await doc.getFileData({ flags: instance.Core.SaveOptions.LINEARIZED });
144 console.log('Sending PDF to server for key-value extraction...');
145 const pdfBlob = new Blob([pdfBuffer], { type: 'application/pdf' });
146 const formData = new FormData();
147 formData.append('pdffile', pdfBlob, 'viewerDocument.pdf');
148
149 // Send the PDF to the server to extract key-value pairs
150 const postResponse = await fetch('http://localhost:5050/server/handler.js/extract-key-value-pairs', {
151 method: 'POST',
152 body: formData,
153 });
154
155 if (postResponse.status !== 200) {
156 throw new Error(`Server error during PDF upload: ${postResponse.status}`);
157 }
158
159 // Retrieve and parse the JSON response
160 const jsonResponse = await postResponse.json();
161 const docStructureData = JSON.parse(jsonResponse);
162 resultData = JSON.stringify(docStructureData, null, 2);
163
164 // Draw annotations on the document based on extracted data
165 drawAnnotations(docStructureData, instance);
166
167 return;
168 }
169}
170window.extractKeyValuePairs = extractKeyValuePairs; // Make extractKeyValuePairs globally available so that the UIElements module can access it
171
172// Function to draw annotations on the document based on extracted key-value data
173const drawAnnotations = (docStructureData, instance) => {
174 const { annotationManager, Annotations } = instance.Core;
175
176 // Retrieve the first page's data
177 const page = docStructureData.pages[startPage - 1];
178 const pageNumber = page?.properties?.pageNumber;
179 console.log(`Processing Page ${pageNumber} for annotations...`);
180 for (const kv of page.keyValueElements ?? []) {
181 const valueRect = kv?.rect;
182 const keyRect = kv?.key?.rect;
183 const hasValueWords = (kv?.words?.length ?? 0) > 0;
184
185 // Only draw if value has words
186 if (!hasValueWords) continue;
187
188 // value: blue
189 const valueAnnot = new Annotations.RectangleAnnotation({
190 PageNumber: pageNumber,
191 X: valueRect[0],
192 Y: valueRect[1],
193 Width: valueRect[2] - valueRect[0],
194 Height: valueRect[3] - valueRect[1],
195 StrokeColor: new Annotations.Color(0, 0, 255),
196 StrokeThickness: 1,
197 });
198 annotationManager.addAnnotation(valueAnnot);
199 annotationManager.redrawAnnotation(valueAnnot);
200
201 // key: red
202 const keyAnnot = new Annotations.RectangleAnnotation({
203 PageNumber: pageNumber,
204 X: keyRect[0],
205 Y: keyRect[1],
206 Width: keyRect[2] - keyRect[0],
207 Height: keyRect[3] - keyRect[1],
208 StrokeColor: new Annotations.Color(255, 0, 0),
209 StrokeThickness: 1,
210 });
211 annotationManager.addAnnotation(keyAnnot);
212 annotationManager.redrawAnnotation(keyAnnot);
213
214 // Green connector
215 const line = new Annotations.LineAnnotation();
216 line.pageNumber = pageNumber;
217 line.StrokeColor = new Annotations.Color(0, 255, 0);
218 line.StrokeThickness = 1;
219 line.Start = topLeftPoint(valueRect, instance);
220 line.End = topLeftPoint(keyRect, instance);
221 annotationManager.addAnnotation(line);
222 annotationManager.redrawAnnotation(line);
223 }
224};
225
226// Helper function to get top-left point of a rectangle
227const topLeftPoint = ([x1, y1, x2, y2], instance) => {
228 return new instance.Core.Math.Point(Math.min(x1, x2), Math.min(y1, y2));
229};
230
231// Cleanup function for when the demo is closed or page is unloaded
232const cleanup = (instance) => {
233 if (typeof instance !== 'undefined' && instance.UI) {
234 if (instance.Core.documentViewer.getDocument()) {
235 // Insert any other cleanup code here
236 }
237 console.log('Cleaning up title-block-data-extraction demo');
238 }
239};
240
241// Register cleanup for page unload
242window.addEventListener('beforeunload', () => cleanup(instance));
243window.addEventListener('unload', () => cleanup(instance));
244
245// Helper function to load the ui-elements.js script
246function loadUIElementsScript() {
247 return new Promise((resolve, reject) => {
248 if (window.UIElements) {
249 console.log('UIElements already loaded');
250 resolve();
251 return;
252 }
253 const script = document.createElement('script');
254 script.src = '/showcase-demos/title-block-data-extraction/client/ui-elements.js';
255 script.onload = function () {
256 console.log('✅ UIElements script loaded successfully');
257 resolve();
258 };
259 script.onerror = function () {
260 console.error('Failed to load UIElements script');
261 reject(new Error('Failed to load ui-elements.js'));
262 };
263 document.head.appendChild(script);
264 });
265}
266
267// Load UIElements script first, then initialize WebViewer
268loadUIElementsScript().then(() => {
269 initializeWebViewer();
270}).catch((error) => {
271 console.error('Failed to load UIElements:', error);
272});
273
1/* CSS standards Compliant Syntax */
2/* GitHub Copilot v1.0, Claude Sonnet 4, October 22, 2025 */
3/* File: index.css */
4
5/* Button Styles */
6.btn {
7 display: flex;
8 align-items: center;
9 justify-content: center;
10 background-color: #007bff;
11 margin: 10px;
12 padding: 5px 10px;
13 border: 1px solid #ccc;
14 border-radius: 4px;
15 cursor: pointer;
16 font-size: 14px;
17 font-weight: bold;
18 transition: all 0.2s ease;
19 box-shadow: 0 2px 4px rgba(0,0,0,0.1);
20 color: white;
21 width: 240px;
22}
23
24.btn:hover {
25 background-color: #0056b3;
26 transform: translateY(-1px);
27 box-shadow: 0 4px 8px rgba(0,0,0,0.2);
28}
29
30.btn:active {
31 transform: translateY(1px);
32 box-shadow: 0 1px 2px rgba(0,0,0,0.2);
33}
34
35.btn:disabled {
36 opacity: 0.4;
37 cursor: not-allowed;
38 box-shadow: none;
39 background-color: #E7EBEE;
40 color: #ADB5BD;
41}
42
43.btn .spinner {
44 display: none;
45 border-top: 2px solid currentColor;
46 border-right: 2px solid currentColor;
47 border-bottom-style: solid;
48 border-left-style: solid;
49 border-radius: 99999px;
50 border-bottom-width: 2px;
51 border-left-width: 2px;
52 border-bottom-color: transparent;
53 border-left-color: transparent;
54 animation: rotateBorder 0.45s linear 0s infinite;
55 width: 1em;
56 height: 1em;
57}
58
59@keyframes rotateBorder {
60 0% {
61 transform: rotate(0deg);
62 }
63 100% {
64 transform: rotate(365deg);
65 }
66}
67
68/* Button Container */
69.button-container {
70 display: flex;
71 flex-direction: row;
72 align-items: center;
73 gap: 15px;
74 margin: 5px 0;
75 padding: 16px;
76 padding-bottom: 5px;
77 border-bottom: 1px solid #DFE1E6;
78 background-color: rgba(112, 198, 255, 0.2);
79}
80
81/* JSON Display Container */
82.json-pre {
83 height: 90%;
84 font-family: monospace;
85 white-space: pre-wrap;
86 display: block;
87 overflow: scroll;
88 background-color: #f1f3f5;
89}
90
91.json-wrapper {
92 width: 100%;
93 max-width: 100%;
94 box-sizing: border-box;
95}
96
97.json-container {
98 display: flex;
99 min-height: 140px;
100 max-height: 200px;
101 width: 100%;
102 max-width: 100%;
103 border-radius: 2px;
104 border: 1px solid rgba(0, 0, 0, 0.12);
105 overflow-y: auto;
106 overflow-x: auto;
107 flex-grow: 1;
108 position: relative;
109 padding-bottom: 2px;
110 background-color: rgb(244, 245, 247);
111 box-sizing: border-box;
112}
113
114.json-container .json-pre {
115 font-family: monospace;
116 white-space: pre-wrap;
117 width: 100%;
118 max-width: 100%;
119 margin: 0;
120 padding: 8px;
121 box-sizing: border-box;
122 overflow-wrap: break-word;
123}
124
125#json-code {
126 width: 100%;
127 max-width: 100%;
128 display: block;
129 box-sizing: border-box;
130 background: transparent;
131 border: none;
132 outline: none;
133 resize: none;
134 font-family: inherit;
135}
136
137/* Legend Container */
138.legend-container {
139 display: none;
140 flex-direction: row;
141 gap: 10px;
142}
143
144.legend-item {
145 display: flex;
146 align-items: center;
147 gap: 5px;
148}
149
150.color-box {
151 display: inline-block;
152 width: 16px;
153 height: 16px;
154 border: 1px solid #ccc;
155 border-radius: 3px;
156}
1// ES6 Compliant Syntax
2// GitHub Copilot v1.0, Claude Sonnet 4, October 22, 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
12 // Function to check if the user is on a mobile device
13 static isMobileDevice = () => {
14 return (
15 /(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(
16 window.navigator.userAgent
17 ) ||
18 /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(
19 window.navigator.userAgent.substring(0, 4)
20 )
21 );
22 }
23
24 // Choose File button
25 static filePicker = (instance) => {
26 const button = document.createElement('button');
27 button.className = 'btn';
28 button.textContent = 'Choose File';
29 button.onclick = () => {
30 const input = document.createElement('input');
31 input.type = 'file';
32 input.accept = '.dwg,.dxf,.dwf,.dgn,.rvt,.pdf'; // Supported CAD file formats
33 input.onchange = async (event) => {
34 try {
35 const file = event.target.files[0];
36 const extension = file.name.split('.').pop().toLowerCase();
37 if (file && ['dwg', 'dxf', 'dgn', 'rvt'].includes(extension)) {
38 const arrayBuffer = await file.arrayBuffer();
39 const pdfBuffer = await window.convertCadtoPdf(arrayBuffer, file.name);
40 instance.UI.loadDocument(pdfBuffer, {
41 extension: 'pdf',
42 });
43 } else if (file && extension === 'pdf') {
44 instance.UI.loadDocument(file);
45 } else {
46 alert('Unsupported file format. Please select a CAD or PDF file.');
47 }
48 } catch (e) {
49 console.error(e);
50 }
51 };
52 input.click();
53 };
54
55 return button;
56 }
57
58 // JSON Code Block Element
59 static jsonElement = () => {
60 const wrapper = document.createElement('div');
61 wrapper.className = 'json-wrapper';
62 wrapper.style.display = 'none'; // Initially hidden
63
64 // Container for the JSON code block
65 const container = document.createElement('div');
66 container.className = 'json-container';
67
68 // Code block for JSON
69 const codePre = document.createElement('pre');
70 codePre.className = 'json-pre';
71
72 const codeBlock = document.createElement('code');
73 codeBlock.id = 'json-code';
74 codeBlock.contentEditable = false;
75
76 // Assemble the JSON code block
77 codePre.appendChild(codeBlock);
78 container.appendChild(codePre);
79 wrapper.appendChild(container);
80 return wrapper;
81 };
82
83 // Extract Key Value Pairs button
84 static extractKeyValuePairsButton = (instance) => {
85 // Spinner element to indicate loading
86 const spinner = document.createElement('div');
87 spinner.className = 'spinner';
88
89 // Button element for extracting key-value pairs
90 const button = document.createElement('button');
91 button.className = 'btn extract-btn';
92 button.textContent = 'Extract Key-Value Pairs';
93 button.onclick = async () => {
94 try {
95 button.disabled = true;
96 spinner.style.display = 'inline-block';
97 await window.extractKeyValuePairs(instance);
98 } catch (e) {
99 console.error(e);
100 button.disabled = false;
101 }
102 finally {
103 // Hide spinner when done
104 spinner.style.display = 'none';
105
106 // Display the extracted JSON data
107 const jsonWrapper = document.querySelector('.json-wrapper');
108 const jsonCodeBlock = document.getElementById('json-code');
109 if (window.resultData) {
110 jsonCodeBlock.textContent = JSON.stringify(JSON.parse(window.resultData), null, 2);
111 jsonWrapper.style.display = 'flex';
112 } else {
113 jsonCodeBlock.textContent = '';
114 jsonWrapper.style.display = 'none';
115 }
116
117 // Show color legend if extraction was successful
118 const legendContainer = document.querySelector('.legend-container');
119 if (window.resultData) {
120 legendContainer.style.display = 'flex';
121 } else {
122 legendContainer.style.display = 'none';
123 }
124 }
125 };
126
127 button.appendChild(spinner);
128
129 return button;
130 }
131
132 // Legends for the Annotations
133 static colorLegend = () => {
134 const legendContainer = document.createElement('div');
135 legendContainer.className = 'legend-container';
136
137 const colors = ['rgb(255, 0, 0)', 'rgb(0, 0, 255)', 'rgb(0, 255, 0)'];
138 const labels = ['Key', 'Value', 'Connector'];
139
140 for (let i = 0; i < colors.length; i++) {
141 const legendItem = document.createElement('div');
142 legendItem.className = 'legend-item';
143
144 const colorBox = document.createElement('span');
145 colorBox.className = 'color-box';
146 colorBox.style.backgroundColor = colors[i];
147
148 const label = document.createElement('span');
149 label.textContent = labels[i];
150
151 legendItem.appendChild(colorBox);
152 legendItem.appendChild(label);
153 legendContainer.appendChild(legendItem);
154 }
155
156 return legendContainer;
157 }
158
159 // Reset JSON Code Block and Legend
160 static resetUI = () => {
161 // Hide JSON code block
162 const jsonWrapper = document.querySelector('.json-wrapper');
163 const jsonCodeBlock = document.getElementById('json-code');
164 jsonCodeBlock.textContent = '';
165 jsonWrapper.style.display = 'none';
166
167 // Hide legend
168 const legendContainer = document.querySelector('.legend-container');
169 legendContainer.style.display = 'none';
170
171 // Enable Extract button
172 const extractButton = document.querySelector('.extract-btn');
173 extractButton.disabled = false;
174 }
175
176 static createUIControls = (instance) => {
177 // Create a container for all controls
178 const controlsContainer = document.createElement('div');
179 controlsContainer.className = 'button-container';
180
181 // Add the file picker and Import/Export buttons to the controls container
182 controlsContainer.appendChild(this.filePicker(instance));
183 controlsContainer.appendChild(this.extractKeyValuePairsButton(instance));
184 controlsContainer.appendChild(this.colorLegend());
185
186 // Add the controls container to the viewer element
187 const element = document.getElementById('viewer');
188 element.insertBefore(this.jsonElement(), element.firstChild);
189 element.insertBefore(controlsContainer, element.firstChild);
190 };
191}
1// ES6 Compliant Syntax
2// GitHub Copilot v1.0, Claude Sonnet 4, October 22, 2025
3// File: handler.js
4// This file will handle CAD file conversion and extraction 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 storage = multer.diskStorage({
18 destination: function (req, file, cb) {
19 cb(null, 'sentFiles/')
20 },
21 filename: function (req, file, cb) {
22 // Save with original filename and extension
23 cb(null, file.originalname)
24 }
25});
26const upload = multer({ storage: storage });
27const { response } = require('express');
28const e = require('express');
29const serverFolder = 'server';
30const sentFiles = 'sentFiles';
31const serverHandler = `/${serverFolder}/handler.js`;
32
33module.exports = async (app) => {
34
35 // Function to initialize PDFNet and check for module availability
36 async function initializePDFNet() {
37 // Create folder sentFiles that will hold the sent CAD format files, if it doesn't exist
38 if (!fs.existsSync(sentFiles))
39 fs.mkdirSync(sentFiles);
40
41 // Initialize PDFNet
42 await PDFNet.initialize(licenseKey);
43
44 // Specify the PDFTron CAD and Data Extraction library path
45 await PDFNet.addResourceSearchPath('./node_modules/@pdftron/cad/lib/');
46 await PDFNet.addResourceSearchPath('./node_modules/@pdftron/data-extraction/lib/');
47
48 // Check if the Apryse SDK CAD module is available.
49 if (await PDFNet.CADModule.isModuleAvailable())
50 console.log('Apryse SDK CAD module is available.');
51 else
52 console.log('Unable to run: Apryse SDK CAD module not available.');
53
54 // Check if the Apryse SDK Data Extraction module is available.
55 if (await PDFNet.DataExtractionModule.isModuleAvailable(PDFNet.DataExtractionModule.DataExtractionEngine.e_GenericKeyValue))
56 console.log('Apryse SDK Data Extraction module is available.');
57 else
58 console.log('Unable to run: Apryse SDK Data Extraction module not available.');
59 }
60
61 // Handle POST request sent to '/server/handler.js'
62 // This endpoint receives the CAD file URL to be loaded in the Apryse webviewer, then saves it to the server
63 app.post(serverHandler, upload.single('cadfile'), async (request, response) => {
64 try {
65 const cadFilename = request.file.originalname;
66 const fullFilename = request.file.path;
67
68 // Convert the CAD file to PDF and get the buffer
69 const buffer = await convertCadToPdfBuffer(fullFilename);
70 console.log(`Conversion complete, extracting title block data...`);
71
72 // Set headers to indicate a PDF file attachment and send the buffer
73 await response.setHeader('Content-Type', 'application/pdf');
74 await response.setHeader('Content-Disposition', `attachment; filename="${cadFilename.replace(/\.[^/.]+$/, ".pdf")}"`);
75 response.status(200).send(buffer);
76 } catch (e) {
77 response.status(500).send(`Error processing CAD file: ${e.message}`);
78 } finally {
79 // Cleanup: remove the sent CAD file
80 const cadPath = request.file.path;
81 fs.unlink(cadPath, (err) => {
82 if (err) {
83 console.error(`Error removing CAD file ${cadPath}: ${err.message}`);
84 }
85 });
86 }
87 });
88
89 // Function to convert CAD file to PDF and return as buffer
90 const convertCadToPdfBuffer = async (fullFilename) => {
91 try {
92 // Create a new PDF document and convert the CAD file to PDF
93 const doc = await PDFNet.PDFDoc.create();
94 console.log('Converting CAD to PDF. Filename and Extension:', fullFilename);
95
96 const options = new PDFNet.Convert.CADConvertOptions();
97 options.setPageWidth(800);
98 options.setPageHeight(600);
99 options.setRasterDPI(150);
100
101 await PDFNet.Convert.fromCAD(doc, fullFilename, options);
102
103 // Initialize security handler and lock the document
104 doc.initSecurityHandler();
105 doc.lock();
106
107 // Save the PDF document to a memory buffer
108 console.log('After Conversion and Stored in PDFDoc Full filename:', doc.fullFilename);
109 const uint8Array = await doc.saveMemoryBuffer(PDFNet.SDFDoc.SaveOptions.e_linearized);
110 const buffer = Buffer.from(uint8Array);
111
112 // Unlock the document
113 doc.unlock();
114
115 // Return the PDF buffer
116 return buffer;
117 }
118 catch (err) {
119 console.log(err);
120 throw new Error(err);
121 }
122 };
123
124 // Handle POST request sent to '/server/handler.js/extract-key-value-pairs'
125 // This endpoint receives the PDF file path, extracts key-value data from the title block, and returns it as JSON
126 app.post(`${serverHandler}/extract-key-value-pairs`, upload.single('pdffile'), async (request, response) => {
127 try {
128 console.log('Received PDF for key-value extraction');
129 const pdfPath = request.file.path;
130 const jsonResponse = await extractKeyValuePairs(pdfPath, 'title_block_template.json');
131 response.status(200).json(jsonResponse);
132 } catch (error) {
133 console.error('Error extracting key-value data:', error);
134 response.status(500).send('Error extracting key-value data');
135 } finally {
136 // Cleanup: remove the sent PDF file
137 const pdfPath = request.file.path;
138 fs.unlink(pdfPath, (err) => {
139 if (err) {
140 console.error(`Error removing PDF file ${pdfPath}: ${err.message}`);
141 }
142 });
143 }
144 });
145
146 // Function to extract key-value pairs from PDF using Data Extraction module
147 const extractKeyValuePairs = async (pdf) => {
148 try {
149 // Set up data extraction options
150 const options = new PDFNet.DataExtractionModule.DataExtractionOptions();
151 console.log('Setting extraction language to English');
152 options.setLanguage('eng');
153
154 // Extract key-value data from the PDF using the provided JSON template
155 const jsonString = await PDFNet.DataExtractionModule.extractDataAsString(pdf, PDFNet.DataExtractionModule.DataExtractionEngine.e_GenericKeyValue, options);
156 return jsonString;
157
158 } catch (err) {
159 console.log(err);
160 throw new Error(err);
161 }
162 };
163
164 // Initialize PDFNet
165 PDFNet.runWithoutCleanup(initializePDFNet, licenseKey).then(
166 function onFulfilled() {
167 response.status(200);
168 },
169 function onRejected(error) {
170 // log error and close response
171 console.error('Error initializing PDFNet', error);
172 response.status(503).send();
173 }
174 );
175};
176
1// ES6 Compliant Syntax
2// GitHub Copilot v1.0, Claude Sonnet 4, October 22, 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.8.0",
12 "@pdftron/data-extraction": "^11.8.0",
13 "@pdftron/pdfnet-node": "^11.8.0",
14 "body-parser": "^1.20.2",
15 "express": "^4.18.2",
16 "multer": "^1.4.4",
17 "open": "^9.1.0"
18 },
19 "keywords": [
20 "cad-viewer",
21 "pdf",
22 "server",
23 "pdftron",
24 "webviewer"
25 ],
26 "author": "Apryse",
27 "license": "MIT"
28}
29
Did you find this helpful?
Trial setup questions?
Ask experts on DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales