> For the complete documentation index, see [llms.txt](https://docs.apryse.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.apryse.com/web/get-started/samples/showcase-demos-title-block-extraction.md).

# CAD Title Block Data Extraction Showcase Demo Code Sample

Extract key-value data pairs from PDF format CAD drawings with well-formed title blocks.

{% hint style="info" %}
**Requirements**

*These packages are required to use these features in production. Trial keys have unlimited access to all features*

<a href="/web/get-started/readme.md" class="button primary">Web SDK</a><a href="/core/get-started/get-started.md" class="button primary">Server SDK</a><a href="https://apryse.com/capabilities#SmartDataExtraction" class="button primary">Package: Smart Data Extraction</a><a href="https://showcase.apryse.com/title-block-extraction" class="button primary">Live demo</a>
{% endhint %}

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:

* Upload a PDF file.
* Extract key-value pairs from title blocks and export the data as JSON.
* Automatically generate visual annotations to highlight key-value relationships.
* Download the annotated document.

If starting with a CAD file, we can also help you convert that to PDF. Please check out our [CAD File Conversion Sample Code](/core/get-started/samples/cad2pdftest.md) for more.

### **Implementation steps**

To add CAD Title Block Data Extraction capability with WebViewer: Step 1: [Get started with WebViewer](/web/get-started/readme.md) 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-license-key/apryse-license-key platform="WEB\_VIEWER" variant="compact" %}

{% tabs %}
{% tab title="index.js" %}

<pre class="language-js" data-line-numbers><code class="lang-js">// ES6 Compliant Syntax
// GitHub Copilot v1.0, Claude Sonnet 4, October 22, 2025
// File: index.js

import WebViewer from '@pdftron/webviewer';

const licenseKey = '<code class="expression">visitor.claims.wvKey || "YOUR_WEBVIEWER_LICENSE_KEY"</code>';

// CAD Title Block Data Extraction Demo
// 
// This code demonstrates how to extract key-value data pairs from CAD drawings with well formed title blocks
//
// **Important** 
// 1. You must get a license key from Apryse for the server to run. 
// A trial key can be obtained from:
// https://docs.apryse.com/core/guides/get-started/trial-key
//
// 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.

function initializeWebViewer() {

    // This code initializes the WebViewer with the basic settings
    WebViewer({
        path: '/lib',
        licenseKey: licenseKey,
    }, document.getElementById('viewer')).then((instance) => {
        // Enable the measurement toolbar so it appears with all the other tools, and disable Cloudy rectangular tool
        const cloudyTools = [
            instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT,
            instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT2,
            instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT3,
            instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT4,
        ];
        instance.UI.enableFeatures([instance.UI.Feature.Measurement, instance.UI.Feature.Initials]);
        instance.UI.disableTools(cloudyTools);
        // Set default toolbar group to Annotate
        instance.UI.setToolbarGroup('toolbarGroup-Annotate');
        // Set default tool on mobile devices to Pan.
        if (UIElements.isMobileDevice()) {
            instance.UI.setToolMode(instance.Core.Tools.ToolNames.PAN);
        }

        instance.Core.documentViewer.addEventListener('documentUnloaded', () => {
            if (searchParams.has('file')) {
                searchParams.delete('file');
                history.replaceState(null, '', '?' + searchParams.toString());
            }
        });

        instance.Core.annotationManager.enableAnnotationNumbering();
        instance.UI.NotesPanel.enableAttachmentPreview();
        // Add the demo-specific functionality
        customizeUI(instance).then(() => {
            // Create UI controls after demo is initialized
            UIElements.createUIControls(instance);
        });
    });
}

const searchParams = new URLSearchParams(globalThis.location.search);
const history = globalThis.history || globalThis.parent?.history || globalThis.top?.history;

// Starting page for extraction
let startPage = 1;

// Global variable to hold result data
globalThis.resultData = null;

const defaultDoc = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/cad_floor_plan.pdf';

const customizeUI = async (instance) => {
    // Customize the UI for the title-block-data-extraction demo
    instance.UI.setToolbarGroup('toolbarGroup-View');
    instance.UI.disableElements(['thumbnailControl']);

    // Reset variables when new document is loaded
    instance.Core.documentViewer.addEventListener('documentLoaded', async () => {
        globalThis.resultData = null;
        startPage = 1;

        // Reset the JSON display area and Color Legend
        UIElements.resetUI(instance);
    });

    // Load the default CAD file for demonstration
    if (defaultDoc) {
        await loadCadDocument(instance, defaultDoc);
    }
};

// Load a CAD document, converting to PDF if necessary
const loadCadDocument = async (instance, cadUrl) => {
    // Get the file name and extension
    const cadFilename = cadUrl.split('/').pop();
    const extension = cadFilename.split('.').pop().toLowerCase();

    console.log(`Preparing to load document: ${cadFilename}`);
    console.log(`File extension detected: ${extension}`);

    // If the file is a CAD format, convert it to PDF first
    if (cadUrl &#x26;&#x26; extension) {
        if (['dwg', 'dxf', 'dgn', 'rvt'].includes(extension)) {
            console.log(`Loading CAD file: ${cadFilename}`);
            const response = await fetch(cadUrl);
            if (!response.ok) {
                throw new Error(`Failed to fetch CAD: ${response.status}`);
            }
            const cadBuffer = await response.arrayBuffer();
            const pdfBuffer = await convertCadtoPdf(cadBuffer, cadFilename);
            instance.UI.loadDocument(pdfBuffer, {
                extension: 'pdf',
            });
        } else {
            console.log(`Loading document: ${cadFilename}`);
            instance.UI.loadDocument(cadUrl);
        }
    }
};

// Function to convert CAD ArrayBuffer to PDF ArrayBuffer via server
const convertCadtoPdf = async (cadBuffer, cadFilename) => {
    // Send the CAD to the server to be converted to PDF
    console.log('Sending CAD to server for conversion...');
    const cadBlob = new Blob([cadBuffer]);
    const formData = new FormData();
    formData.append('cadfile', cadBlob, cadFilename);

    const postResponse = await fetch('http://localhost:5050/server/handler.js', {
        method: 'POST',
        body: formData,
    });

    if (postResponse.status !== 200) {
        throw new Error(`Server error during CAD upload: ${postResponse.status}`);
    }
    const buffer = await postResponse.arrayBuffer();
    return buffer;
};
globalThis.convertCadtoPdf = convertCadtoPdf; // Make convertCadtoPdf globally available so that the UIElements module can access it

// Function to extract key-value pairs from title block via server
const extractKeyValuePairs = async (instance) => {
    const doc = instance.Core.documentViewer.getDocument();
    if (doc) {
        const pdfBuffer = await doc.getFileData({ flags: instance.Core.SaveOptions.LINEARIZED });
        console.log('Sending PDF to server for key-value extraction...');
        const pdfBlob = new Blob([pdfBuffer], { type: 'application/pdf' });
        const formData = new FormData();
        formData.append('pdffile', pdfBlob, 'viewerDocument.pdf');

        // Send the PDF to the server to extract key-value pairs
        const postResponse = await fetch('http://localhost:5050/server/handler.js/extract-key-value-pairs', {
            method: 'POST',
            body: formData,
        });

        if (postResponse.status !== 200) {
            throw new Error(`Server error during PDF upload: ${postResponse.status}`);
        }

        // Retrieve and parse the JSON response
        const jsonResponse = await postResponse.json();
        const docStructureData = JSON.parse(jsonResponse);
        globalThis.resultData = JSON.stringify(docStructureData, null, 2);

        // Draw annotations on the document based on extracted data
        drawAnnotations(docStructureData, instance);
    }
}
globalThis.extractKeyValuePairs = extractKeyValuePairs; // Make extractKeyValuePairs globally available so that the UIElements module can access it

// Function to draw annotations on the document based on extracted key-value data
const drawAnnotations = (docStructureData, instance) => {
    const { annotationManager, Annotations } = instance.Core;

    // Retrieve the first page's data
    const page = docStructureData.pages[startPage - 1];
    const pageNumber = page?.properties?.pageNumber;
    console.log(`Processing Page ${pageNumber} for annotations...`);
    for (const kv of page.keyValueElements ?? []) {
        const valueRect = kv?.rect;
        const keyRect = kv?.key?.rect;
        const hasValueWords = (kv?.words?.length ?? 0) > 0;

        // Only draw if value has words
        if (!hasValueWords) continue;

        // value: blue
        const valueAnnot = new Annotations.RectangleAnnotation({
            PageNumber: pageNumber,
            X: valueRect[0],
            Y: valueRect[1],
            Width: valueRect[2] - valueRect[0],
            Height: valueRect[3] - valueRect[1],
            StrokeColor: new Annotations.Color(0, 0, 255),
            StrokeThickness: 1,
        });
        annotationManager.addAnnotation(valueAnnot);
        annotationManager.redrawAnnotation(valueAnnot);

        // key: red
        const keyAnnot = new Annotations.RectangleAnnotation({
            PageNumber: pageNumber,
            X: keyRect[0],
            Y: keyRect[1],
            Width: keyRect[2] - keyRect[0],
            Height: keyRect[3] - keyRect[1],
            StrokeColor: new Annotations.Color(255, 0, 0),
            StrokeThickness: 1,
        });
        annotationManager.addAnnotation(keyAnnot);
        annotationManager.redrawAnnotation(keyAnnot);

        // Green connector
        const line = new Annotations.LineAnnotation();
        line.pageNumber = pageNumber;
        line.StrokeColor = new Annotations.Color(0, 255, 0);
        line.StrokeThickness = 1;
        line.Start = topLeftPoint(valueRect, instance);
        line.End = topLeftPoint(keyRect, instance);
        annotationManager.addAnnotation(line);
        annotationManager.redrawAnnotation(line);
    }
};

// Helper function to get top-left point of a rectangle
const topLeftPoint = ([x1, y1, x2, y2], instance) => {
    return new instance.Core.Math.Point(Math.min(x1, x2), Math.min(y1, y2));
};

// Cleanup function for when the demo is closed or page is unloaded
const cleanup = (instance) => {
    if (instance !== undefined &#x26;&#x26; instance.UI) {
        if (instance.Core.documentViewer.getDocument()) {
            // Insert any other cleanup code here
        }
        console.log('Cleaning up title-block-data-extraction demo');
    }
};

// Register cleanup for page unload
globalThis.addEventListener('beforeunload', () => cleanup(instance));
globalThis.addEventListener('unload', () => cleanup(instance));

// Helper function to load the ui-elements.js script
function loadUIElementsScript() {
    return new Promise((resolve, reject) => {
        if (globalThis.UIElements) {
            console.log('UIElements already loaded');
            resolve();
            return;
        }
        const script = document.createElement('script');
        script.src = '/showcase-demos/title-block-extraction/client/ui-elements.js';
        script.onload = function () {
            console.log('✅ UIElements script loaded successfully');
            resolve();
        };
        script.onerror = function () {
            console.error('Failed to load UIElements script');
            reject(new Error('Failed to load ui-elements.js'));
        };
        document.head.appendChild(script);
    });
}

// Load UIElements script first, then initialize WebViewer
loadUIElementsScript().then(() => {
    initializeWebViewer();
}).catch((error) => {
    console.error('Failed to load UIElements:', error);
});

</code></pre>

{% endtab %}

{% tab title="index.css" %}
{% code title="index.css" lineNumbers="true" %}

```css
/* CSS standards Compliant Syntax */
/* GitHub Copilot v1.0, Claude Sonnet 4, October 22, 2025 */
/* File: index.css */

/* Button Styles */
.btn {
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: #0056b3;
    margin: 10px;
    padding: 5px 10px;
    border: 1px solid #ccc;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
    font-weight: bold;
    transition: all 0.2s ease;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    color: #ffffff;
    width: 240px;
}

.btn:hover {
    background-color: #003d80;
    transform: translateY(-1px);
    box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}

.btn:active {
    transform: translateY(1px);
    box-shadow: 0 1px 2px rgba(0,0,0,0.2);
}

.btn:disabled {
    opacity: 0.4;
    cursor: not-allowed;
    box-shadow: none;
    background-color: #E7EBEE;
    color: #495057;
}

.btn .spinner {
    display: none;
    border-top: 2px solid currentColor;
    border-right: 2px solid currentColor;
    border-bottom-style: solid;
    border-left-style: solid;
    border-radius: 99999px;
    border-bottom-width: 2px;
    border-left-width: 2px;
    border-bottom-color: transparent;
    border-left-color: transparent;
    animation: rotateBorder 0.45s linear 0s infinite;
    width: 1em;
    height: 1em;    
}

@keyframes rotateBorder {
    0% {
        transform: rotate(0deg);
    }
    100% {
        transform: rotate(365deg);
    }
}

/* Button Container */
.button-container {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 15px;
    margin: 5px 0;
    padding: 16px;
    padding-bottom: 5px;
    border-bottom: 1px solid #DFE1E6;
    background-color: rgba(112, 198, 255, 0.2);
}

/* JSON Display Container */
.json-pre {
    height: 90%;
    font-family: monospace;
    white-space: pre-wrap;
    display: block;
    overflow: scroll;
    background-color: #f1f3f5;
}

.json-wrapper {
    width: 100%;
    max-width: 100%;
    box-sizing: border-box;
}

.json-container {
    display: flex;
    min-height: 140px;
    max-height: 200px;
    width: 100%;
    max-width: 100%;
    border-radius: 2px;
    border: 1px solid rgba(0, 0, 0, 0.12);
    overflow-y: auto;
    overflow-x: auto;
    flex-grow: 1;
    position: relative;
    padding-bottom: 2px;
    background-color: rgb(244, 245, 247);
    box-sizing: border-box;
}

.json-container .json-pre {
    font-family: monospace;
    white-space: pre-wrap;
    width: 100%;
    max-width: 100%;
    margin: 0;
    padding: 8px;
    box-sizing: border-box;
    overflow-wrap: break-word;
}

#json-code {
    width: 100%;
    max-width: 100%;
    display: block;
    box-sizing: border-box;
    background: transparent;
    border: none;
    outline: none;
    resize: none;
    font-family: inherit;
}

/* Legend Container */
.legend-container {
    display: none;
    flex-direction: row;
    gap: 10px;
}

.legend-item {
    display: flex;
    align-items: center;
    gap: 5px;
}

.color-box {
    display: inline-block;
    width: 16px;
    height: 16px;
    border: 1px solid #ccc;
    border-radius: 3px;
}
```

{% endcode %}
{% endtab %}

{% tab title="ui-elements.js" %}
{% code title="ui-elements.js" lineNumbers="true" %}

```js
// ES6 Compliant Syntax
// GitHub Copilot v1.0, Claude Sonnet 4, October 22, 2025
// File: ui-elements.js

// UI Elements class to create and manage custom UI controls
//
// Helper code to add controls to the viewer holding the buttons
// This code creates a container for the buttons, styles them, and adds them to the viewer
//
class UIElements {

    // Function to check if the user is on a mobile device
    static isMobileDevice = () => {
        const ua = globalThis.navigator.userAgent;
        const uaShort = ua.substring(0, 4);
        return (
            // UA string patterns (split for complexity)
            /android.*mobile|avantgo|bada\/|blackberry|blazer/i.test(ua) ||
            /compal|elaine|fennec|hiptop|iemobile|ip(hone|od)/i.test(ua) ||
            /ipad|iris|kindle|silk|lge |maemo|midp|mmp/i.test(ua) ||
            /netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\//i.test(ua) ||
            /plucker|pocket|psp|series[46]0|symbian|treo/i.test(ua) ||
            /up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(ua) ||
            // 4-char prefix patterns (split for complexity, single-char alternations use character classes)
            /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s/i.test(uaShort) ||
            /a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)/i.test(uaShort) ||
            /amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)/i.test(uaShort) ||
            /attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)/i.test(uaShort) ||
            /bl(ac|az)|br[ev]w|bumb|bw-[nu]|c55\/|capi|ccwa/i.test(uaShort) ||
            /cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw/i.test(uaShort) ||
            /da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do[cp]o/i.test(uaShort) ||
            /ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8/i.test(uaShort) ||
            /ez([4-7]0|os|wa|ze)|fetc|fly[-_]|g1 u|g560|gene/i.test(uaShort) ||
            /gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit/i.test(uaShort) ||
            /hd-[mpt]|hei-|hi(pt|ta)|hp( i|ip)|hs-c/i.test(uaShort) ||
            /ht(c[- _agpst]|tp)|hu(aw|tc)|i-(20|go|ma)|i230/i.test(uaShort) ||
            /iac[ \-/]|ibro|idea|ig01|ikom|im1k|inno|ipaq/i.test(uaShort) ||
            /iris|ja[tv]a|jbro|jemu|jigs|kddi|keji/i.test(uaShort) ||
            /kgt[ /]|klon|kpt |kwc-|kyo[ck]|le(no|xi)/i.test(uaShort) ||
            /lg( g|\/[klu]|50|54|-[a-w])|libw|lynx|m1-w/i.test(uaShort) ||
            /m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr/i.test(uaShort) ||
            /me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t[- ov]|zz)/i.test(uaShort) ||
            /mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30[02]/i.test(uaShort) ||
            /n50[025]|n7(0[01]|10)|ne([cm]-|on|tf|wf|wg|wt)/i.test(uaShort) ||
            /nok[6i]|nzph|o2im|op(ti|wv)|oran|owg1|p800/i.test(uaShort) ||
            /pan[adt]|pdxg|pg(13|-[1-8c])|phil|pire|pl(ay|uc)/i.test(uaShort) ||
            /pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a/i.test(uaShort) ||
            /qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks/i.test(uaShort) ||
            /rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)/i.test(uaShort) ||
            /sc(01|h-|oo|p-)|sdk\/|se(c[-01]|47|mc|nd|ri)/i.test(uaShort) ||
            /sgh-|shar|sie[-m]|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)/i.test(uaShort) ||
            /so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)/i.test(uaShort) ||
            /t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel[im]/i.test(uaShort) ||
            /tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9/i.test(uaShort) ||
            /up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)/i.test(uaShort) ||
            /vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)/i.test(uaShort) ||
            /w3c[- ]|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700/i.test(uaShort) ||
            /yas-|your|zeto|zte-/i.test(uaShort)
        );
    }

    // Choose File button
    static filePicker = (instance) => {
        const button = document.createElement('button');
        button.className = 'btn';
        button.textContent = 'Choose File';
        button.onclick = () => {
            const input = document.createElement('input');
            input.type = 'file';
            input.accept = '.dwg,.dxf,.dwf,.dgn,.rvt,.pdf'; // Supported CAD file formats
            input.onchange = async (event) => {
                try {
                    const file = event.target.files[0];
                    const extension = file.name.split('.').pop().toLowerCase();
                    if (file && ['dwg', 'dxf', 'dgn', 'rvt'].includes(extension)) {
                        const arrayBuffer = await file.arrayBuffer();
                        const pdfBuffer = await globalThis.convertCadtoPdf(arrayBuffer, file.name);
                        instance.UI.loadDocument(pdfBuffer, {
                            extension: 'pdf',
                        });
                    } else if (file && extension === 'pdf') {
                        instance.UI.loadDocument(file);
                    } else {
                        alert('Unsupported file format. Please select a CAD or PDF file.');
                    }
                } catch (e) {
                    console.error(e);
                }
            };
            input.click();
        };

        return button;
    }

    // JSON Code Block Element
    static jsonElement = () => {
        const wrapper = document.createElement('div');
        wrapper.className = 'json-wrapper';
        wrapper.style.display = 'none'; // Initially hidden

        // Container for the JSON code block
        const container = document.createElement('div');
        container.className = 'json-container';

        // Code block for JSON
        const codePre = document.createElement('pre');
        codePre.className = 'json-pre';

        const codeBlock = document.createElement('code');
        codeBlock.id = 'json-code';
        codeBlock.contentEditable = false;

        // Assemble the JSON code block
        codePre.appendChild(codeBlock);
        container.appendChild(codePre);
        wrapper.appendChild(container);
        return wrapper;
    };

    // Extract Key Value Pairs button
    static extractKeyValuePairsButton = (instance) => {
        // Spinner element to indicate loading
        const spinner = document.createElement('div');
        spinner.className = 'spinner';

        // Button element for extracting key-value pairs
        const button = document.createElement('button');
        button.className = 'btn extract-btn';
        button.textContent = 'Extract Key-Value Pairs';
        button.onclick = async () => {
            try {
                button.disabled = true;
                spinner.style.display = 'inline-block';
                await globalThis.extractKeyValuePairs(instance);
            } catch (e) {
                console.error(e);
                button.disabled = false;
            }
            finally {
                // Hide spinner when done
                spinner.style.display = 'none';

                // Display the extracted JSON data
                const jsonWrapper = document.querySelector('.json-wrapper');
                const jsonCodeBlock = document.getElementById('json-code');
                if (globalThis.resultData) {
                    jsonCodeBlock.textContent = JSON.stringify(JSON.parse(globalThis.resultData), null, 2);
                    jsonWrapper.style.display = 'flex';
                } else {
                    jsonCodeBlock.textContent = '';
                    jsonWrapper.style.display = 'none';
                }

                // Show color legend if extraction was successful
                const legendContainer = document.querySelector('.legend-container');
                if (globalThis.resultData) {
                    legendContainer.style.display = 'flex';
                } else {
                    legendContainer.style.display = 'none';
                }
            }
        };

        button.appendChild(spinner);

        return button;
    }

    // Legends for the Annotations
    static colorLegend = () => {
        const legendContainer = document.createElement('div');
        legendContainer.className = 'legend-container';

        const colors = ['rgb(255, 0, 0)', 'rgb(0, 0, 255)', 'rgb(0, 255, 0)'];
        const labels = ['Key', 'Value', 'Connector'];

        for (let i = 0; i < colors.length; i++) {
            const legendItem = document.createElement('div');
            legendItem.className = 'legend-item';

            const colorBox = document.createElement('span');
            colorBox.className = 'color-box';
            colorBox.style.backgroundColor = colors[i];

            const label = document.createElement('span');
            label.textContent = labels[i];

            legendItem.appendChild(colorBox);
            legendItem.appendChild(label);
            legendContainer.appendChild(legendItem);
        }

        return legendContainer; 
    }

    // Reset JSON Code Block and Legend
    static resetUI = () => {
        // Hide JSON code block
        const jsonWrapper = document.querySelector('.json-wrapper');
        const jsonCodeBlock = document.getElementById('json-code');
        jsonCodeBlock.textContent = '';
        jsonWrapper.style.display = 'none';

        // Hide legend
        const legendContainer = document.querySelector('.legend-container');
        legendContainer.style.display = 'none';

        // Enable Extract button
        const extractButton = document.querySelector('.extract-btn');
        extractButton.disabled = false;
    }

    static createUIControls = (instance) => {
        // Create a container for all controls
        const controlsContainer = document.createElement('div');
        controlsContainer.className = 'button-container';

        // Add the file picker and Import/Export buttons to the controls container
        controlsContainer.appendChild(this.filePicker(instance));
        controlsContainer.appendChild(this.extractKeyValuePairsButton(instance));
        controlsContainer.appendChild(this.colorLegend());

        // Add the controls container to the viewer element
        const element = document.getElementById('viewer');
        element.insertBefore(this.jsonElement(), element.firstChild);
        element.insertBefore(controlsContainer, element.firstChild);
    };
}
```

{% endcode %}
{% endtab %}

{% tab title="handler.js" %}

<pre class="language-js" data-line-numbers><code class="lang-js">// ES6 Compliant Syntax
// GitHub Copilot v1.0, Claude Sonnet 4, October 22, 2025
// File: handler.js
// This file will handle CAD file conversion and extraction requests.

const fs = require('node:fs');
const { PDFNet } = require('@pdftron/pdfnet-node');

// **Important** 
// 1. You must get a license key from Apryse for the server to run. 
// A trial key can be obtained from:
// https://docs.apryse.com/core/guides/get-started/trial-key 
// 
// 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.
const licenseKey = '<code class="expression">visitor.claims.wvKey || "YOUR_SERVER_LICENSE_KEY"</code>';
const multer = require('multer');
const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, 'sentFiles/')
  },
  filename: function (req, file, cb) {
    // Save with original filename and extension
    cb(null, file.originalname)
  }
});
const upload = multer({ storage: storage });
const { response } = require('express');
const e = require('express');
const serverFolder = 'server';
const sentFiles = 'sentFiles';
const serverHandler = `/${serverFolder}/handler.js`;

module.exports = async function handler(app) {

  // Function to initialize PDFNet and check for module availability
  async function initializePDFNet() {
    // Create folder sentFiles that will hold the sent CAD format files, if it doesn't exist
    if (!fs.existsSync(sentFiles))
      fs.mkdirSync(sentFiles);

    // Initialize PDFNet
    await PDFNet.initialize(licenseKey);

    // Specify the PDFTron CAD and Data Extraction library path
    await PDFNet.addResourceSearchPath('./node_modules/@pdftron/cad/lib/');
    await PDFNet.addResourceSearchPath('./node_modules/@pdftron/data-extraction/lib/');

    // Check if the Apryse SDK CAD module is available.
    if (await PDFNet.CADModule.isModuleAvailable())
      console.log('Apryse SDK CAD module is available.');
    else
      console.log('Unable to run: Apryse SDK CAD module not available.');

    // Check if the Apryse SDK Data Extraction module is available.
    if (await PDFNet.DataExtractionModule.isModuleAvailable(PDFNet.DataExtractionModule.DataExtractionEngine.e_GenericKeyValue))
      console.log('Apryse SDK Data Extraction module is available.');
    else
      console.log('Unable to run: Apryse SDK Data Extraction module not available.');
  }

  // Handle POST request sent to '/server/handler.js'
  // This endpoint receives the CAD file URL to be loaded in the Apryse webviewer, then saves it to the server
  app.post(serverHandler, upload.single('cadfile'), async (request, response) => {
    try {
      const cadFilename = request.file.originalname;
      const fullFilename = request.file.path;

      // Convert the CAD file to PDF and get the buffer
      const buffer = await convertCadToPdfBuffer(fullFilename);
      console.log(`Conversion complete, extracting title block data...`);

      // Set headers to indicate a PDF file attachment and send the buffer
      await response.setHeader('Content-Type', 'application/pdf');
      await response.setHeader('Content-Disposition', `attachment; filename="${cadFilename.replace(/\.[^/.]+$/, ".pdf")}"`);
      response.status(200).send(buffer);
    } catch (e) {
      response.status(500).send(`Error processing CAD file: ${e.message}`);
    } finally {
      // Cleanup: remove the sent CAD file
      const cadPath = request.file.path;
      fs.unlink(cadPath, (err) => {
        if (err) {
          console.error(`Error removing CAD file ${cadPath}: ${err.message}`);
        }
      });
    }
  });

  // Function to convert CAD file to PDF and return as buffer
  const convertCadToPdfBuffer = async (fullFilename) => {
    try {
      // Create a new PDF document and convert the CAD file to PDF
      const doc = await PDFNet.PDFDoc.create();
      console.log('Converting CAD to PDF. Filename and Extension:', fullFilename);

      const options = new PDFNet.Convert.CADConvertOptions();
      options.setPageWidth(800);
      options.setPageHeight(600);
      options.setRasterDPI(150);

      await PDFNet.Convert.fromCAD(doc, fullFilename, options);

      // Initialize security handler and lock the document
      doc.initSecurityHandler();
      doc.lock();

      // Save the PDF document to a memory buffer
      console.log('After Conversion and Stored in PDFDoc Full filename:', doc.fullFilename);
      const uint8Array = await doc.saveMemoryBuffer(PDFNet.SDFDoc.SaveOptions.e_linearized);
      const buffer = Buffer.from(uint8Array);

      // Unlock the document
      doc.unlock();

      // Return the PDF buffer
      return buffer;
    }
    catch (err) {
      console.log(err);
      throw new Error(err);
    }
  };

  // Handle POST request sent to '/server/handler.js/extract-key-value-pairs'
  // This endpoint receives the PDF file path, extracts key-value data from the title block, and returns it as JSON
  app.post(`${serverHandler}/extract-key-value-pairs`, upload.single('pdffile'), async (request, response) => {
    try {
      console.log('Received PDF for key-value extraction');
      const pdfPath = request.file.path;
      const jsonResponse = await extractKeyValuePairs(pdfPath);
      response.status(200).json(jsonResponse);
    } catch (error) {
      console.error('Error extracting key-value data:', error);
      response.status(500).send('Error extracting key-value data');
    } finally {
      // Cleanup: remove the sent PDF file
      const pdfPath = request.file.path;
      fs.unlink(pdfPath, (err) => {
        if (err) {
          console.error(`Error removing PDF file ${pdfPath}: ${err.message}`);
        }
      });
    }
  });

  // Function to extract key-value pairs from PDF using Data Extraction module
  const extractKeyValuePairs = async (pdf) => {
    try {
      // Set up data extraction options
      const options = new PDFNet.DataExtractionModule.DataExtractionOptions();
      console.log('Setting extraction language to English');
      options.setLanguage('eng');

      // Extract key-value data from the PDF using the provided JSON template
      const jsonString = await PDFNet.DataExtractionModule.extractDataAsString(pdf, PDFNet.DataExtractionModule.DataExtractionEngine.e_GenericKeyValue, options);
      return jsonString;
      
    } catch (err) {
      console.log(err);
      throw new Error(err);
    }
  };

  // Initialize PDFNet
  PDFNet.runWithoutCleanup(initializePDFNet, licenseKey).then(
    function onFulfilled() {
      response.status(200);
    },
    function onRejected(error) {
      // log error and close response
      console.error('Error initializing PDFNet', error);
      response.status(503).send();
    }
  );
};

</code></pre>

{% endtab %}

{% tab title="server.js" %}
{% code title="server.js" lineNumbers="true" %}

```js
// ES6 Compliant Syntax
// GitHub Copilot v1.0, Claude Sonnet 4, October 22, 2025
// File: server.js
// This file is to run a server in localhost.

const express = require('express');
const fs = require('node:fs');
const bodyParser = require('body-parser');
const handler = require('./handler.js');
const port = process.env.PORT || 5050;
const app = express();
const sentPdfs = 'sentPdfs';

// CORS middleware to allow cross-origin requests from the playground
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
  
  // Handle preflight OPTIONS requests
  if (req.method === 'OPTIONS') {
    res.sendStatus(200);
  } else {
    next();
  }
});

app.use(bodyParser.text());
app.use('/client', express.static('../client')); // For statically serving 'client' folder at '/'

handler(app);

// Run server
const server = app.listen(port, 'localhost', (err) => {
  if (err) {
    console.error(err);
  } else {
    console.info(`Server is listening at http://localhost:${port}`);
    
  }
});

// Server shutdown and cleanup
function shutdown() {
  console.log('Cleanup started...');

  // Example: Close server
  server.close(() => {
    console.log('Server closed.');

    // Removes sent PDFs folder
    if (fs.existsSync(sentPdfs))
      fs.rmdirSync(sentPdfs, { recursive: true });

    // If no async cleanup, exit directly
    process.exit(0);
  });
}

// Handle shutdown signals
process.on('SIGINT', shutdown);   // Ctrl+C
process.on('SIGTERM', shutdown);  // kill command or Docker stop
process.on('uncaughtException', (err) => {
  console.error('Uncaught Exception:', err);
  shutdown();
});
```

{% endcode %}
{% endtab %}

{% tab title="package.json" %}
{% code title="package.json" lineNumbers="true" %}

```json
{
  "name": "cad-viewer-server",
  "version": "1.0.0",
  "description": "CAD Viewer Demo Server Component",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "node server.js"
  },
  "dependencies": {
    "@pdftron/cad": "^11.8.0",
    "@pdftron/data-extraction": "^11.8.0",
    "@pdftron/pdfnet-node": "^11.8.0",
    "body-parser": "^1.20.2",
    "express": "^4.18.2",
    "multer": "^1.4.4",
    "open": "^9.1.0"
  },
  "keywords": [
    "cad-viewer",
    "pdf",
    "server",
    "pdftron",
    "webviewer"
  ],
  "author": "Apryse",
  "license": "MIT"
}

```

{% endcode %}
{% endtab %}
{% endtabs %}

[View the full sample on GitHub](https://github.com/ApryseSDK/webviewer-samples/tree/main/showcase-demos-playground)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.apryse.com/web/get-started/samples/showcase-demos-title-block-extraction.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
