Scan from a Browser on Linux with SANE and JavaScript (Dynamic Web TWAIN 19.5)

A scanner plugged into an Ubuntu box is invisible to the browser. JavaScript has no path to /dev/bus/usb, and SANE is a C library, not something fetch() can call. So every “scan from the web on Linux” stack is really two programs talking to each other: a page, and a local service that speaks SANE on the page’s behalf.

This post builds that pair with Dynamic Web TWAIN 19.5. You will confirm SANE sees the device, install the local service, list devices from JavaScript, scan, preview and export — and you will see exactly where OCR stops working on Linux, because that boundary moved in 19.5 and it is easy to be surprised by it.

What you’ll build: a static page served by any web server on Linux that lists the SANE devices the service can reach, acquires pages into an in-browser viewer, and exports them as PDF, TIFF or PNG — no backend code, no native addon.

Key Takeaways

  • The browser never touches SANE directly. It calls the local Dynamic Web TWAIN Service on 127.0.0.1:18622 (HTTP) or 18623 (HTTPS), and the service drives SANE.
  • Verify with scanimage -L first. If SANE cannot see the scanner, the web page cannot either, and no JavaScript change will fix it.
  • Since 19.x the SDK is one file, dynamsoft.webtwain.min.js. There is no dynamsoft.webtwain.config.js to edit — configuration is an inline block before the object is created.
  • OCR is the exception: the OCR Connector add-on ships for Windows and macOS only. On Linux the call rejects with a platform error, so plan server-side OCR if you scan on Linux.

Common Developer Questions

How do I access a SANE scanner from browser JavaScript on Linux?

Install the Dynamic Web TWAIN Service on the Linux machine, make sure scanimage -L lists the device, then call the SDK from a normal page: GetDevicesAsync() to enumerate, SelectDeviceAsync() + AcquireImageAsync() to scan. The page only ever talks to the local service; the service is what links against SANE.

Why does the web app list no scanners on Ubuntu?

Almost always the SANE backend, not the web app. Run scanimage -L in the same user session. No output means a missing driver, a missing /etc/sane.d/*.conf entry, or a permission problem on the USB device — fix that first, then reload the page.

Does Dynamic Web TWAIN support arm64 Linux boards such as Raspberry Pi and Jetson?

Yes, the Linux build covers x64 and arm64, which is why this approach works on single-board computers as well as desktops. Mind the packaging detail: the npm/CDN tarball only carries the x64 .deb and .rpm; arm64 installers come from the Dynamsoft download page.

Can I run OCR on Linux with this SDK?

Not with the bundled OCR Connector. It is supported on Windows and macOS only; on Linux GetInstalledOCRInfo() rejects with a platform error. The usual split is: capture on Linux, ship the page image to server-side OCR (Tesseract, PaddleOCR, a cloud API) — or run the client on Windows/macOS if you need in-browser OCR.

Prerequisites

  • Ubuntu 20.04/22.04 (or another glibc distribution), x64 or arm64, with a SANE-capable scanner.
  • sane-utils for scanimage, and the SANE driver for your hardware (sane-airscan for eSCL/AirScan network devices).
  • Dynamic Web TWAIN Service 19.5 for Linux, plus a 30-day trial license.
sudo apt-get update
sudo apt-get install -y sane-utils
scanimage -L          # must print your device before you touch any JavaScript

Step 1: Prove SANE works before writing any code

Everything downstream depends on this one command. A healthy machine prints a device line for every backend that found hardware:

$ scanimage -L
device `custom_scanner:virtual' is a Custom Virtual Scanner virtual scanner

Then do one scan at the command line, with no browser in the picture. If this produces a file, the rest of the tutorial is plumbing:

$ scanimage -d custom_scanner:virtual --format=png --output-file=/tmp/scan-01.png
$ ls -lh /tmp/scan-01.png
-rw-r--r-- 1 root root 248K Oct 28 11:02 /tmp/scan-01.png

A page captured through the SANE virtual scanner

That image came out of exactly those two commands, run inside a container with the virtual scanner described in the testing section below.

If scanimage -L prints nothing, stop here: install the backend (/etc/sane.d/dll.d/ lists the enabled ones), check sane-find-scanner, and confirm your user can open the USB device. Adding your account to the scanner group and re-plugging is the usual fix:

sudo usermod -aG scanner "$USER"

Step 2: Install the Dynamic Web TWAIN Service

On Linux the service is a small daemon plus the SANE bridge. Install the package that matches your architecture and confirm it is up:

sudo dpkg -i DynamicWebTWAINServiceSetup.deb
sudo systemctl status DynamicWebTWAINService    # or: service DynamicWebTWAINService status

Two ports matter for debugging: 18622 (HTTP) and 18623 (HTTPS). They are loopback-only by default — that is why a page served from localhost can reach them and a page served from another host cannot, unless you deliberately open the service up.

$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:18622/
200

Step 3: Build the page against the 19.5 SDK

Put the page and the SDK’s Resources folder on any web server. Since 19.x there is a single entry point, so the <head> is short:

<script src="Resources/dynamsoft.webtwain.min.js"></script>

Configuration is inline, and AutoLoad is off so you decide when the object is created:

Dynamsoft.DWT.ProductKey = 'LICENSE-KEY';
Dynamsoft.DWT.ResourcesPath = 'Resources';
Dynamsoft.DWT.AutoLoad = false;

Enumerate what the SANE bridge can see, scan, and keep the pages in the buffer:

let DWObject = null;

async function connect() {
    DWObject = await Dynamsoft.DWT.CreateDWTObjectEx({ WebTwainId: 'scanner' });
    DWObject.Viewer.bind(document.getElementById('viewer'));
    DWObject.Viewer.show();

    const devices = await DWObject.GetDevicesAsync();
    const select = document.getElementById('device');
    devices.forEach((device, index) => {
        select.add(new Option(device.displayName || device.name, index));
    });
}

async function scan() {
    const devices = await DWObject.GetDevicesAsync();
    const device = devices[Number(document.getElementById('device').value)];
    await DWObject.SelectDeviceAsync(device);
    await DWObject.AcquireImageAsync({
        IfShowUI: false,
        IfCloseSourceAfterAcquire: true,
        IfFeederEnabled: document.getElementById('feeder').checked,
        PixelType: 2,      // 0 = B&W, 1 = grayscale, 2 = colour
        Resolution: 300
    });
}

Export is the same call you would use on Windows or macOS:

const types = Dynamsoft.DWT.EnumDWT_ImageType;
await DWObject.SaveAllAsPDF('scanned.pdf');                       // or:
const blob = await new Promise((resolve, reject) => {
    DWObject.ConvertToBlob(
        Array.from({ length: DWObject.HowManyImagesInBuffer }, (_, i) => i),
        types.IT_PDF, resolve, (code, message) => reject(new Error(message)));
});

Step 4: Serve it

Anything that speaks HTTP works. For a quick check, Python’s built-in server is enough:

cd /var/www/dwt-linux
python3 -m http.server 8080
# open http://localhost:8080/helloworld.html

For production, nginx with the files under /usr/share/nginx/html/ behaves identically — the page is static, and the only moving part is the loopback service.

Where OCR stops on Linux

This is the part worth reading twice. Dynamic Web TWAIN 19.5 made the OCR add-on engine-aware: GetInstalledOCRInfo() returns the engines the local service can drive — the operating system’s built-in OCR on Windows and macOS, plus any ONNX Runtime engine you deploy — together with the languages each one supports.

const info = await DWObject.Addon.OCRConnector.GetInstalledOCRInfo();
// { engines: [ { name: '...', languages: ['en', 'de', ...] } ] }

const result = await DWObject.Addon.OCRConnector.Recognize(0, {
    engine: { name: info.engines[0].name },
    settings: { language: info.engines[0].languages[0] }
});

But the add-on itself is a Windows/macOS component. Call it against a Linux service and the promise rejects with a “not supported on this platform” error, before any text is produced. There is no flag to flip and no extra package that changes that today.

So the honest architecture for a Linux capture box is:

Runs where Job Tool
Linux, in the browser scan, rotate, delete, PDF/TIFF/PNG export Dynamic Web TWAIN + SANE
Linux, server side text extraction, indexing, searchable PDF Tesseract / PaddleOCR / a cloud OCR API
Windows or macOS, in the browser interactive OCR with the page in front of the operator Dynamic Web TWAIN OCR Connector

If the operator must see the OCR result immediately, run the client on Windows or macOS and keep the scanner on the network — the same page, the same code, with the OCR block guarded by a capability check.

Testing without a physical scanner

You do not need hardware to exercise the whole path. The virtual-scanner v1.0.7 release ships a SANE backend that pretends to be a scanner and feeds PNM images from disk; on Linux it is a 1.7 MB .deb. This is how the image and the command output above were produced, in Docker:

curl -LO https://github.com/yushulx/virtual-scanner/releases/download/v1.0.7/sane-custom-scanner_1.0.7_amd64.deb
sudo dpkg -i sane-custom-scanner_1.0.7_amd64.deb
scanimage -L                 # device `custom_scanner:virtual' is a Custom Virtual Scanner virtual scanner
scanimage -d custom_scanner:virtual --format=png --output-file=page1.png

A single job returns one image and remembers the position, so the next job returns the next page; selecting the automatic document feeder returns all of them as one multi-page job. Drop your own pages in with convert input.png -depth 8 <name>.pnm under /usr/share/custom_scanner/images/.

The same release also has Windows (VirtualScannerSetup-1.0.7.exe, or the portable zip) and macOS (.pkg) builds, which is handy when the Linux side is a headless box and you want to test the page from a desktop.

Common Issues & Edge Cases

  • scanimage -L works as root but not as your user: the device node is not readable by your account. Add the user to scanner, re-plug the device, log out and back in.
  • The page connects but lists zero devices: the service runs as a system user that may not see the same SANE configuration, and network backends such as sane-airscan need discovery to reach the machine. Run scanimage -L as the service account, or enable the backend in /etc/sane.d/dll.d/.
  • curl http://127.0.0.1:18622/ fails: the service is not running, or a proxy environment variable is hijacking loopback traffic. curl --noproxy '*' and a browser started with --no-proxy-server are the quick checks.
  • arm64 boards scan slowly or return blank pages: USB bandwidth on single-board computers is the usual culprit. Lower the resolution, disable duplex/feeder, and avoid unpowered hubs.
  • PDF export throws instead of downloading: the PDF add-on must be present in the Resources folder. A half-copied Resources directory is the most common cause.
  • OCR throws not supported on this platform: expected on Linux, see the OCR section above.

Try the Online Demo

The page from this tutorial runs as a hosted demo, including the OCR row when it is opened on Windows or macOS:

https://www.dynamsoft.com/codepool/demos/web-twain-scanner/

Source Code

Get the complete sample project source code on GitHub