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:
- Use Web TWAIN’s LoadImageFromBinary to load an image file into the buffer of Web TWAIN.
- Use Web TWAIN’s ConvertToBlob function to convert the image into the desired format.
- 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:
-
Add a container item to the config.
export interface Config { license?:string; + container?:HTMLDivElement; }
-
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; }
-
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); } } } }
-
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; }
-
If the
convert
button is clicked, convert the files and download them. If theDownload 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.
-
Install
devDependencies
:npm install -D @types/node vite-plugin-dts
-
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()], });
-
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