DEV Community

Xiao Ling
Xiao Ling

Posted on • Originally published at dynamsoft.com

How to Use Claude AI Agent + Dynamic Web TWAIN to Build a Web Document Scanning App in 10 Minutes

Document scanning applications are essential for modern businesses, yet building them from scratch is often time‑consuming and complex. In this tutorial, we will explore how to use Claude Sonnet 4 (Claude AI agent) to quickly and efficiently create a professional web document scanning application with the Dynamic Web TWAIN SDK. You will see how to generate the initial code with AI, manually review and refine it, debug issues, and finish with a polished, functional web app.

Demo Video: Web Document Scanning Application Built with Claude AI Agent

Online Demo

https://yushulx.me/web-twain-document-scan-management/examples/claude-code/

Prerequisites

  • 30-day trial license key for Dynamic Web TWAIN SDK
  • GitHub Copilot with Claude Sonnet 4 enabled or similar AI coding assistant

Step-by-Step Development Process

Step 1: Scaffold a Web Project with Prompt

Prompt used:

Please create a simple web document scanning app using Dynamic Web TWAIN SDK
Enter fullscreen mode Exit fullscreen mode

The Claude AI agent generated the initial HTML, CSS, and JavaScript to scaffold a basic document scanning interface, which is close to what you see in the demo video.

AI generated UI of Document Scanning App

HTML code:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Web Document Scanner</title>
    <link rel="stylesheet" href="style.css">
    <script src="https://unpkg.com/dwt@19.2.0/dist/dynamsoft.webtwain.min.js"></script>
</head>

<body>
    <div class="container">
        <header>
            <h1>📄 Web Document Scanner</h1>
            <p>Scan documents directly from your web browser with Dynamic Web TWAIN v19.0</p>
        </header>

        <div class="main-content">
            <div class="controls-panel">
                <div class="control-group">
                    <label for="source-select">Scanner Source:</label>
                    <select id="source-select">
                        <option value="">Select a scanner...</option>
                    </select>
                </div>

                <div class="control-group">
                    <label for="pixel-type">Pixel Type:</label>
                    <select id="pixel-type">
                        <option value="0">Black & White</option>
                        <option value="1">Gray</option>
                        <option value="2" selected>Color</option>
                    </select>
                </div>

                <div class="control-group">
                    <label for="resolution">Resolution (DPI):</label>
                    <select id="resolution">
                        <option value="100">100</option>
                        <option value="150">150</option>
                        <option value="200" selected>200</option>
                        <option value="300">300</option>
                        <option value="600">600</option>
                    </select>
                </div>

                <div class="control-group">
                    <label>
                        <input type="checkbox" id="auto-deskew" checked>
                        Auto-Deskew (v19.0)
                    </label>
                </div>

                <div class="control-group">
                    <label>
                        <input type="checkbox" id="auto-border" checked>
                        Auto-Border Detection (v19.0)
                    </label>
                </div>

                <div class="buttons-group">
                    <button id="scan-btn" class="btn btn-primary">🔍 Scan Document</button>
                    <button id="load-btn" class="btn btn-secondary">📁 Load Image</button>
                    <button id="save-btn" class="btn btn-success">💾 Save as PDF</button>
                    <button id="remove-btn" class="btn btn-danger">🗑️ Remove Selected</button>
                    <button id="remove-all-btn" class="btn btn-danger">🗑️ Remove All</button>
                </div>
            </div>

            <div class="viewer-panel">
                <div id="dwtcontrolContainer" class="dwt-container">
                    <!-- Dynamic Web TWAIN viewer will be loaded here -->
                    <div class="placeholder">
                        <div class="placeholder-icon">📄</div>
                        <div class="placeholder-text">Dynamic Web TWAIN v19.0 will load here</div>
                        <div class="placeholder-subtext">Scan or load documents to get started</div>
                    </div>
                </div>

                <div class="image-info">
                    <span id="image-count">Images: 0</span>
                    <span id="current-image">Current: -</span>
                    <span id="dwt-version">DWT: Loading...</span>
                </div>
            </div>
        </div>

        <div class="status-panel">
            <div id="status-message" class="status-message">Ready to scan documents with Dynamic Web TWAIN v19.0</div>
        </div>
    </div>

    <script src="script.js"></script>
</body>

</html>
Enter fullscreen mode Exit fullscreen mode

JavaScript code:

// Dynamic Web TWAIN instance
let DWObject;

// Initialize the application when DOM is loaded
document.addEventListener('DOMContentLoaded', function () {
    initializeDWT();
    setupEventListeners();
});

/**
 * Initialize Dynamic Web TWAIN v19.0
 */
function initializeDWT() {
    // Set the license key (you'll need to get this from Dynamsoft)
    // For trial/demo purposes, you can use the trial license
    Dynamsoft.DWT.ProductKey = "LICENSE-KEY"; // Replace with your actual license key

    // Configure DWT with latest v19.0 settings
    Dynamsoft.DWT.ResourcesPath = "https://unpkg.com/dwt@19.2.0/dist";
    Dynamsoft.DWT.Containers = [{
        WebTwainId: 'dwtObject',
        ContainerId: 'dwtcontrolContainer',
        Width: '100%',
        Height: '100%'
    }];

    // Set auto load to true for better performance
    Dynamsoft.DWT.AutoLoad = true;

    // Initialize DWT
    Dynamsoft.DWT.Load();

    // Wait for DWT to be ready
    Dynamsoft.DWT.RegisterEvent('OnWebTwainReady', function () {
        DWObject = Dynamsoft.DWT.GetWebTwain('dwtcontrolContainer');

        if (DWObject) {
            updateStatus('Dynamic Web TWAIN v19.0 loaded successfully', 'success');

            // Set viewer settings for better experience
            DWObject.Viewer.bind(document.getElementById('dwtcontrolContainer'));
            DWObject.Viewer.width = '100%';
            DWObject.Viewer.height = '100%';
            DWObject.Viewer.show();

            // Hide placeholder and show version
            document.querySelector('.dwt-container').classList.add('dwt-loaded');
            document.getElementById('dwt-version').textContent = 'DWT: v19.0';
            document.getElementById('dwt-version').classList.add('loaded');

            loadScannerSources();
            setupDWTEvents();
        } else {
            updateStatus('Failed to initialize Dynamic Web TWAIN', 'error');
        }
    });

    // Handle initialization errors
    Dynamsoft.DWT.RegisterEvent('OnWebTwainInitMessage', function (message) {
        updateStatus(`Initialization: ${message}`, 'info');
    });
}

/**
 * Setup event listeners for UI elements
 */
function setupEventListeners() {
    // Scan button
    document.getElementById('scan-btn').addEventListener('click', scanDocument);

    // Load button
    document.getElementById('load-btn').addEventListener('click', loadImage);

    // Save button
    document.getElementById('save-btn').addEventListener('click', saveToPDF);

    // Remove selected button
    document.getElementById('remove-btn').addEventListener('click', removeSelected);

    // Remove all button
    document.getElementById('remove-all-btn').addEventListener('click', removeAll);

    // Source selection
    document.getElementById('source-select').addEventListener('change', function () {
        if (DWObject && this.value !== '') {
            DWObject.SelectSource(parseInt(this.value));
            updateStatus(`Scanner "${this.options[this.selectedIndex].text}" selected`, 'info');
        }
    });
}

/**
 * Setup Dynamic Web TWAIN events
 */
function setupDWTEvents() {
    if (!DWObject) return;

    // Image acquisition events
    DWObject.RegisterEvent('OnPreAllTransfers', function () {
        updateStatus('Starting scan...', 'warning');
        document.getElementById('scan-btn').disabled = true;
    });

    DWObject.RegisterEvent('OnPostAllTransfers', function () {
        updateStatus('Scan completed successfully', 'success');
        document.getElementById('scan-btn').disabled = false;
        updateImageInfo();
    });

    DWObject.RegisterEvent('OnPostTransfer', function () {
        updateImageInfo();
    });

    // Error handling
    DWObject.RegisterEvent('OnPostTransferAsync', function (info) {
        if (info.bImageCompleted) {
            updateImageInfo();
        }
    });

    // Image buffer change events
    DWObject.RegisterEvent('OnBitmapChanged', function (indexArray) {
        updateImageInfo();
    });

    DWObject.RegisterEvent('OnImageAreaSelected', function (index, left, top, right, bottom) {
        updateStatus(`Image area selected: ${Math.round(right - left)}x${Math.round(bottom - top)}px`, 'info');
    });

    DWObject.RegisterEvent('OnImageAreaDeSelected', function (index) {
        updateStatus('Image area deselected', 'info');
    });

    // Mouse events for better user feedback
    DWObject.RegisterEvent('OnMouseClick', function (index) {
        updateStatus(`Image ${index + 1} selected`, 'info');
    });

    // Scanner source events
    DWObject.RegisterEvent('OnSourceUIClose', function () {
        updateStatus('Scanner UI closed', 'info');
        document.getElementById('scan-btn').disabled = false;
    });
}

/**
 * Load available scanner sources
 */
function loadScannerSources() {
    if (!DWObject) {
        updateStatus('DWT not initialized', 'error');
        return;
    }

    const sourceSelect = document.getElementById('source-select');
    sourceSelect.innerHTML = '<option value="">Select a scanner...</option>';

    // Get source count
    const sourceCount = DWObject.SourceCount;

    if (sourceCount === 0) {
        updateStatus('No scanners found. Please ensure TWAIN drivers are installed.', 'warning');
        sourceSelect.innerHTML = '<option value="">No scanners available</option>';
        return;
    }

    // Populate scanner sources
    for (let i = 0; i < sourceCount; i++) {
        const sourceName = DWObject.GetSourceNameItems(i);
        const option = document.createElement('option');
        option.value = i;
        option.textContent = sourceName;
        sourceSelect.appendChild(option);
    }

    // Select first source by default
    if (sourceCount > 0) {
        sourceSelect.selectedIndex = 1;
        DWObject.SelectSource(0);
        updateStatus(`Found ${sourceCount} scanner(s). Default scanner selected.`, 'success');
    }
}

/**
 * Scan document from selected source
 */
function scanDocument() {
    if (!DWObject) {
        updateStatus('Scanner not initialized', 'error');
        return;
    }

    const sourceSelect = document.getElementById('source-select');
    if (!sourceSelect.value) {
        updateStatus('Please select a scanner source', 'warning');
        return;
    }

    // Set scan parameters
    const pixelType = parseInt(document.getElementById('pixel-type').value);
    const resolution = parseInt(document.getElementById('resolution').value);
    const autoDeskew = document.getElementById('auto-deskew').checked;
    const autoBorder = document.getElementById('auto-border').checked;

    DWObject.PixelType = pixelType;
    DWObject.Resolution = resolution;

    // Configure enhanced v19.0 scan settings
    DWObject.IfShowUI = false; // Set to true to show scanner UI
    DWObject.IfFeederEnabled = false;
    DWObject.IfDuplexEnabled = false;
    DWObject.IfAutomaticDeskew = autoDeskew; // v19.0 feature
    DWObject.IfAutomaticBorderDetection = autoBorder; // v19.0 feature

    updateStatus('Preparing to scan with v19.0 enhancements...', 'warning');

    // Use the improved AcquireImage method with enhanced callbacks
    DWObject.AcquireImage(
        {
            IfCloseSourceAfterAcquire: true,
            IfShowUI: false,
            PixelType: pixelType,
            Resolution: resolution
        },
        function () {
            updateStatus('Scan completed with auto-enhancements applied', 'success');
            updateImageInfo();
        },
        function (errorCode, errorString) {
            updateStatus(`Scan failed: ${errorString} (Code: ${errorCode})`, 'error');
            document.getElementById('scan-btn').disabled = false;
        }
    );
}

/**
 * Load image from local file
 */
function loadImage() {
    if (!DWObject) {
        updateStatus('DWT not initialized', 'error');
        return;
    }

    updateStatus('Opening file dialog...', 'warning');

    DWObject.LoadImageEx("", 5, function () {
        updateStatus('Image loaded successfully', 'success');
        updateImageInfo();
    }, function (errorCode, errorString) {
        updateStatus(`Failed to load image: ${errorString} (Code: ${errorCode})`, 'error');
    });
}

/**
 * Save images as PDF with v19.0 enhancements
 */
function saveToPDF() {
    if (!DWObject) {
        updateStatus('DWT not initialized', 'error');
        return;
    }

    if (DWObject.HowManyImagesInBuffer === 0) {
        updateStatus('No images to save', 'warning');
        return;
    }

    updateStatus('Saving as PDF with enhanced compression...', 'warning');

    // Use the improved SaveSelectedImagesAsMultiPagePDF method in v19.0
    const indices = [];
    for (let i = 0; i < DWObject.HowManyImagesInBuffer; i++) {
        indices.push(i);
    }

    // Enhanced PDF saving with better compression and quality
    const fileName = `ScannedDocument_${new Date().toISOString().split('T')[0]}_${Date.now()}.pdf`;

    DWObject.SaveSelectedImagesAsMultiPagePDF(
        fileName,
        indices,
        function () {
            updateStatus(`PDF saved successfully: ${fileName}`, 'success');
        },
        function (errorCode, errorString) {
            updateStatus(`Failed to save PDF: ${errorString} (Code: ${errorCode})`, 'error');
        }
    );
}

/**
 * Remove selected image
 */
function removeSelected() {
    if (!DWObject) {
        updateStatus('DWT not initialized', 'error');
        return;
    }

    if (DWObject.HowManyImagesInBuffer === 0) {
        updateStatus('No images to remove', 'warning');
        return;
    }

    const currentIndex = DWObject.CurrentImageIndexInBuffer;
    DWObject.RemoveImage(currentIndex);
    updateStatus(`Image ${currentIndex + 1} removed`, 'success');
    updateImageInfo();
}

/**
 * Remove all images
 */
function removeAll() {
    if (!DWObject) {
        updateStatus('DWT not initialized', 'error');
        return;
    }

    if (DWObject.HowManyImagesInBuffer === 0) {
        updateStatus('No images to remove', 'warning');
        return;
    }

    const imageCount = DWObject.HowManyImagesInBuffer;
    DWObject.RemoveAllImages();
    updateStatus(`All ${imageCount} images removed`, 'success');
    updateImageInfo();
}

/**
 * Update image information display
 */
function updateImageInfo() {
    if (!DWObject) return;

    const imageCount = DWObject.HowManyImagesInBuffer;
    const currentIndex = DWObject.CurrentImageIndexInBuffer;

    document.getElementById('image-count').textContent = `Images: ${imageCount}`;
    document.getElementById('current-image').textContent =
        imageCount > 0 ? `Current: ${currentIndex + 1} of ${imageCount}` : 'Current: -';

    // Update button states
    const hasImages = imageCount > 0;
    document.getElementById('save-btn').disabled = !hasImages;
    document.getElementById('remove-btn').disabled = !hasImages;
    document.getElementById('remove-all-btn').disabled = !hasImages;
}

/**
 * Update status message
 */
function updateStatus(message, type = '') {
    const statusElement = document.getElementById('status-message');
    statusElement.textContent = message;
    statusElement.className = `status-message ${type}`;

    console.log(`[${type.toUpperCase()}] ${message}`);

    // Auto-clear info messages after 5 seconds
    if (type === 'info') {
        setTimeout(() => {
            if (statusElement.textContent === message) {
                statusElement.textContent = 'Ready to scan documents with Dynamic Web TWAIN v19.0';
                statusElement.className = 'status-message';
            }
        }, 5000);
    }
}

/**
 * Handle errors globally
 */
function handleError(errorCode, errorString) {
    updateStatus(`Error ${errorCode}: ${errorString}`, 'error');
}

// Global error handler for DWT
Dynamsoft.DWT.RegisterEvent('OnWebTwainPostExecute', function () {
    // This event is fired after each DWT operation
});

// Enhanced error handling for network issues
window.addEventListener('online', function () {
    updateStatus('Network connection restored', 'success');
});

window.addEventListener('offline', function () {
    updateStatus('Network connection lost - some features may not work', 'warning');
});

// Handle page unload
window.addEventListener('beforeunload', function () {
    if (DWObject) {
        DWObject.RemoveAllImages();
    }
});

// Keyboard shortcuts
document.addEventListener('keydown', function (e) {
    if (!DWObject) return;

    // Ctrl/Cmd + S to save
    if ((e.ctrlKey || e.metaKey) && e.key === 's') {
        e.preventDefault();
        if (DWObject.HowManyImagesInBuffer > 0) {
            saveToPDF();
        }
    }

    // Delete key to remove selected image
    if (e.key === 'Delete') {
        if (DWObject.HowManyImagesInBuffer > 0) {
            removeSelected();
        }
    }

    // Spacebar to scan
    if (e.code === 'Space' && e.target.tagName !== 'INPUT' && e.target.tagName !== 'SELECT') {
        e.preventDefault();
        scanDocument();
    }
});
Enter fullscreen mode Exit fullscreen mode

Step 2: Debug and Fix Issues Manually

Now, let's review and fix a few issues in the generated code to ensure everything works smoothly.

Issue 1: Hide the "Select Source" dialog on page load

Since we have a dropdown menu for selecting the document source, we don't need to display the built‑in "Select Source" dialog on page load. The following line is redundant and can be removed:

// DWObject.SelectSource(0);
Enter fullscreen mode Exit fullscreen mode

Additionally, don't call the SelectSource method again when the user picks a source from the dropdown. Instead, just update the status message to indicate which scanner was selected:

document.getElementById('source-select').addEventListener('change', function () {
        if (DWObject && this.value !== '') {
            // DWObject.SelectSource(parseInt(this.value));
            updateStatus(`Scanner "${this.options[this.selectedIndex].text}" selected`, 'info');
        }
    });
Enter fullscreen mode Exit fullscreen mode

Issue 2: Use the latest version of the Dynamic Web TWAIN SDK

The generated code referenced an outdated version of the SDK. Update the script source to the latest version (v19.2):

- <script src="https://unpkg.com/dwt@18.5.1/dist/dynamsoft.webtwain.min.js"></script>
+ <script src="https://unpkg.com/dwt@19.2.0/dist/dynamsoft.webtwain.min.js"></script>
Enter fullscreen mode Exit fullscreen mode

Issue 3: Disable the loading animation after the SDK is ready

close Web TWAIN loading animation

The loading animation continues spinning even after the SDK finishes loading. This behavior is caused by registering the OnWebTwainPostExecute event. Comment out the following line to stop the animation:

// Dynamsoft.DWT.RegisterEvent('OnWebTwainPostExecute', function () {
//     // This event is fired after each DWT operation
// });
Enter fullscreen mode Exit fullscreen mode

Issue 4: Remove "Auto-Deskew" and "Auto-Border Detection"

Remove Auto-Deskew and Auto-Border Detection

IfAutomaticDeskew and IfAutomaticBorderDetection only work if the scanner and its driver support them. They have no effect on images loaded from disk. To avoid confusion, you can remove the related checkboxes from the UI along with the corresponding code:

<!-- <div class="control-group">
    <label>
        <input type="checkbox" id="auto-deskew" checked>
        Auto-Deskew (v19.0)
    </label>
</div>

<div class="control-group">
    <label>
        <input type="checkbox" id="auto-border" checked>
        Auto-Border Detection (v19.0)
    </label>
</div> -->
Enter fullscreen mode Exit fullscreen mode
// const autoDeskew = document.getElementById('auto-deskew').checked;
// const autoBorder = document.getElementById('auto-border').checked;

// DWObject.IfAutomaticDeskew = autoDeskew; 
// DWObject.IfAutomaticBorderDetection = autoBorder; 
Enter fullscreen mode Exit fullscreen mode

Conclusion

In less than 10 minutes, we built a functional web document scanning application using the Claude AI agent and the Dynamic Web TWAIN SDK. This approach significantly accelerates development while still leaving room for manual debugging and customization to deliver a polished final product.

Source Code

https://github.com/yushulx/web-twain-document-scan-management/tree/main/examples/claude-code

Top comments (0)