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"
}
}
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}`)
})
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)
}
}
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);
}
}
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)
}
}
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;
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
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");
});
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;
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
App.css
h1{
text-align: center;
}
p{
text-align: center; margin-top: 20px;
}
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
}
this tool to extract media files inner data that not see in file.
Top comments (1)
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 withexif-parser
- much less popular, but is pure JS library, no dependencies whatsoever.