DEV Community 👩‍💻👨‍💻

Cover image for Extract All Exif Data from media files in ReactJS and Nodejs Using Exiftool library
Sandeep
Sandeep

Posted on

Extract All Exif Data from media files in ReactJS and Nodejs Using Exiftool library

I am making this post to capture all meta data from file using exiftools in nodejs.

what is exif data

Exchangeable image file data format is a standard that specifies formats for images file, sound, and ancillary tags used by digital cameras, scanners and other systems handling image and sound files recorded by digital cameras.

backend

these library use and install

  • express
  • mongoose
  • cors
  • node-exiftool
  • dist-exiftool
  • multer

package.json

{
  "name": "metadata-extractor",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "cors": "^2.8.5",
    "dist-exiftool": "^10.53.0",
    "express": "^4.18.1",
    "mongoose": "^6.4.6",
    "multer": "^1.4.5-lts.1",
    "node-exiftool": "^2.3.0"
  }
}

Enter fullscreen mode Exit fullscreen mode

server.js

const express=require('express');
const cors=require('cors');

const metaDataRoute=require('./routes/metadata.route')
require('./services/connection');
const app=express();
const PORT=process.env.PORT|5000;

app.use(express.json());
app.use(cors());
app.use(express.static(__dirname+"./public"));

app.use('/api',metaDataRoute);

app.listen(PORT,()=>{
    console.log(`server is listening on port ${PORT}`)
})
Enter fullscreen mode Exit fullscreen mode

metadata.controller.js

const exiftoolBin = require('dist-exiftool');
const exiftool =  require('node-exiftool');
const fs = require('fs');
const path = require('path');
const MetaDataModel=require("../models/meta.models");

module.exports.createMetaData=(req,res,next)=>{
    try {
        if(!req.file){
            return res.status(404).send({message:"File Not Found",status:404})
        }
    const PHOTO_PATH = path.join(__dirname, '../public/upload/'+req.file.filename)
    const rs = fs.createReadStream(PHOTO_PATH)
    const ep = new exiftool.ExiftoolProcess(exiftoolBin)
    ep.open()
      .then(() => ep.readMetadata(rs, ['-File:all']))
      .then(async (result) => {
          let metadata=new MetaDataModel({
              fileName:req.file.filename,
              originalName:req.file.originalname,
              size:req.file.size,
              information:result.data[0]
              });
          metadata=await metadata.save();
          return res.send(metadata);
    })
    .then(() => ep.close(), () => ep.close())
    .catch(console.error);

    } catch (error) {
        next(error);
    }
}

module.exports.getAllMetaData=async (req,res,next)=>{
    try {
        let allData=await MetaDataModel.find({}).sort({createdAt:-1});
        res.send(allData)
    } catch (error) {
        next(error)
    }
}

module.exports.deleteMetaData=async (req,res,next)=>{
    try {
        let metadata=await MetaDataModel.findOneAndDelete({_id:req.params.id});
        if(!metadata){
            return res.status(400).send({message:"Metadata not exist"})
        }
        const PHOTO_PATH = path.join(__dirname, '../public/upload/'+metadata.fileName)
        fs.unlink(PHOTO_PATH,(err,data)=>{
            if(err){

            }
        })
        res.send({message:"Deleted Successfully",status:200});
    } catch (error) {
        next(error)
    }
}
Enter fullscreen mode Exit fullscreen mode

middlewares/uploadFile.middleware.js

const multer=require('multer');
const path=require('path');

const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, path.join(__dirname,'../public/upload/'))
  },
  filename: function (req, file, cb) {
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9)
    cb(null, file.fieldname + '-'+uniqueSuffix+'-' +file.originalname)
  }
})

const upload = multer({ storage: storage }).single('file');

module.exports.uploadFile=(req,res,next)=>{
try {
      upload(req, res, function (err) {
    if (err instanceof multer.MulterError) {
        return res.status(400).send({message:"Error: "+err,status:400})
    } else if (err) {
       return res.status(400).send({message:"Error: "+err,status:400})
    }
    next()
  })
} catch (error) {
    next(error);
}
}
Enter fullscreen mode Exit fullscreen mode

middlewres/validateObjectId.middleware.js

const mongoose=require('mongoose');

module.exports.validateObjectId=(req,res,next)=>{
    try {
        if(!mongoose.Types.ObjectId.isValid(req.params.id)){
            return res.status(400).send({message:"Invalid Id"});
        }
        next()
    } catch (error) {
        next(error)
    }
}
Enter fullscreen mode Exit fullscreen mode

metadata.model.js

const mongoose=require('mongoose');

const {Schema,model}=mongoose;

const metaDataSchema=new Schema({
    fileName:{
        type:String,
        required:true,
        index:true
    },
    originalName:{
        type:String
    },
    size:{
        type:Number
    },
    information:{
        type:Object
    }
},{timestamps:true});

const MetaDataModel=model('metadata',metaDataSchema);

module.exports=MetaDataModel;

Enter fullscreen mode Exit fullscreen mode

metadata.route.js

const express=require('express');

const router=express.Router();

const metaDataController=require("../controllers/metadata.controller");
const {uploadFile}=require("../middlewares/uploadFile.middleware");
const {validateObjectId}=require('../middlewares/validateObjectId.middleware')

router.post('/metadata',uploadFile, metaDataController.createMetaData);

router.get('/metadata',metaDataController.getAllMetaData);

router.delete('/metadata/:id',validateObjectId,metaDataController.deleteMetaData);

module.exports=router
Enter fullscreen mode Exit fullscreen mode

services/connection.js

const mongoose=require('mongoose')

module.exports=mongoose.connect('mongodb://localhost:27017/metadata').then((conn)=>{
    console.log('Database Connected');
}).catch((err)=>{
    console.log("Database not connected");
});
Enter fullscreen mode Exit fullscreen mode

Frontend

App.js

import {useState,useEffect} from 'react';
import './App.css';
import Accordian from './Accordian'
import axios from 'axios';

function App() {
  const [file,setFile]= useState();
  const [metaData,setMetadata]=useState([]);
  const BASE_URL='http://localhost:5000/api';
  useEffect(() => {
    getAllData();
  }, [])

  const getAllData=async()=>{
    let data=await axios.get(BASE_URL+'/metadata');
    setMetadata(data?.data);
    console.log(data.data)
  }

  const handleSubmit=async(event)=>{
    event.preventDefault();
    let formData=new FormData();
    formData.append("file",file);
    let uploadMetaData=await axios.post(BASE_URL+'/metadata',formData);
    if(uploadMetaData){
      let metadata=[uploadMetaData.data,...metaData];
      setMetadata(metadata);
      setFile()
    }
  }

  const deleteMetaData=async(id)=>{
    let doc=await axios.delete(BASE_URL+'/metadata/'+id);
    if(doc){
      let metadata=metaData.filter(m=>m._id!==id);
      setMetadata(metadata);
    }
  }
  return (
    <>
    <div className="page-header mt-5">
       <h1>Extract File Upload Control </h1>
    </div>


<div className="container mt-5">
    <div className="">
    </div>
    <div className="col-md-6 mx-auto mt-4">
        <form onSubmit={handleSubmit} method="post" encType="multipart/form-data">
            <input type="file" id="files" onChange={(e)=>setFile(e.target.files[0])} className="form-control" name="files" />
            <p className="mt-4">
                <input type="submit" value="Upload File" disabled={!file} className="btn btn-primary" />
            </p>
        </form>
    </div>
    <div className="col-md-4"></div>
</div>

<div className="col-md-6 mx-auto">
<div className="accordion" id="accordionExample">
      {
        metaData.length>0 && metaData.map((data,index)=>(
      <div key={index}>    
     <Accordian data={data} deleteMetaData={deleteMetaData}></Accordian>
</div>

        ))
      }
     </div> 
</div>
</>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

Accordian.jsx

import React,{useState} from 'react'

const Accordian = ({data,deleteMetaData}) => {
    const [show,setShow]=useState(false);

    const deleteMeta=(id)=>{
      setShow(false);
      deleteMetaData(id);
    }

  return (
    <div className="accordion-item">
    <h2 className="accordion-header" id={'#heading'+data.fileName}>
      <button className="accordion-button" onClick={()=>setShow(!show)} type="button">
        {data?.originalName}
      </button>
    </h2>
    {show &&<div  className="accordion-collapse"  >
      <div className="accordion-body">
        <pre style={{overflowWrap:'break-word'}}>
           {JSON.stringify(data.information,null,2)}
        </pre>
        <div className="text-end">
        <button className="btn btn-danger btn-sm " onClick={()=>deleteMeta(data?._id)}>Delete</button>
        </div>
      </div>
    </div>}
  </div>
  )
}

export default Accordian
Enter fullscreen mode Exit fullscreen mode

App.css

h1{
  text-align: center;
}    
p{
text-align: center; margin-top: 20px; 
}       

Enter fullscreen mode Exit fullscreen mode

response

{
  "SourceFile": "C:/Users/DELL/AppData/Local/Temp/wrote-93199.data",
  "ExifToolVersion": 10.53,
  "Model": "Redmi 5A",
  "ModifyDate": "2019:10:01 16:12:33",
  "YCbCrPositioning": "Centered",
  "ISO": 100,
  "ExposureProgram": "Not Defined",
  "FNumber": 2,
  "ExposureTime": "1/117",
  "SensingMethod": "One-chip color area",
  "SubSecTimeDigitized": "033060",
  "SubSecTimeOriginal": "033060",
  "SubSecTime": "033060",
  "FocalLength": "2.6 mm",
  "Flash": "Off, Did not fire",
  "MeteringMode": "Center-weighted average",
  "SceneCaptureType": "Standard",
  "InteropIndex": "R98 - DCF basic file (sRGB)",
  "InteropVersion": "0100",
  "FocalLengthIn35mmFormat": "3 mm",
  "CreateDate": "2019:10:01 16:12:33",
  "ExifImageHeight": 2592,
  "WhiteBalance": "Auto",
  "DateTimeOriginal": "2019:10:01 16:12:33",
  "BrightnessValue": 3.92,
  "ExifImageWidth": 1944,
  "ExposureMode": "Auto",
  "ApertureValue": 2,
  "ComponentsConfiguration": "Y, Cb, Cr, -",
  "ColorSpace": "sRGB",
  "SceneType": "Directly photographed",
  "ShutterSpeedValue": "1/117",
  "ExifVersion": "0220",
  "FlashpixVersion": "0100",
  "ResolutionUnit": "inches",
  "GPSLatitudeRef": "North",
  "GPSLongitudeRef": "East",
  "GPSAltitudeRef": "Unknown (2)",
  "GPSTimeStamp": "10:42:24",
  "GPSProcessingMethod": "ASCII",
  "GPSDateStamp": "2019:10:01",
  "XResolution": 72,
  "YResolution": 72,
  "Make": "Xiaomi",
  "ThumbnailOffset": 1040,
  "ThumbnailLength": 15180,
  "Compression": "JPEG (old-style)",
  "Aperture": 2,
  "GPSAltitude": "0 m Above Sea Level",
  "GPSDateTime": "2019:10:01 10:42:24Z",
  "GPSLatitude": "25 deg 42' 21.34\" N",
  "GPSLongitude": "81 deg 46' 35.33\" E",
  "GPSPosition": "25 deg 42' 21.34\" N, 81 deg 46' 35.33\" E",
  "ImageSize": "1944x2592",
  "Megapixels": 5,
  "ScaleFactor35efl": 1.1,
  "ShutterSpeed": "1/117",
  "SubSecCreateDate": "2019:10:01 16:12:33.033060",
  "SubSecDateTimeOriginal": "2019:10:01 16:12:33.033060",
  "SubSecModifyDate": "2019:10:01 16:12:33.033060",
  "ThumbnailImage": "(Binary data 15180 bytes, use -b option to extract)",
  "CircleOfConfusion": "0.026 mm",
  "FOV": "161.1 deg",
  "FocalLength35efl": "2.6 mm (35 mm equivalent: 3.0 mm)",
  "HyperfocalDistance": "0.13 m",
  "LightValue": 8.9
}
Enter fullscreen mode Exit fullscreen mode

Image description

this tool to extract media files inner data that not see in file.

Top comments (1)

Collapse
 
tonfotos profile image
Andrey

node-exiftool looks to be quite popular, but for my project that was not an option, as it requires complicated dependencies management - you should install separate command utility on Windows, or even go with full blown perl installation on other platforms. That's why I decided to go with exif-parser - much less popular, but is pure JS library, no dependencies whatsoever.

🌚 Life is too short to browse without dark mode