DEV Community

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

Posted on

5 2 1 1 1

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.

Billboard image

Monitor more than uptime.

With Checkly, you can use Playwright tests and Javascript to monitor end-to-end scenarios in your NextJS, Astro, Remix, or other application.

Get started now!

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.

Cloudinary image

Video API: manage, encode, and optimize for any device, channel or network condition. Deliver branded video experiences in minutes and get deep engagement insights.

Learn more

👋 Kindness is contagious

Immerse yourself in a wealth of knowledge with this piece, supported by the inclusive DEV Community—every developer, no matter where they are in their journey, is invited to contribute to our collective wisdom.

A simple “thank you” goes a long way—express your gratitude below in the comments!

Gathering insights enriches our journey on DEV and fortifies our community ties. Did you find this article valuable? Taking a moment to thank the author can have a significant impact.

Okay