How to Build an Ionic Vue QR Code Scanner with the Dynamsoft Capture Vision Native SDK
Decoding QR codes and 1D barcodes from a phone camera needs a production-grade native pipeline.
With the Dynamsoft Capture Vision native SDK (com.dynamsoft:capturevisionbundle:3.6.2000
on Android, DynamsoftCaptureVisionBundle 3.6.2000 on iOS) wrapped in a small in-app Capacitor
plugin, every frame is processed on-device with the multi-format preset template and the
result is returned as JSON. This article walks through the ionic-vue-qr-code-scanner
reference project — one Ionic Vue app, one in-app plugin, two data sources, and zero
JavaScript scanning SDK.
The scanner supports QR Code, DataMatrix, PDF417, Aztec, Code 39/93/128, EAN, UPC, ITF and Codabar, with real-time contour drawing on the native preview. The same code path accepts a still image from the gallery so a user can pick a saved photo and decode it with the same multi-format pipeline.
What you’ll build
- An Ionic Vue app that exposes two clearly separated data sources — Camera and File — with the live preview running entirely on the native side.
- A multi-format barcode scanner that runs the built-in barcode preset template (
ReadBarcodeson Android,ReadBarcodes_Defaultafter loading the SDK templates on iOS) on every camera frame and overlays the decoded contour through Dynamsoft’sDrawingLayersystem on both platforms (DBR_LAYER_IDon Android, a custom green-styled layer on iOS). - An in-app Capacitor plugin (
BarcodeScannerNative) that returns every decoded barcode with text, format, format string and four corner points, ready to be rendered as a list. - A matching iOS Swift implementation that drops into the same Xcode project once the
Dynamsoft pods are installed via
pod install(see Step 4).
Key Takeaways
- Native SDK end-to-end. All decoding happens on-device. The WebView only renders the result; it never loads a JavaScript scanner.
- Camera + file, one engine. Both data sources feed the same
CaptureVisionRouterthrough the sameScannerEnginesingleton so the deep-learning models load only once. - Latest Capture Vision release. Both platforms use the same
3.6.2000SDK (com.dynamsoft:capturevisionbundleandDynamsoftCaptureVisionBundle). - No third-party plugin. The Capacitor bridge is fully implemented in this project.
Architecture
Common Developer Questions
What barcode formats are supported?
QR Code, DataMatrix, PDF417, Aztec, Code 39, Code 93, Code 128, EAN-8, EAN-13, UPC-A, UPC-E,
ITF and Codabar. The ReadBarcodes preset template enables the entire set out of the box —
no extra configuration is required.
Should I add @capacitor/camera to handle the gallery picker?
No. The native plugin implements its own gallery picker on Android (an ACTION_PICK intent
through MediaStore) and on iOS (UIImagePickerController after requesting photo-library
access). Adding @capacitor/camera would
introduce a second picker that the user has to authorise separately. The reference project
keeps the picker inside the plugin for exactly this reason.
Why does the camera page not show an HTML video tag?
DSCameraView (iOS) and CameraView (Android) are native views from the Camera Enhancer
module. They run AVFoundation and CameraX respectively, which gives the scanner access to
frame-accurate control and the drawing layer for real-time contour rendering. An HTML
<video> element would not work — the JS SDK is intentionally not used in this project.
How does the camera result differ from the file result?
The camera path also runs the camera frame through the multi-frame result filter, so contours
stay on screen until the user confirms. The file path returns whatever the SDK can decode from
the supplied still image; if no barcodes are present, the call rejects with "No barcodes were
found in the image.".
Prerequisites
- Node.js 20+ and npm
- Android Studio with Android SDK 36 (compileSdk 36, minSdk 24)
- Xcode 16+ (or newer) for iOS
- A physical device — the camera does not work in the iOS simulator or the Android emulator
- A Dynamsoft Capture Vision trial license (already embedded in
android/app/src/main/res/values/strings.xmland mirrored in the iOS plugin source)
Get a 30-day free trial license
Step 1: Scaffold the project
git clone https://github.com/yushulx/android-camera-barcode-mrz-document-scanner.git
cd android-camera-barcode-mrz-document-scanner/examples/ionic-vue-qr-code-scanner
npm install
npm run build
npx cap sync
npx cap open android # or `npx cap open ios`
The Vite + Vue 3 build outputs the SPA in dist/. Capacitor copies that directory into the
native assets so the WebView loads it offline.
Step 2: The web layer
The web layer exports the plugin contract and renders the two data sources as separate cards.
// src/scanner/BarcodeScannerNative.ts
import { registerPlugin } from '@capacitor/core';
export interface BarcodeResult {
text: string;
format: string;
formatString: string;
points: Array<{ x: number; y: number }>;
}
export interface BarcodeScannerNativePlugin {
initLicense(): Promise<{ success: boolean; message: string }>;
startScan(): Promise<{ results: BarcodeResult[] }>;
scanFromGallery(): Promise<{ results: BarcodeResult[] }>;
scanFile(options: { uri: string }): Promise<{ results: BarcodeResult[] }>;
}
export const BarcodeScannerNative = registerPlugin<BarcodeScannerNativePlugin>('BarcodeScannerNative');
HomePage.vue shows two cards — Camera and File — and a spinner while the native scanner is
running. The result is stored in a tiny reactive store and the app navigates to a result view
that lists every decoded barcode. Each entry shows the format, the decoded text, and (when
available) the four corner points.
Step 3: The native plugin on Android
The in-app plugin lives at
android/app/src/main/java/com/dynamsoft/ionic/qrcodescanner/. MainActivity registers the
plugin class with the Capacitor bridge.
public class MainActivity extends BridgeActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
registerPlugin(BarcodeScannerNativePlugin.class);
super.onCreate(savedInstanceState);
}
}
ScannerEngine owns a single CaptureVisionRouter and the license state. The
startCapturing overload uses the built-in PT_READ_BARCODES preset template.
public static String template() {
return EnumPresetTemplate.PT_READ_BARCODES;
}
public DecodedBarcodesResult decodeFile(String path) throws ScannerException {
return unwrap(router.capture(path, template()));
}
ScannerActivity renders the live preview with a com.dynamsoft.dce.CameraView and registers a
CapturedResultReceiver whose onDecodedBarcodesReceived callback is fired on every frame
that contains at least one barcode. Each item is drawn on the DBR_LAYER_ID through a
QuadDrawingItem, so the user sees a green outline while aiming. The Confirm button
serializes the latest items into a JSON array and finishes the activity.
The file data source uses engine.decodeFile / engine.decodeBitmap, which call
CaptureVisionRouter.capture on a single still image. BarcodeScannerNativePlugin.decodeUri
handles content:// and file:// URIs transparently by switching between
engine.decodeBitmap and engine.decodeFile.
The app/build.gradle adds the capturevisionbundle dependency and the
packagingOptions that keep the bundled .so files intact.
dependencies {
// Dynamsoft Capture Vision native SDK (latest release)
implementation 'com.dynamsoft:capturevisionbundle:3.6.2000'
}
packagingOptions {
doNotStrip "**/*.so"
pickFirst "**/libc++_shared.so"
}
The project root build.gradle adds the Dynamsoft Maven repository so the AAR can be
resolved.
Step 4: The native plugin on iOS
The iOS side mirrors the Java plugin. Swift files in ios/App/App/ include the plugin
(BarcodeScannerNativePlugin.swift), the camera view controller
(BarcodeCameraScanViewController.swift) and a SharedImagePicker wrapper around
UIImagePickerController that requests photo-library access first.
The plugin class is registered automatically because it appears in
ios/App/App/capacitor.config.json. Note that on Capacitor 7+ the Swift class must also
conform to CAPBridgedPlugin (declare identifier, jsName and pluginMethods) — a bare
@objc subclass of CAPPlugin is silently skipped, and every web call then resolves to
“plugin is not implemented”:
{
"appId": "com.dynamsoft.ionic.qrcodescanner",
"packageClassList": ["BarcodeScannerNativePlugin"]
}
The camera view controller uses CameraView + CameraEnhancer + CaptureVisionRouter
from the DynamsoftCaptureVisionBundle CocoaPods framework and draws the contours through
the SDK’s own drawing system — not a hand-drawn overlay. A custom layer created with
cameraView.createDrawingLayer() receives QuadDrawingItems built from the decoded
locations, and the SDK maps the video-frame coordinates (landscape pixel space) onto the
portrait preview automatically.
let dce = CameraEnhancer()
let cvr = CaptureVisionRouter()
try? cvr.setInput(dce)
cvr.addResultReceiver(self)
// The iOS SDK ships its presets as a JSON template file inside
// DynamsoftCaptureVisionBundle.framework - load it once, then use the
// platform template name (ReadBarcodes_Default on iOS, ReadBarcodes on Android).
if let templates = Bundle.allFrameworks.lazy
.compactMap({ $0.path(forResource: "dbr-bundle-mobile-templates", ofType: "json") })
.first {
try? cvr.initSettingsFromFile(templates)
}
try? cvr.startCapturing("ReadBarcodes_Default") { success, error in
DispatchQueue.main.async {
if !success { self.statusLabel.text = error?.localizedDescription }
}
}
Contour drawing is wired through the SDK drawing layers:
private func configureDrawingLayers() {
let barcodeStyle = DrawingStyleManager.createDrawingStyle(
.green, strokeWidth: 3,
fill: UIColor.green.withAlphaComponent(0.15),
textColor: .white, font: .systemFont(ofSize: 12))
let barcodeLayer = cameraView.createDrawingLayer()
barcodeLayer.visible = true
barcodeLayer.setDefaultStyle(barcodeStyle)
barcodeLayerId = barcodeLayer.layerId
}
// Called with the decoded items of every frame (empty array when a frame has no results).
private func apply(items: [BarcodeResultItem]) {
latestItems = items
let layer = cameraView.getDrawingLayer(barcodeLayerId)
layer?.clearDrawingItems()
if !items.isEmpty {
layer?.addDrawingItems(items.map { QuadDrawingItem(quadrilateral: $0.location) })
}
// ... status label + Confirm button state
}
Keep the contours on an SDK drawing layer. A custom UIView overlay defaults to
isOpaque = true: once its draw(_:) strokes a path, the un-painted pixels of the backing
store composite as opaque black and cover the camera preview the moment the first barcode is
found — and the raw item.location points are video-frame coordinates that would need manual
orientation and aspect-fill conversion anyway. The SDK drawing layer handles both problems.
The Swift SDK modules are integrated with CocoaPods (not Swift Package Manager). The
ios/App/Podfile of the project looks like this:
target 'App' do
capacitor_pods
pod 'DynamsoftCaptureVisionBundle', '3.6.2000' # self-contained: camera + core + license
end
The Capture Vision framework already ships DSCameraEnhancer / DSCameraView, so no
separate DynamsoftCameraEnhancer pod is needed — adding one would register a second
DSCameraEnhancer class and live frames would never reach the router (the camera would
stay on “Scanning…” while file decoding still works). The bundle framework is also
self-contained: it embeds the Core and License modules and its binary declares no load
dependency on any other Dynamsoft framework, so DynamsoftCore / DynamsoftLicense must
not be added as separate pods. Doing so registers dozens of duplicate Objective-C classes
at launch (Class DSxxx is implemented in both ... One of the two will be used in the
console) and dyld then picks one implementation at random, which breaks the camera preview
and the drawing layers in hard-to-debug ways. Run pod install once before opening
App.xcworkspace:
cd ios/App
pod install
open App.xcworkspace # use the workspace, not the .xcodeproj
The Info.plist already declares NSCameraUsageDescription.
Step 5: Run it
# Android
npx cap run android --target=<device-id>
# iOS
npx cap run ios
When the camera scanner is on screen every detected barcode is outlined immediately, the Confirm button becomes enabled, and tapping it returns the full list back to the web layer. The file picker opens the system gallery; whichever image is picked is decoded with the same multi-format preset template and the result is rendered as a list.

Common Issues & Edge Cases
- Nothing happens when the camera is opened. Check that the
CAMERApermission is granted (the manifest already declares it, but the runtime grant happens on first use). On Android 13+ theREAD_MEDIA_IMAGESpermission is also required for the gallery picker. - The contour never shows up. The scanner only draws when at least one barcode is decoded in the current frame. Aim at a single barcode first, then try multi-barcode scenarios.
- The preview turns black as soon as a contour is drawn (iOS). The overlay is not going
through the SDK drawing layer. A hand-drawn
UIViewdefaults toisOpaque = true, so the un-painted pixels of its backing store composite as opaque black over the preview oncedraw(_:)strokes the first path — and the rawitem.locationpoints are video-frame coordinates that need conversion. UseQuadDrawingItemon aCameraViewdrawing layer (see Step 4). Class DSxxx is implemented in both ...at launch (iOS). Two copies of the SDK are linked — typically standaloneDynamsoftCore/DynamsoftLicensepods next to the bundle pod. Remove them (see Step 4); duplicate class registration makes dyld pick one implementation at random.- The plugin call rejects with “canceled”. The user dismissed the system photo picker or the native camera scanner; no state was changed.
- “Not implemented” in the web browser. That is intentional. The plugin is a native-only bridge; it has no web fallback in this project.
Source Code
Get the complete sample project source code on GitHub:
examples/ionic-vue-qr-code-scanner