Извлечение всех Exif-данных из медиафайлов в ReactJS и Nodejs с помощью библиотеки Exiftool

Я делаю этот пост, чтобы извлечь все метаданные из файла с помощью exiftools в nodejs.

что такое данные exif

Формат данных файлов изображений с возможностью обмена — это стандарт, определяющий форматы файлов изображений, звука и вспомогательных тегов, используемых цифровыми камерами, сканерами и другими системами, работающими с файлами изображений и звука, записанными цифровыми камерами.

бэкэнд

использование и установка библиотеки

  • экспресс
  • 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");
});
Войти в полноэкранный режим Выход из полноэкранного режима

Фронтенд

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; 
}       

Войти в полноэкранный режим Выход из полноэкранного режима

ответ

{
  "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
}
Ввести полноэкранный режим Выход из полноэкранного режима

этот инструмент для извлечения внутренних данных медиафайлов, которые не видны в файле.

Оцените статью
devanswers.ru
Добавить комментарий