Online Image Converter

Dynamic Web TWAIN is a document-scanning SDK which makes it possible to access document scanners in the browser. It comes with the support of various image formats. We can use it to create an on-premises online image converter. You can press the button above to have a try. It has the following features:

  • Supported input formats: BMP, JPEG, PNG, TIFF, PDF, ZIP
  • Supported output formats: JPEG, PNG, TIFF, PDF, ZIP
  • Batch conversion
  • Client-side processing which does not require a server

In the following, we are going to talk about how it is made.

New Project

Create a new project with Vite and the typescript template:

npm create vite@latest ImageConverter -- --template vanilla-ts

Add Dependencies

Install Dynamic Web TWAIN and JSZip.

npm install dwt@18.0.2 jszip

Image Converter Class

Next, create an image-converter.ts file for the ImageConverter class.

export class ImageConverter {
  constructor() {}
}

Initialize Dynamic Web TWAIN

In the constructor, initialize an instance of Dynamic Web TWAIN. A config param is used in the constructor for passing related params like the license. You can apply for a license here.

export interface Config {
  license?:string;
}

export class ImageConverter {
  private DWObject!:WebTwain;
  constructor(config?:Config) {
    if (config) {
      if (config.license) {
        Dynamsoft.DWT.ProductKey = config.license;
      }
    }
    this.initDWT();
  }
  
  initDWT(){
    Dynamsoft.DWT.AutoLoad = false;
    Dynamsoft.DWT.Containers = [];
    Dynamsoft.DWT.ResourcesPath = "https://unpkg.com/dwt@18.0.2/dist";
    Dynamsoft.DWT.UseLocalService = false;
    let pThis = this;
    Dynamsoft.DWT.CreateDWTObjectEx(
      {
        WebTwainId: 'dwtcontrol'
      },
      function(obj:WebTwain) {
        pThis.DWObject = obj;
      },
      function(err:string) {
        console.log(err);
      }
    );
  }
}

New Convert Function

Next, create a new function for converting an image file. It does the following:

  1. Use Web TWAIN’s LoadImageFromBinary to load an image file into the buffer of Web TWAIN.
  2. Use Web TWAIN’s ConvertToBlob function to convert the image into the desired format.
  3. The function returns an array of image files because PDF or TIFF can have multiple images in one file.

Code:

export enum ImageFormat {
  JPG = 0,
  PNG = 1,
  PDF = 2,
  TIFF = 3
}

export interface ConvertedFile {
  filename:string;
  blob:Blob;
}

export class ImageConverter {
  private DWObject!:WebTwain;
  async convert(file:File,targetFormat:ImageFormat){
    let files:ConvertedFile[] = [];
    this.DWObject.RemoveAllImages();
    await this.loadImageFromFile(file);
    let fileType = 7;
    let extension = "";
    if (targetFormat === ImageFormat.JPG) {
      fileType = Dynamsoft.DWT.EnumDWT_ImageType.IT_JPG;
      extension = ".jpg";
    }else if (targetFormat === ImageFormat.PNG) {
      fileType = Dynamsoft.DWT.EnumDWT_ImageType.IT_PNG;
      extension = ".png";
    }else if (targetFormat === ImageFormat.PDF) {
      fileType = Dynamsoft.DWT.EnumDWT_ImageType.IT_PDF;
      extension = ".pdf";
    }else if (targetFormat === ImageFormat.TIFF) {
      fileType = Dynamsoft.DWT.EnumDWT_ImageType.IT_TIF;
      extension = ".tiff";
    }
    if (targetFormat > 1) {
      let blob = await this.getBlob(this.getImageIndices(),fileType);
      files.push({filename:this.getFileNameWithoutExtension(file.name)+extension,blob:blob})
    }else{
      if (this.DWObject.HowManyImagesInBuffer > 1) {
        for (let index = 0; index < this.DWObject.HowManyImagesInBuffer; index++) {
          let blob = await this.getBlob([index],fileType);
          files.push({filename:this.getFileNameWithoutExtension(file.name)+"-"+index+extension,blob:blob})
        }
      }else{
        let blob = await this.getBlob([0],fileType);
        files.push({filename:this.getFileNameWithoutExtension(file.name)+extension,blob:blob})
      }
    }
    return files;
  }
  
  async loadImageFromFile(file:File){
    return new Promise<void>((resolve, reject) => {
      this.DWObject.LoadImageFromBinary(file,
        function(){
          resolve();
        },
        function(_errorCode: number, errorString: string){
          reject(errorString);
        }
      )
    })
  }
  
  getBlob(indices:number[],type:number){
    return new Promise<Blob>((resolve, reject) => {
      this.DWObject.ConvertToBlob(indices,type,
        function(blob:Blob){
          resolve(blob);
        },
        function(_errorCode: number, errorString: string){
          reject(errorString);
        }
      )  
    })
  }
  
  getImageIndices(){
    let indices = [];
    for (let index = 0; index < this.DWObject.HowManyImagesInBuffer; index++) {
      indices.push(index);
    }
    return indices;
  }
  
  getFileNameWithoutExtension(filename:string){
    if (filename.lastIndexOf(".") != -1) {
      return filename.substring(0,filename.lastIndexOf("."));
    }else{
      return filename;
    }
  }
}

Define a UI for the Converter

For ease of use, we can build a UI for the converter. The UI can be bound to a container.

Here is a screenshot of the final result:

screenshot

  1. Add a container item to the config.

     export interface Config {
       license?:string;
    +  container?:HTMLDivElement;
     }
    
  2. In the constructor, bind the UI elements to the container.

    constructor(config?:Config) {
      if (config) {
        if (config.container) {
          this.container = config.container;
          this.createElements();
        }
        if (config.license) {
          Dynamsoft.DWT.ProductKey = config.license;
        }
      }
      this.initDWT();
    }
       
    createElements(){
      let pThis = this;
      this.files = document.createElement("div");
      let actionsContainer = document.createElement("div");
      actionsContainer.className = styles.actions;
      let chooseFilesContainer = document.createElement("div");
      this.fileInput = document.createElement("input");
      this.fileInput.style.display = "none";
      this.fileInput.multiple = true;
      this.fileInput.accept = ".bmp,.jpeg,.jpg,.png,.pdf,.tiff,.tif,.zip";
      this.fileInput.type = "file";
      this.fileInput.addEventListener("change",async function(){
        pThis.chooseFilesButton.innerText = "Loading...";
        await pThis.appendFiles();
        pThis.listFiles();
        pThis.chooseFilesButton.innerText = "Choose Files";
      })
      this.chooseFilesButton = this.DynamsoftButton("Choose Files");
      this.chooseFilesButton.addEventListener("click",function(){
        pThis.fileInput!.click();
      });
      chooseFilesContainer.appendChild(this.fileInput);
      chooseFilesContainer.appendChild(this.chooseFilesButton);
      actionsContainer.appendChild(chooseFilesContainer);
      this.convertActions = document.createElement("div");
      this.convertActions.className = styles.convertActions;
      this.convertActions.style.display = "none";
      let formatSelector = document.createElement("label");
      formatSelector.innerText = "To:"
      this.formatSelect = document.createElement("select");
      for (const format of ["JPG","PNG","PDF","TIFF"]) {
        this.formatSelect.options.add(new Option(format,format));
      }
      formatSelector.appendChild(this.formatSelect);
      let useZip = document.createElement("label");
      useZip.innerText = "Download as Zip:"
      useZip.style.marginLeft = "10px";
      this.useZipCheckbox = document.createElement("input");
      this.useZipCheckbox.type = "checkbox";
      useZip.appendChild(this.useZipCheckbox);
      this.convertButton = this.DynamsoftButton("Convert");
      this.convertButton.style.marginLeft = "10px";
      this.convertButton.addEventListener("click",function(){
        pThis.convertAndDownload();
      })
      this.convertActions.appendChild(formatSelector);
      this.convertActions.appendChild(useZip);
      this.convertActions.appendChild(this.convertButton);
      actionsContainer.appendChild(this.convertActions);
      this.container.appendChild(this.files);
      this.container.appendChild(actionsContainer);
    }
    

    The styles are defined in a CSS module named style.module.css (here, we use CSS module to avoid class names conflicts):

    .oneFile {
      background: #F5F5F5;
      display: flex;
      padding: 1em;
      align-items: center;
      justify-content: space-between;
    }
    
    .title {
      width: 30%;
      word-break: break-all;
    }
    
    .convertActions {
      padding-right: 1em;
      text-align: right;
    }
    
    .actions {
      display: flex;
      align-items: center;
      justify-content: space-between;
      margin-top: 10px;
    }
    
    .textUpperCase {
      text-transform: uppercase;
    }
    
    .primaryBtn {
      display: inline-block;
      padding: 5px 10px;
      background-color: #fe8e14;
      color: #fff;
      text-align: center;
      border: 2px solid #fe8e14;
      cursor: pointer;
      transition: ease-in 0.2s all;
      font-family: "sans-serif";
    }
    
    @media(any-hover:hover){
      .primaryBtn:hover {
        box-shadow: -4px 4px 0 0 #000;
        transform: translate(4px,-4px);
      }
      .secondaryBtn:hover {
        color: #fea543;
      }
    }
    
    .primaryBtn:active {
      color: #fea543;
    }
    
  3. Append selected files in a files array. If the file is a zip file, then add the image files in it.

    private filesSelected:File[] = [];
    async appendFiles(){
      if (this.fileInput.files) {
        for (let index = 0; index < this.fileInput.files.length; index++) {
          let file = this.fileInput.files[index];
          if (file.name.endsWith(".zip")) {
            this.useZipCheckbox.checked = true;
            await this.loadImagesFromZip(file);
          }else{
            this.filesSelected.push(file);
          }
             
        }
      }
    }
       
    async loadImagesFromZip(zipFile:File){
      const buffer = await zipFile.arrayBuffer();
      let zip = new JSZip();
      await zip.loadAsync(buffer);
      const files = zip.files;
      const filenames = Object.keys(files);
      for (let index = 0; index < filenames.length; index++) {
        const filename = filenames[index];
        const lowerCase = filename.toLowerCase();
        const file = files[filename];
        if (file.dir === false) {
          if (lowerCase.endsWith(".jpg") || lowerCase.endsWith(".jpeg") || lowerCase.endsWith(".png") || lowerCase.endsWith(".bmp") || lowerCase.endsWith(".pdf") || lowerCase.endsWith(".tif") || lowerCase.endsWith(".tiff")) {
            let blob:Blob = await file.async("blob");
            let imgFile = new File([blob],filename);
            this.filesSelected.push(imgFile);
          }
        }
      }
    }
    
  4. List selected files. A file item displays the name and the size of the file and a button to delete the file. If the name is too long, use ellipses to make it shorter.

    listFiles(){
      this.files.innerHTML = "";
      for (let index = 0; index < this.filesSelected.length; index++) {
        const file = this.filesSelected[index];
        let oneFile = this.fileItem(file);
        if (index != this.filesSelected.length - 1) {
          oneFile.style.marginBottom = "10px";
        }
        this.files.appendChild(oneFile);
      }
      if (this.filesSelected.length > 0) {
        this.convertActions.style.display = "";
      }else{
        this.convertActions.style.display = "none";
      }
    }
    
    fileItem(file:File){
      let container = document.createElement("div");
      container.className = styles.oneFile;
      let title = document.createElement("div");
      title.innerText = this.useEllipsesForLongText(file.name);
      title.className = styles.title;
      let fileSize = document.createElement("div");
      fileSize.innerText = file.size/1000 + "kb";
      let deleteButton = this.DynamsoftButton("Delete");
      let pThis = this;
      deleteButton.addEventListener("click",function(){
        pThis.deleteSelected(file);
      })
      container.appendChild(title);
      container.appendChild(fileSize);
      container.appendChild(deleteButton);
      return container;
    }
    
    deleteSelected(file:File){
      let index = this.filesSelected.indexOf(file);
      this.filesSelected.splice(index,1);
      this.listFiles();
    }
    
    useEllipsesForLongText(text:string){
      if (text.length>28){
        text = text.substring(0,14) + "..." + text.substring(text.length-14,text.length);
      }
      return text;
    }
    
  5. If the convert button is clicked, convert the files and download them. If the Download as zip checkbox is checked, then download the files in a zip.

    async convertAndDownload(){
      this.convertButton.innerText = "Converting...";
      let zip:JSZip|undefined;
      if (this.useZipCheckbox.checked) {
        zip = new JSZip();
      }
      for (let index = 0; index < this.filesSelected.length; index++) {
        const file = this.filesSelected[index];
        this.DWObject.RemoveAllImages();
        await this.loadImageFromFile(file);
        await this.save(file,zip);
      }
      if (this.useZipCheckbox.checked && zip) {
        let pThis = this;
        zip.generateAsync({type:"blob"}).then(function(content) {
          pThis.downloadBlob(content,"images.zip");
        });
      }
      this.convertButton.innerText = "Convert";
    }
       
    async save(file:File,zip:JSZip|undefined){
      if (this.useZipCheckbox.checked === false) {
        await this.saveImages(file);
      }else{
        if (zip) {
          await this.appendImagesToZip(file,zip);
        }
      }
    }
       
    async saveImages(file:File){
      let selectedFormatIndex = this.formatSelect.selectedIndex;
      //"JPG","PNG","PDF","TIFF"
      let fileType = 7;
      let extension = "";
      if (selectedFormatIndex === 0) {
        fileType = Dynamsoft.DWT.EnumDWT_ImageType.IT_JPG;
        extension = ".jpg";
      }else if (selectedFormatIndex === 1) {
        fileType = Dynamsoft.DWT.EnumDWT_ImageType.IT_PNG;
        extension = ".png";
      }else if (selectedFormatIndex === 2) {
        fileType = Dynamsoft.DWT.EnumDWT_ImageType.IT_PDF;
        extension = ".pdf";
      }else if (selectedFormatIndex === 3) {
        fileType = Dynamsoft.DWT.EnumDWT_ImageType.IT_TIF;
        extension = ".tiff";
      }
      if (selectedFormatIndex > 1) {
        let blob = await this.getBlob(this.getImageIndices(),fileType);
        this.downloadBlob(blob,this.getFileNameWithoutExtension(file.name)+extension);
      }else{
        if (this.DWObject.HowManyImagesInBuffer > 1) {
          for (let index = 0; index < this.DWObject.HowManyImagesInBuffer; index++) {
            let blob = await this.getBlob([index],fileType);
            this.downloadBlob(blob,this.getFileNameWithoutExtension(file.name)+"-"+index+extension);
          }
        }else{
          let blob = await this.getBlob([0],fileType);
          this.downloadBlob(blob,this.getFileNameWithoutExtension(file.name)+extension);
        }
      }
    }
    
    async appendImagesToZip(file:File,zip:JSZip){
      let selectedFormatIndex = this.formatSelect.selectedIndex;
      //"JPG","PNG","PDF","TIFF"
      let fileType = 7;
      let extension = "";
      if (selectedFormatIndex === 0) {
        fileType = Dynamsoft.DWT.EnumDWT_ImageType.IT_JPG;
        extension = ".jpg";
      }else if (selectedFormatIndex === 1) {
        fileType = Dynamsoft.DWT.EnumDWT_ImageType.IT_PNG;
        extension = ".png";
      }else if (selectedFormatIndex === 2) {
        fileType = Dynamsoft.DWT.EnumDWT_ImageType.IT_PDF;
        extension = ".pdf";
      }else if (selectedFormatIndex === 3) {
        fileType = Dynamsoft.DWT.EnumDWT_ImageType.IT_TIF;
        extension = ".tiff";
      }
      if (selectedFormatIndex > 1) {
        let blob = await this.getBlob(this.getImageIndices(),fileType);
        zip.file(this.getFileNameWithoutExtension(file.name)+extension, blob);
      }else{
        if (this.DWObject.HowManyImagesInBuffer > 1) {
          for (let index = 0; index < this.DWObject.HowManyImagesInBuffer; index++) {
            let blob = await this.getBlob([index],fileType);
            zip.file(this.getFileNameWithoutExtension(file.name)+"-"+index+extension, blob);
          }
        }else{
          let blob = await this.getBlob([0],fileType);
          zip.file(this.getFileNameWithoutExtension(file.name)+extension, blob);
        }
      }
    }
       
    downloadBlob(content:Blob,filename:string){
      const link = document.createElement('a')
      link.href = URL.createObjectURL(content);
      link.download = filename;
      document.body.appendChild(link)
      link.click()
      document.body.removeChild(link)
    }
    

All right, we’ve now finished writing the demo.

Package as a Library

We can package the image converter as a library according to this guide.

  1. Install devDependencies:

    npm install -D @types/node vite-plugin-dts
    
  2. Create a new vite.config.ts file:

    // vite.config.ts
    import { resolve } from 'path';
    import { defineConfig } from 'vite';
    import dts from 'vite-plugin-dts';
    // https://vitejs.dev/guide/build.html#library-mode
    export default defineConfig({
      build: {
        lib: {
          entry: resolve(__dirname, 'src/index.ts'),
          name: 'image-converter',
          fileName: 'image-converter',
        },
      },
      plugins: [dts()],
    });
    
  3. Add the entry points of our package to package.json.

    {
      "name": "image-converter",
      "version": "0.0.0",
      "private": false,
      "type": "module",
      "main": "./dist/image-converter.umd.cjs",
      "module": "./dist/image-converter.js",
      "types": "./dist/index.d.ts",
      "exports": {
        ".": {
          "import": "./dist/image-converter.js",
          "require": "./dist/image-converter.cjs"
        }
      },
      "files": [
        "dist/*.css",
        "dist/*.js",
        "dist/*.cjs",
        "dist/*.d.ts"
      ],
    }
    

Run npm run build. Then, we can have the packaged files in the dist:

dist
    image-converter.d.ts
    image-converter.js
    image-converter.umd.cjs
    index.d.ts
    main.d.ts
    style.css

Source Code

Get the source code of the library to have a try:

https://github.com/tony-xlh/Image-Converter

Other Demos