Ionic Document Scanner in React

Dynamic Web TWAIN has a new scanDocument API since v17.2, which makes it easy to capture documents using mobile cameras with its document border detection feature. Along with the RemoteScan feature that connects physical document scanners, we can now build a decent mobile document scanner with web technologies.

In this article, we are going to create a mobile document scanning app using Ionic React. Ionic is a cross-platform framework for developing mobile apps with web technologies. An Ionic app can not only run as a web app in browsers but also run as a native app on Android and iOS.

Screenshot of the final result:

Home

An online demo running on netlify: link.

What You Should Know About Dynamic Web TWAIN

Build an Ionic Document Scanner

Let’s build the app in steps.

New project

First, install Ionic according to its guide.

After installation, create a new project:

ionic start documentScanner tabs --type react

We can run ionic serve to start a test server.

Install Dynamic Web TWAIN

  1. Install the npm package mobile-web-capture:

     npm install mobile-web-capture
    
  2. Download Dynamic Web TWAIN, install it and put its Resources folder in the project’s public folder.

PS: The version of mobile-web-capture for this article is 17.2.5.

Create a Home Page and a Settings Page

We need two pages. A home page which displays scanned documents and provides all kinds of operations and a settings page where we can configure RemoteScan.

  1. Create Home.tsx and Settings.tsx under pages with the following template content:

    import Home from './pages/Home';
    import Settings from './pages/Settings';
       
    const Home: React.FC<RouteComponentProps> = (props:RouteComponentProps) => {
      return (
       <IonPage>
          <IonHeader>
            <IonToolbar>
              <IonTitle slot="start">Docs Scan</IonTitle>
            </IonToolbar>
          </IonHeader>
          <IonContent>
          </IonContent>
        </IonPage>
      );
    }
    export default Home;
    
  2. Manage the navigation in App.tsx:

    const App: React.FC = () => (
      <IonApp>
        <IonReactRouter>
        <IonRouterOutlet>
            <Route path="/" component={Home} exact={true} />
            <Route path="/settings" component={Settings} exact={true} />
          </IonRouterOutlet>
        </IonReactRouter>
      </IonApp>
    );
    

Create a Scanner Component and Add it to the Home Page

We need to create a component to use Dynamic Web TWAIN in React.

Create Scanner.tsx under components with the following content:

import { useEffect, useRef } from "react";
import Dynamsoft from 'mobile-web-capture';
import { WebTwain } from "mobile-web-capture/dist/types/WebTwain";
import { ThumbnailViewer } from "mobile-web-capture/dist/types/WebTwain.Viewer";

interface props {
  license?:string;
  width?: string|number;
  height?: string|number;
}

let DWObject:WebTwain | undefined;
let thumbnail:ThumbnailViewer | undefined;

const Scanner: React.FC<props> = (props: props) => {
  const containerID = "dwtcontrolContainer";
  
  const container = useRef<HTMLDivElement>(null);
  
  const OnWebTWAINReady = () => {
    DWObject = Dynamsoft.DWT.GetWebTwain(containerID);
    if (container.current) {
      if (props.height) {
        DWObject.Viewer.height = props.height;
        container.current.style.height = props.height as string;
      }
      if (props.width) {
        DWObject.Viewer.width = props.width;
        container.current.style.width = props.width as string;
      }
    }

    let thumbnailViewerSettings = {
        location: 'left',
        size: '100%',
        columns: 2,
        rows: 3,
        scrollDirection: 'vertical', // 'horizontal'
        pageMargin: 10,
        background: "rgb(255, 255, 255)",
        border: '',
        allowKeyboardControl: true,
        allowPageDragging: true,
        allowResizing: false,
        showPageNumber: true,
        pageBackground: "transparent",
        pageBorder: "1px solid rgb(238, 238, 238)",
        hoverBackground: "rgb(239, 246, 253)",
        hoverPageBorder: "1px solid rgb(238, 238, 238)",
        placeholderBackground: "rgb(251, 236, 136)",
        selectedPageBorder: "1px solid rgb(125,162,206)",
        selectedPageBackground: "rgb(199, 222, 252)"
    };
    thumbnail = DWObject.Viewer.createThumbnailViewer(thumbnailViewerSettings);
    thumbnail.show();
  }

  useEffect(() => {
    console.log("on mount");
    Dynamsoft.DWT.RegisterEvent('OnWebTwainReady', () => {
      OnWebTWAINReady();
    });
    if (props.license) {
      Dynamsoft.DWT.ProductKey = props.license;
    }
    Dynamsoft.DWT.UseLocalService = false;
    Dynamsoft.DWT.Containers = [{
        WebTwainId: 'dwtObject',
        ContainerId: containerID,
        Width: '300px',
        Height: '400px'
    }];
    Dynamsoft.DWT.Load();
  }, []);

  return (
    <div ref={container} id={containerID}></div>
  );
}
  
export default Scanner;

In the above code, we load Dynamic Web TWAIN and display a viewer after it is ready to use. Here, ThumbnailViewer is used since it supports long tapping to reorder and multiple selection, which is suitable for mobile devices.

We can now use the component in the Home page. You may need to apply for a license to use Dynamic Web TWAIN.

const Home: React.FC<RouteComponentProps> = (props:RouteComponentProps) => {
  return (
   <IonPage>
      <IonHeader>
        <IonToolbar>
          <IonTitle slot="start">Docs Scan</IonTitle>
        </IonToolbar>
      </IonHeader>
      <IonContent>
+       <Scanner
+          width={"100%"} 
+          height={"100%"} 
+          license="your license key"
+        />
      </IonContent>
    </IonPage>
  );
}
export default Home;

Scan Documents with Camera

  1. Add a scan prop to the Scanner component.

    interface props {
      license?:string;
      width?: string|number;
      height?: string|number;
    + scan?: boolean;
    }
    
  2. When the scan property is set to true, start document scanning with cameras.

     useEffect(() => {
       if (props.scan == true) {
         if (DWObject) {
           let cameraContainer = document.createElement("div");
           cameraContainer.className = "fullscreen";
           document.body.appendChild(cameraContainer);
    
           const funcConfirmExit = (bExistImage:boolean):boolean => {
             cameraContainer.remove();
             return true;
           }
           let showVideoConfigs:ScanConfiguration = {
             element: cameraContainer,
             scannerViewer:{
               autoDetect:{
                 enableAutoDetect: false
               },
               funcConfirmExit: funcConfirmExit,
               continuousScan:{
                 visibility: false,
                 enableContinuousScan: false,
               }
             },
             filterViewer: {
               exitDocumentScanAfterSave: false
             }
           };
           DWObject.Addon.Camera.scanDocument(showVideoConfigs).then(
             function(){
               console.log("OK");
             }, 
             function(error){
               console.log(error.message);
             });
         }
       }
     }, [props.scan]);
    

    A full-screen container is appended to body and the camera view will be bound to it. When the camera is closed by the users, the container will be removed.

    Scanner.css under styles:

     .fullscreen {
       position: absolute;
       left:0;
       top:0;
       width: 100%;
       height: 100%;
     }
    
  3. Create a floating action button in Home page to start scan and import the CSS.

    Added content to Home.tsx:

     import "../styles/Scanner.css";
        
     //...
        
     const [scan,setScan] = useState(false);
     const resetScanStateDelayed = () => {
       const reset = () => {
         setScan(false);
       }
       setTimeout(reset,1000);
     }
        
     //...
        
     return (
      <IonPage>
         <IonHeader>
           <IonToolbar>
             <IonTitle slot="start">Docs Scan</IonTitle>
           </IonToolbar>
         </IonHeader>
         <IonContent>
           <Scanner
             scan={scan}
             width={"100%"} 
             height={"100%"} 
             license="your license key"
           />
           <IonFab style= vertical="bottom" horizontal="start" slot="fixed">
             <IonFabButton onClick={() => {
               setScan(true);
               resetScanStateDelayed();
             }} >
               <IonIcon icon={cameraOutline} />
             </IonFabButton>
           </IonFab>
         </IonContent>
       </IonPage>
     );
    

We can now test scanning documents with cameras. Please note that camera access needs localhost or https.

Camera

Scan Documents with RemoteScan

RemoteScan makes it possible to scan documents from physical document scanners.

We need to have a desktop device which runs Dynamsoft Service and make it accessible via an Intranet. Then, we can scan documents on other devices by communicating with that device. You can learn about how to configure it here.

Let’s add RemoteScan to the app.

  1. Add seven props to the Scanner component.

    interface props {
      license?:string;
      width?: string|number;
      height?: string|number;
      scan?: boolean;
    + deviceConfig?: DeviceConfiguration;
    + remoteScan?: boolean;
    + remoteIP?: string;
    + onWebTWAINReady?: (dwt:WebTwain) => void;
    + onScannerListLoaded?: (list:string[]) => void;
    + onScanned?: (success:boolean) => void;
    + onRemoteServiceConnected?: (success:boolean) => void;
    }
    
  2. After Dynamic Web TWAIN is loaded and the remote IP address is set, create a new DWObjectRemote and bind it with DWObject.

    const OnWebTWAINReady = () => {
       DWObject = Dynamsoft.DWT.GetWebTwain(containerID);
       if (props.onWebTWAINReady) {
         props.onWebTWAINReady(DWObject);
       }
       //...
     };
    
     const initializeDWObjectRemote = () => {
       Dynamsoft.DWT.DeleteDWTObject("remoteScan");
       DWObjectRemote = undefined;
       if (props.remoteIP == "") {
         return;
       }
       if (props.remoteIP) {
         console.log("initializing");
         var dwtConfig = {
           WebTwainId: "remoteScan",
           Host: props.remoteIP,
           Port: "18622",
           PortSSL: "18623",
           UseLocalService: "true",
         };
         Dynamsoft.DWT.CreateDWTObjectEx(
           dwtConfig,
           function (dwt) {
             DWObjectRemote = dwt;
             bindDWObjects();
             console.log("service connected!");
             // List the available scanners
             DWObjectRemote.GetSourceNamesAsync(false).then(
               function (devices) {
                 let scanners:string[] = [];
                 for (let i = 0; i < devices.length; i++) {
                     scanners.push(devices[i].toString());
                 }
                 if (props.onScannerListLoaded){
                   props.onScannerListLoaded(scanners);
                 }
               },
               function (error){
                 console.log(error);
               }
             );
           },
           function (error) {
             console.log(error);
           }
         );
       }
     }
    
     const bindDWObjects = () => {
       if (DWObjectRemote && DWObject) {
         DWObjectRemote.RegisterEvent("OnPostTransferAsync", function (outputInfo) {
           DWObjectRemote!.ConvertToBlob(
             [DWObjectRemote!.ImageIDToIndex(outputInfo.imageId)],
             Dynamsoft.DWT.EnumDWT_ImageType.IT_PNG,
             function (result, indices, type) {
               DWObject!.LoadImageFromBinary(
                 result,
                 function () {
                   console.log("LoadImageFromBinary success");
                   DWObjectRemote!.RemoveImage(
                     DWObjectRemote!.ImageIDToIndex(outputInfo.imageId)
                   );
                 },
                 function (errorCode, errorString) {
                   console.log(errorString);
                 }
               );
             },
             function (errorCode, errorString) {
               console.log(errorString);
             }
           );
         });
       }
     }
        
     useEffect(() => {
       initializeDWObjectRemote();
     }, [props.remoteIP]);
    

    DWObjectRemote communicates with a remote Dynamsoft Service to scan documents. After it fetched an image, it will pass the image to DWObject and display the image in the viewer. The image stored in the remote Dynamsoft Service will be deleted after the scanning process is completed.

    When the remoteScan prop is set to true, start remote scan:

     useEffect(() => {
       if (props.remoteScan == true) {
         if (DWObjectRemote) {
           const OnAcquireImageSuccess = function () {
             if (props.onScanned) {
               props.onScanned(true);
             }
             DWObjectRemote!.CloseSource();
           };
           const OnAcquireImageFailure = function () {
             if (props.onScanned) {
               props.onScanned(false);
             }
             DWObjectRemote!.CloseSource();
           };
    
           let deviceConfiguration:DeviceConfiguration;
              
           if (props.deviceConfig) {
             deviceConfiguration = props.deviceConfig;
           }else{
             deviceConfiguration = {
               SelectSourceByIndex: 0,
               IfShowUI: false,
               PixelType: Dynamsoft.DWT.EnumDWT_PixelType.TWPT_RGB,
               Resolution: 300,
               IfFeederEnabled: false,
               IfDuplexEnabled: false,
               IfDisableSourceAfterAcquire: true,
               RemoteScan: true,
               ShowRemoteScanUI: false,
             };
           }
    
           DWObjectRemote.AcquireImage(
             deviceConfiguration,
             OnAcquireImageSuccess,
             OnAcquireImageFailure
           );
         } else {
           if (props.onScanned) {
             props.onScanned(false);
           }
         }
       }
     }, [props.remoteScan]);
    
  3. In Home page, add the newly added props to the Scanner component:

     const [remoteScan,setRemoteScan] = useState(false);
     const [remoteIP,setRemoteIP] = useState(""); // leave the value empty
     const [deviceConfiguration, setDeviceConfiguration] = useState<DeviceConfiguration|undefined>(undefined);
        
     //......
    
     <Scanner scan={scan} 
       remoteScan={remoteScan} 
       width={"100%"} 
       height={"100%"} 
       license="your license key"
       remoteIP={remoteIP}
       deviceConfig={deviceConfiguration}
       onWebTWAINReady={(dwt) =>{ DWObject = dwt; loadSettings(); }}
       onScannerListLoaded={onScannerListLoaded} 
       onRemoteServiceConnected={(success) =>{
         if (success == false) {
           localStorage.removeItem("IP");
         }
       }}
       onScanned={(success) => {
         if (success == false) {
           alert("Failed. Please check your settings.");
         }
       }} 
     />
    

    After Web TWAIN is ready, load settings.

     const loadSettings = () => {
       const IP = localStorage.getItem("IP");
       if (IP) {
         setRemoteIP(IP);
       }
     }
    

    Currently, the only setting is the remote IP. We will add more settings like which scanner to use later.

    If the remote service is not available, remove the remoteIP item stored in local storage.

     onRemoteServiceConnected={(success) =>{
       if (success == false) {
         localStorage.removeItem("IP");
       }
     }}
    
  4. In Home page, add a floating action button to start remote scan:

     <IonFabButton style= onClick={() => {
       setRemoteScan(true);
       resetScanStateDelayed();
     }} >
       <IonIcon icon={documentOutline} />
     </IonFabButton>
    

We can now set a remote IP and have a test:

window.localStorage.setItem("IP","192.168.8.65");

Remote Scan

Settings

In the settings page, add options related to remote scan.

Settings

The ScanSettings interface has keys like resolution, pixel type, which scanner to use, whether to show a configuration UI, whether to enable duplex scan and auto document feeder.

export interface ScanSettings{
  selectedIndex: number;
  showUI: boolean;
  autoFeeder: boolean;
  duplex: boolean;
  resolution: number;
  pixelType: number;
}

If the save button is clicked, save the settings and return to the Home page. The scan settings and remote IP are stored in local storage.

const save = () =>{
  const selectedIndex = Math.max(0, scanners.indexOf(selectedScanner));
  let scanSettings: ScanSettings = {
    selectedIndex: selectedIndex,
    showUI: showUI,
    autoFeeder: autoFeeder,
    duplex: duplex,
    resolution: resolution,
    pixelType: pixelType
  }
  localStorage.setItem("settings",JSON.stringify(scanSettings));
  if (IP) {
    localStorage.setItem("IP",IP);
  }
  props.history.replace({state:{settingsSaved:true}});
  props.history.goBack();
};

In the Home page, load the settings if the settings are changed:

useEffect(() => {
  const state = props.location.state as { settingsSaved:boolean };
  if (state && state.settingsSaved == true) {
    loadSettings();
  }
}, [props.location.state]);

const loadSettings = () => {
  const settingsAsJSON = localStorage.getItem("settings");
  if (settingsAsJSON) {
    let settings:ScanSettings = JSON.parse(settingsAsJSON);
    let deviceConfig:DeviceConfiguration = {
      SelectSourceByIndex: settings.selectedIndex,
      ShowRemoteScanUI: settings.showUI,
      IfShowUI: settings.showUI,
      IfFeederEnabled: settings.autoFeeder,
      IfDuplexEnabled: settings.duplex,
      PixelType: settings.pixelType,
      Resolution: settings.resolution,
      RemoteScan: true
    }
    setDeviceConfiguration(deviceConfig);
  }

  const IP = localStorage.getItem("IP");
  if (IP) {
    setRemoteIP(IP);
  }
}

Edit Scanned Documents

Documents can be edited after scanning like cropping, rotation and deleting.

Action

Show Image Editor

  1. Add a showEditor prop to Scanner.

    interface props {
    + showEditor?: boolean;
    }
    
  2. Show image editor if props.showEditor is true.

     useEffect(() => {
       if (props.showEditor == true) {
         if (DWObject) {
    
           let settings:EditorSettings = {};
    
           let editorContainer = document.createElement("div");
           editorContainer.className = "fullscreen";
           document.body.appendChild(editorContainer);
           settings.element = editorContainer as HTMLDivElement;
           settings.width = "100%";
           settings.height = "100%";
    
           let imageEditor = DWObject.Viewer.createImageEditor(settings);
           imageEditor.show();
    
           const onImageEditorUIClosed = () => {
             editorContainer.remove();
           };
           DWObject.RegisterEvent('CloseImageEditorUI', onImageEditorUIClosed);
         }
       }
     }, [props.showEditor]);
    
  3. In Home page, add a floating action button to show an action sheet:

     <IonFab style= vertical="bottom" horizontal="end" slot="fixed">
       <IonFabButton onClick={showImageActionSheet}>
         <IonIcon icon={ellipsisVerticalOutline} />
       </IonFabButton>
     </IonFab>
    
  4. Users can select which action to do using the sheet:

     const showImageActionSheet = () => {
       const deleteSelected = () => {
         if (DWObject) {
           DWObject.RemoveAllSelectedImages();
         }
       }
    
       const editSelected = () => {
         if (DWObject) {
           setShowEditor(true);
         }
         const reset = () => {
           setShowEditor(false);
         }
         setTimeout(reset,1000);
       }
    
       present({
         buttons: [{ text: 'Edit selected', handler: editSelected }, 
                   { text: 'Delete selected', handler: deleteSelected }, 
                   { text: 'Cancel' } ],
         header: 'Select an action'
       })
     }
    

The image editor:

ImageEditor

Multiple Selection

We can let the thumbnail viewer show checkboxes so that users can select multiple images on mobile devices.

  1. Add a showCheckbox prop to Scanner.

    interface props {
    +  showCheckbox?: boolean;
    }
    
  2. Update the thumbnail viewer’s showCheckbox property when props.showCheckbox is changed.

     useEffect(() => {
       if (thumbnail && props.showCheckbox != undefined) {
         thumbnail.showCheckbox = props.showCheckbox;
       }
     }, [props.showCheckbox]);
    
  3. Add a toggle selection item to the image editing action sheet.

     const [showCheckbox,setShowCheckbox] = useState(false);
     //...
     const toggleMultipleSelection = () => {
       setShowCheckbox(!showCheckbox);
     }
        
     present({
       buttons: [{ text: 'Toggle multiple selection', handler: toggleMultipleSelection }, 
                 { text: 'Delete selected', handler: deleteSelected }, 
                 { text: 'Edit selected', handler: editSelected }, 
                 { text: 'Cancel' } ],
       header: 'Select an action'
     })
    

Screenshot when checkboxes are shown:

Multiple selection

Export Scanned Documents

After documents are scanned and edited, we can export them.

We can save them as a PDF file and download it or share it.

  1. In Home page, add a share button on the toolbar.

     <IonButton onClick={showShareActionSheet} color="secondary">
       <IonIcon slot="icon-only"  icon={shareOutline} />
     </IonButton>
    
  2. The showShareActionSheet function:

     const showShareActionSheet = () => {
       const save = () => {
         if (DWObject) {
           const OnSuccess = () => {
             console.log('successful');
           }
        
           const OnFailure = () => {
             console.log('error');
           }
           DWObject.SaveAllAsPDF("Scanned.pdf",OnSuccess,OnFailure);
         }
       }
    
       const share = () => {
         console.log("share");
         const success = async (result:Blob, indices:number[], type:number) => {
           let pdf:File = new File([result],"scanned.pdf");
           const data:ShareData = {files:[pdf]};
           await navigator.share(data);
         }
            
         const failure = (errorCode:number, errorString:string) => {
           console.log(errorString);
         }
         if (DWObject) {
           DWObject.ConvertToBlob(getImageIndices(),Dynamsoft.DWT.EnumDWT_ImageType.IT_PDF,success,failure)
         }
       }
          
       const getImageIndices = () => {
         var indices = [];
         if (DWObject) {
           for (var i=0;i<DWObject.HowManyImagesInBuffer;i++){
             indices.push(i)
           }
         }
         return indices;
       }
          
       present({
         buttons: [{ text: 'Save as PDF', handler:save }, { text: 'Export to PDF and share', handler:share }, { text: 'Cancel' } ],
         header: 'Select an action'
       })
     }
    

Export

Share

Load Existing Documents

We can also load previously scanned documents to the current viewer using the LoadImageEx API.

We can add a floating action button to call this API. It can load PDF files as well as images. We can use an action sheet for users to select the desired file format.

  1. Add the floating action button in JSX:

     <IonFabButton onClick={loadFile} >
       <IonIcon icon={imageOutline} />
     </IonFabButton>
    
  2. The loadFile function:

     const loadFile = () => {
       if (DWObject) {
         present({
           buttons: 
           [{ text: 'PDF', handler: () => {
             DWObject.LoadImageEx("", Dynamsoft.DWT.EnumDWT_ImageType.IT_PDF);
           } }, 
           { text: 'Image', handler: () => {
             DWObject.LoadImageEx("", Dynamsoft.DWT.EnumDWT_ImageType.IT_ALL);
           }}, 
           { text: 'Cancel' } ],
           header: 'Select file type'
         })
       }
     }
    

Import

All right, we’ve finished building the ionic document scanner.

Turn the Document Scanner to Android/iOS Native Apps

It is also possible to make the scanner run as a native mobile app using Ionic.

Setup Android and iOS Platforms

  1. Add Android and iOS projects:

     ionic capacitor add android
     ionic capacitor add ios
    
  2. Add permissions in project files:

    For Android, add the following to android\app\src\mainAndroidManifest.xml:

     <uses-permission android:name="android.permission.CAMERA" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    

    For iOS, add the following to ios\App\App\Info.plist:

     <key>NSCameraUsageDescription</key>
     <string>For document scanning</string>
    
  3. Request camera permission for Android when the app is started:

     useEffect(() => {
       console.log("on mount");
       if (isPlatform("android")) {
         checkAndRequestCameraPermission();
       }
     }, []);
        
     const checkAndRequestCameraPermission = async () => {
       let result = await AndroidPermissions.checkPermission(AndroidPermissions.PERMISSION.CAMERA);
       if (result.hasPermission == false) {
         let response = await AndroidPermissions.requestPermission(AndroidPermissions.PERMISSION.CAMERA);
         console.log(response.hasPermission);
       }
     }
    
  4. Set Resources path for iOS.

    Web TWAIN uses WebAssembly for all kinds of operations. WebAssembly needs HTTP protocol while Capacitor for iOS uses the capacitor:// protocol. We need to set the Resources path to use HTTP.

    In Scanner.tsx, add the following:

    + const RemoteResourcesPath = "https://unpkg.com/dwt@17.2.5/dist";
    + if (isPlatform("ios") == true) {
    +   Dynamsoft.DWT.ResourcesPath = RemoteResourcesPath;
    + }
      Dynamsoft.DWT.Load();
    
  5. Set scheme and cleartext so that RemoteScan works on Android.

    {
      "appId": "io.ionic.starter",
      "appName": "document-scanner",
      "webDir": "build",
      "server":{
        "hostname": "localhost",
    +   "androidScheme": "http",
    +   "cleartext": true
      },
      "bundledWebRuntime": false
    }
    

    On Android, all cleartext traffic is disabled by default as of API 28. Because the remote service is accessible via http://ip:18622 or https://ip:18623 and the latter cannot use a valid certificate, we need to use unencrypted HTTP traffic.

  6. Update styles\Scanner.css to avoid blocking the status bar:

    .fullscreen {
      position: absolute;
      left:0;
      top:0;
    + top: env(safe-area-inset-top);
      width: 100%;
      height: 100%;
    + height: calc(100% - env(safe-area-inset-top));
    }
    
  7. Run with Capacitor​:

     ionic capacitor run android
     ionic capacitor run ios
    

Use Native Plugins for Export

Saving and sharing are not handled correctly in WebView. We need to use native plugins to do these.

For saving as a PDF file, we convert the documents to a PDF file and save it to the external directory.

if (Capacitor.isNativePlatform()) {
  const OnSuccess = async (result:Base64Result, indices:number[], type:number) => {
    console.log('successful');
    let writingResult = await Filesystem.writeFile({
      path: getFormattedDate()+".pdf",
      data: result.getData(0,result.getLength()),
      directory: Directory.External
    })
    await Toast.show({
      text: "File is written to "+writingResult.uri,
      duration: "long"
    });
  }

  const OnFailure = () => {
    console.log('error');
  }
  DWObject.ConvertToBase64(getImageIndices(),Dynamsoft.DWT.EnumDWT_ImageType.IT_PDF,OnSuccess,OnFailure)
  
}

For sharing, we convert the documents to a PDF file, save it to the cache directory and then share it:

if (Capacitor.isNativePlatform()) {
  const success = async (result:Base64Result, indices:number[], type:number) => {
    let fileName = getFormattedDate()+".pdf";
    let writingResult = await Filesystem.writeFile({
      path: fileName,
      data: result.getData(0,result.getLength()),
      directory: Directory.Cache
    });
    Share.share({
      title: fileName,
      text: fileName,
      url: writingResult.uri,
    });
  }
  
  const failure = (errorCode:number, errorString:string) => {
    console.log(errorString);
  }

  if (DWObject) {
    DWObject.ConvertToBase64(getImageIndices(),Dynamsoft.DWT.EnumDWT_ImageType.IT_PDF,success,failure)
  }
}

Source Code

https://github.com/xulihang/Ionic-React-Document-Scanner