Extract Metadata from Images in the Browser Using JavaScript and EXIF

Reading EXIF metadata directly from image files in the browser is a powerful feature — whether you're building a photo uploader, a metadata inspector, or a tool to strip sensitive data before sharing. In this tutorial, you'll build a native JavaScript EXIF parser using no external libraries and organize your code cleanly into HTML, CSS, and JS files.
✅ What You’ll Learn
- How to structure your project using separate files
- How to parse EXIF metadata from JPEG files
- How to use
FileReader
,ArrayBuffer
, andDataView
- How to extract fields like Camera Model, Date Taken, and Orientation
📁 Step 1: Create the Project Structure
Start by creating a new folder, e.g., js-exif-parser
, and inside it, create the following files:
1 js-exif-parser/2 ├── index.html3 ├── style.css4 └── script.js
📄 Step 2: Write the HTML (index.html
)
This file provides the structure: a file input and a placeholder to show the EXIF results.
1 <!doctype html>2 <html lang="en">3 <head>4 <meta charset="UTF-8" />5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />6 <title>EXIF Data Viewer</title>7 <link rel="stylesheet" href="style.css" />8 </head>9 <body>10 <div class="container">11 <h1>EXIF Data Viewer</h1>1213 <div class="drop-area" id="dropArea">14 <div class="icon">📁</div>15 <p>Drag & drop an image here</p>16 <p>or</p>17 <div class="file-input-wrapper">18 <span class="browse-button">Browse Files</span>19 <input20 type="file"21 class="file-input"22 id="imageInput"23 accept="image/*"24 />25 </div>26 </div>2728 <div class="image-preview hidden" id="imagePreview">29 <div class="preview-container" id="previewContainer">30 <!-- Image preview will be shown here -->31 </div>32 <div class="exif-data hidden" id="exifDataDisplay">33 <!-- EXIF data will be displayed here -->34 </div>35 </div>36 </div>3738 <!-- Loader -->39 <div class="loader-container" id="loaderContainer">40 <div class="loader"></div>41 <div class="loader-text">Processing EXIF data...</div>42 </div>4344 <script src="script.js"></script>45 </body>46 </html>
This HTML file sets up a user interface to upload or drag-and-drop an image, preview it, and then display its EXIF metadata using a separate JavaScript file (script.js
).
🎨 Step 3: Add Styling (style.css
)
1 :root {2 --primary-color: #3498db;3 --secondary-color: #2980b9;4 --success-color: #2ecc71;5 --danger-color: #e74c3c;6 --text-color: #333;7 --light-bg: #f5f5f5;8 --border-color: #ddd;9 }1011 * {12 box-sizing: border-box;13 margin: 0;14 padding: 0;15 }1617 body {18 font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;19 line-height: 1.6;20 color: var(--text-color);21 background-color: var(--light-bg);22 padding: 20px;23 }2425 .container {26 max-width: 1000px;27 margin: 0 auto;28 background-color: white;29 padding: 30px;30 border-radius: 8px;31 box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);32 }3334 h1 {35 text-align: center;36 margin-bottom: 30px;37 color: var(--primary-color);38 }3940 .drop-area {41 border: 3px dashed var(--border-color);42 border-radius: 8px;43 padding: 50px 20px;44 text-align: center;45 margin-bottom: 30px;46 background-color: var(--light-bg);47 transition: all 0.3s ease;48 cursor: pointer;49 }5051 .drop-area.drag-over {52 border-color: var(--primary-color);53 background-color: rgba(52, 152, 219, 0.1);54 }5556 .drop-area p {57 font-size: 18px;58 margin-bottom: 15px;59 }6061 .drop-area .icon {62 font-size: 48px;63 color: var(--primary-color);64 margin-bottom: 15px;65 }6667 .file-input-wrapper {68 position: relative;69 display: inline-block;70 }7172 .file-input {73 position: absolute;74 left: 0;75 top: 0;76 opacity: 0;77 width: 100%;78 height: 100%;79 cursor: pointer;80 }8182 .browse-button {83 display: inline-block;84 background-color: var(--primary-color);85 color: white;86 padding: 10px 25px;87 border-radius: 4px;88 cursor: pointer;89 font-weight: 500;90 transition: background-color 0.3s;91 }9293 .browse-button:hover {94 background-color: var(--secondary-color);95 }9697 .image-preview {98 display: flex;99 justify-content: space-between;100 margin-bottom: 30px;101 height: 300px;102 }103104 .preview-container {105 flex: 0 0 48%;106 height: 100%;107 border-radius: 8px;108 overflow: hidden;109 background-color: var(--light-bg);110 position: relative;111 display: flex;112 align-items: center;113 justify-content: center;114 }115116 .preview-container img {117 max-width: 100%;118 max-height: 100%;119 object-fit: contain;120 }121122 .exif-data {123 flex: 0 0 48%;124 height: 100%;125 overflow-y: auto;126 padding: 15px;127 border: 1px solid var(--border-color);128 border-radius: 8px;129 }130131 .exif-data.hidden,132 .image-preview.hidden {133 display: none;134 }135136 .exif-group {137 margin-bottom: 15px;138 }139140 .exif-group h3 {141 margin-bottom: 8px;142 padding-bottom: 5px;143 border-bottom: 1px solid var(--border-color);144 color: var(--primary-color);145 }146147 .exif-item {148 display: flex;149 justify-content: space-between;150 margin-bottom: 5px;151 padding: 5px 0;152 border-bottom: 1px dashed var(--border-color);153 }154155 .exif-label {156 font-weight: 500;157 flex: 0 0 50%;158 }159160 .exif-value {161 flex: 0 0 50%;162 text-align: right;163 word-break: break-word;164 }165166 /* Loader styles */167 .loader-container {168 display: none;169 position: fixed;170 top: 0;171 left: 0;172 width: 100%;173 height: 100%;174 background-color: rgba(255, 255, 255, 0.7);175 z-index: 1000;176 justify-content: center;177 align-items: center;178 }179180 .loader {181 border: 5px solid var(--light-bg);182 border-top: 5px solid var(--primary-color);183 border-radius: 50%;184 width: 50px;185 height: 50px;186 animation: spin 1s linear infinite;187 }188189 .loader-text {190 margin-left: 15px;191 font-size: 18px;192 font-weight: 500;193 }194195 @keyframes spin {196 0% {197 transform: rotate(0deg);198 }199 100% {200 transform: rotate(360deg);201 }202 }203204 .no-exif-message {205 text-align: center;206 padding: 20px;207 color: var(--danger-color);208 font-weight: 500;209 }210211 .gps-map {212 height: 200px;213 margin-top: 20px;214 background-color: var(--light-bg);215 border-radius: 8px;216 display: flex;217 align-items: center;218 justify-content: center;219 }220221 .section-title {222 margin: 30px 0 15px;223 font-size: 20px;224 color: var(--primary-color);225 }226227 @media (max-width: 768px) {228 .image-preview {229 flex-direction: column;230 height: auto;231 }232233 .preview-container,234 .exif-data {235 flex: 0 0 100%;236 width: 100%;237 margin-bottom: 20px;238 }239240 .preview-container {241 height: 250px;242 }243244 .exif-data {245 height: 300px;246 }247 }
⚙️ Step 4: Write the JavaScript (script.js)
1 class ExifService {2 constructor() {3 this.tiffHeaderOffset = null;4 this.bigEndian = null;5 this.tags = {6 // EXIF tags and their meanings7 0x0100: "ImageWidth",8 0x0101: "ImageHeight",9 0x0102: "BitsPerSample",10 0x0103: "Compression",11 0x010e: "ImageDescription",12 0x010f: "Make",13 0x0110: "Model",14 0x0112: "Orientation",15 0x011a: "XResolution",16 0x011b: "YResolution",17 0x0128: "ResolutionUnit",18 0x0131: "Software",19 0x0132: "DateTime",20 0x013b: "Artist",21 0x0213: "YCbCrPositioning",22 0x8298: "Copyright",23 0x8769: "ExifIFDPointer",24 0x8825: "GPSInfoIFDPointer",2526 // EXIF IFD tags27 0x829a: "ExposureTime",28 0x829d: "FNumber",29 0x8822: "ExposureProgram",30 0x8827: "ISOSpeedRatings",31 0x9000: "ExifVersion",32 0x9003: "DateTimeOriginal",33 0x9004: "DateTimeDigitized",34 0x9201: "ShutterSpeedValue",35 0x9202: "ApertureValue",36 0x9203: "BrightnessValue",37 0x9204: "ExposureBiasValue",38 0x9205: "MaxApertureValue",39 0x9206: "SubjectDistance",40 0x9207: "MeteringMode",41 0x9208: "LightSource",42 0x9209: "Flash",43 0x920a: "FocalLength",44 0x927c: "MakerNote",45 0x9286: "UserComment",46 0xa000: "FlashpixVersion",47 0xa001: "ColorSpace",48 0xa002: "PixelXDimension",49 0xa003: "PixelYDimension",50 0xa004: "RelatedSoundFile",51 0xa005: "InteroperabilityIFDPointer",52 0xa20e: "FocalPlaneXResolution",53 0xa20f: "FocalPlaneYResolution",54 0xa210: "FocalPlaneResolutionUnit",55 0xa217: "SensingMethod",56 0xa300: "FileSource",57 0xa301: "SceneType",58 0xa302: "CFAPattern",59 0xa401: "CustomRendered",60 0xa402: "ExposureMode",61 0xa403: "WhiteBalance",62 0xa404: "DigitalZoomRatio",63 0xa405: "FocalLengthIn35mmFilm",64 0xa406: "SceneCaptureType",65 0xa407: "GainControl",66 0xa408: "Contrast",67 0xa409: "Saturation",68 0xa40a: "Sharpness",69 0xa40b: "DeviceSettingDescription",70 0xa40c: "SubjectDistanceRange",7172 // GPS tags73 0x0000: "GPSVersionID",74 0x0001: "GPSLatitudeRef",75 0x0002: "GPSLatitude",76 0x0003: "GPSLongitudeRef",77 0x0004: "GPSLongitude",78 0x0005: "GPSAltitudeRef",79 0x0006: "GPSAltitude",80 0x0007: "GPSTimeStamp",81 0x0008: "GPSSatellites",82 0x0009: "GPSStatus",83 0x000a: "GPSMeasureMode",84 0x000b: "GPSDOP",85 0x000c: "GPSSpeedRef",86 0x000d: "GPSSpeed",87 0x000e: "GPSTrackRef",88 0x000f: "GPSTrack",89 0x0010: "GPSImgDirectionRef",90 0x0011: "GPSImgDirection",91 0x0012: "GPSMapDatum",92 0x0013: "GPSDestLatitudeRef",93 0x0014: "GPSDestLatitude",94 0x0015: "GPSDestLongitudeRef",95 0x0016: "GPSDestLongitude",96 0x0017: "GPSDestBearingRef",97 0x0018: "GPSDestBearing",98 0x0019: "GPSDestDistanceRef",99 0x001a: "GPSDestDistance",100 0x001b: "GPSProcessingMethod",101 0x001c: "GPSAreaInformation",102 0x001d: "GPSDateStamp",103 0x001e: "GPSDifferential",104 };105106 // Orientation values107 this.orientationDescriptions = {108 1: "Normal",109 2: "Mirrored horizontally",110 3: "Rotated 180°",111 4: "Mirrored vertically",112 5: "Mirrored horizontally and rotated 90° CCW",113 6: "Rotated 90° CW",114 7: "Mirrored horizontally and rotated 90° CW",115 8: "Rotated 90° CCW",116 };117 }118119 /**120 * Process an image file and extract its EXIF data121 * @param {File|Blob} imageFile - The image file to process122 * @returns {Promise<Object>} - A promise that resolves to an object containing EXIF data123 */124 extractExifData(imageFile) {125 return new Promise((resolve, reject) => {126 if (127 !imageFile ||128 !["image/jpeg", "image/jpg", "image/tiff"].includes(imageFile.type)129 ) {130 reject(131 new Error(132 "Unsupported file type. Only JPEG and TIFF formats support EXIF."133 )134 );135 return;136 }137138 const reader = new FileReader();139140 reader.onload = (e) => {141 try {142 const buffer = e.target.result;143 const dataView = new DataView(buffer);144145 // Check for JPEG format146 if (dataView.getUint8(0) !== 0xff || dataView.getUint8(1) !== 0xd8) {147 reject(new Error("Not a valid JPEG"));148 return;149 }150151 const exifData = this.parseExifData(dataView);152 resolve(exifData);153 } catch (error) {154 reject(new Error(`Error processing EXIF data: ${error.message}`));155 }156 };157158 reader.onerror = () => {159 reject(new Error("Error reading file"));160 };161162 reader.readAsArrayBuffer(imageFile);163 });164 }165166 /**167 * Parse EXIF data from a DataView object168 * @param {DataView} dataView - The DataView containing the image data169 * @returns {Object} - An object containing the parsed EXIF data170 */171 parseExifData(dataView) {172 const length = dataView.byteLength;173 let offset = 2; // Skip the first two bytes (JPEG marker)174 const exifData = {};175176 // Search for the EXIF APP1 marker (0xFFE1)177 while (offset < length) {178 if (dataView.getUint8(offset) !== 0xff) {179 offset++;180 continue;181 }182183 const marker = dataView.getUint8(offset + 1);184185 // If we found the EXIF APP1 marker186 if (marker === 0xe1) {187 const exifOffset = offset + 4; // Skip marker and size188 const exifLength = dataView.getUint16(offset + 2, false);189190 // Check for "Exif\0\0" header191 if (192 this.getStringFromCharCodes(dataView, exifOffset, 6) === "Exif\0\0"193 ) {194 return this.readExifTags(dataView, exifOffset + 6, exifLength - 6);195 }196 } else if (marker === 0xda) {197 // Start of Scan marker - no more metadata198 break;199 }200201 // Move to the next marker202 offset += 2 + dataView.getUint16(offset + 2, false);203 }204205 return exifData;206 }207208 /**209 * Read EXIF tags from the specified offset210 * @param {DataView} dataView - The DataView containing the image data211 * @param {number} start - The offset to start reading from212 * @param {number} length - The length of data to read213 * @returns {Object} - An object containing the parsed EXIF tags214 */215 readExifTags(dataView, start, length) {216 const result = {};217218 // Determine endianness219 if (dataView.getUint16(start) === 0x4949) {220 this.bigEndian = false; // Little endian221 } else if (dataView.getUint16(start) === 0x4d4d) {222 this.bigEndian = true; // Big endian223 } else {224 throw new Error("Invalid TIFF data: endianness marker not found");225 }226227 // Check for TIFF marker228 if (dataView.getUint16(start + 2, !this.bigEndian) !== 0x002a) {229 throw new Error("Invalid TIFF data: TIFF marker not found");230 }231232 // Get offset to first IFD (Image File Directory)233 const firstIFDOffset = dataView.getUint32(start + 4, !this.bigEndian);234 this.tiffHeaderOffset = start;235236 // Read the main IFD237 const ifdData = this.readIFD(238 dataView,239 this.tiffHeaderOffset + firstIFDOffset240 );241 Object.assign(result, ifdData);242243 // Check for and read Exif sub-IFD if it exists244 if (ifdData.ExifIFDPointer) {245 const exifData = this.readIFD(246 dataView,247 this.tiffHeaderOffset + ifdData.ExifIFDPointer248 );249 result.exif = exifData;250 delete result.ExifIFDPointer;251 }252253 // Check for and read GPS sub-IFD if it exists254 if (ifdData.GPSInfoIFDPointer) {255 const gpsData = this.readIFD(256 dataView,257 this.tiffHeaderOffset + ifdData.GPSInfoIFDPointer258 );259 result.gps = gpsData;260 delete result.GPSInfoIFDPointer;261 }262263 // Add orientation description if orientation exists264 if (result.Orientation) {265 result.OrientationDescription =266 this.orientationDescriptions[result.Orientation] || "Unknown";267 }268269 return result;270 }271272 /**273 * Read an Image File Directory274 * @param {DataView} dataView - The DataView containing the image data275 * @param {number} offset - The offset to start reading from276 * @returns {Object} - An object containing the parsed IFD entries277 */278 readIFD(dataView, offset) {279 const result = {};280 const numEntries = dataView.getUint16(offset, !this.bigEndian);281282 for (let i = 0; i < numEntries; i++) {283 const entryOffset = offset + 2 + i * 12; // 12 bytes per entry284 const tagNumber = dataView.getUint16(entryOffset, !this.bigEndian);285 const dataType = dataView.getUint16(entryOffset + 2, !this.bigEndian);286 const numValues = dataView.getUint32(entryOffset + 4, !this.bigEndian);287 const valueOffset = dataView.getUint32(entryOffset + 8, !this.bigEndian);288289 // Get tag name if known, otherwise use hex code290 const tagName = this.tags[tagNumber] || `0x${tagNumber.toString(16)}`;291292 // Get tag value293 let tagValue;294 const totalSize = this.getDataTypeSize(dataType) * numValues;295296 if (totalSize <= 4) {297 // Value is stored in the offset field itself298 tagValue = this.readTagValue(299 dataView,300 entryOffset + 8,301 dataType,302 numValues303 );304 } else {305 // Value is stored at the specified offset306 tagValue = this.readTagValue(307 dataView,308 this.tiffHeaderOffset + valueOffset,309 dataType,310 numValues311 );312 }313314 result[tagName] = tagValue;315 }316317 return result;318 }319320 /**321 * Get the size in bytes of a specific data type322 * @param {number} dataType - The EXIF data type323 * @returns {number} - The size in bytes324 */325 getDataTypeSize(dataType) {326 const sizes = {327 1: 1, // BYTE328 2: 1, // ASCII329 3: 2, // SHORT330 4: 4, // LONG331 5: 8, // RATIONAL332 6: 1, // SBYTE333 7: 1, // UNDEFINED334 8: 2, // SSHORT335 9: 4, // SLONG336 10: 8, // SRATIONAL337 11: 4, // FLOAT338 12: 8, // DOUBLE339 };340341 return sizes[dataType] || 0;342 }343344 /**345 * Read a tag value based on its data type346 * @param {DataView} dataView - The DataView containing the image data347 * @param {number} offset - The offset to start reading from348 * @param {number} dataType - The data type of the value349 * @param {number} numValues - The number of values to read350 * @returns {*} - The parsed tag value351 */352 readTagValue(dataView, offset, dataType, numValues) {353 let values = [];354 let i, size;355356 switch (dataType) {357 case 1: // BYTE358 case 6: // SBYTE359 case 7: // UNDEFINED360 for (i = 0; i < numValues; i++) {361 values.push(dataView.getUint8(offset + i));362 }363 break;364365 case 2: // ASCII366 // Read null-terminated ASCII string367 const chars = [];368 for (i = 0; i < numValues; i++) {369 const charCode = dataView.getUint8(offset + i);370 if (charCode === 0) break;371 chars.push(charCode);372 }373 return String.fromCharCode(...chars);374375 case 3: // SHORT376 size = 2;377 for (i = 0; i < numValues; i++) {378 values.push(dataView.getUint16(offset + i * size, !this.bigEndian));379 }380 break;381382 case 8: // SSHORT383 size = 2;384 for (i = 0; i < numValues; i++) {385 values.push(dataView.getInt16(offset + i * size, !this.bigEndian));386 }387 break;388389 case 4: // LONG390 size = 4;391 for (i = 0; i < numValues; i++) {392 values.push(dataView.getUint32(offset + i * size, !this.bigEndian));393 }394 break;395396 case 9: // SLONG397 size = 4;398 for (i = 0; i < numValues; i++) {399 values.push(dataView.getInt32(offset + i * size, !this.bigEndian));400 }401 break;402403 case 5: // RATIONAL404 size = 8;405 for (i = 0; i < numValues; i++) {406 const numerator = dataView.getUint32(407 offset + i * size,408 !this.bigEndian409 );410 const denominator = dataView.getUint32(411 offset + i * size + 4,412 !this.bigEndian413 );414 values.push(numerator / denominator);415 }416 break;417418 case 10: // SRATIONAL419 size = 8;420 for (i = 0; i < numValues; i++) {421 const numerator = dataView.getInt32(422 offset + i * size,423 !this.bigEndian424 );425 const denominator = dataView.getInt32(426 offset + i * size + 4,427 !this.bigEndian428 );429 values.push(numerator / denominator);430 }431 break;432433 default:434 values = [dataType, numValues]; // Return the data type and count for unknown types435 }436437 // If there's only one value, return it directly instead of an array438 return numValues === 1 ? values[0] : values;439 }440441 /**442 * Convert array of character codes to a string443 * @param {DataView} dataView - The DataView containing the data444 * @param {number} start - The start offset445 * @param {number} length - The number of characters to read446 * @returns {string} - The resulting string447 */448 getStringFromCharCodes(dataView, start, length) {449 const chars = [];450 for (let i = 0; i < length; i++) {451 chars.push(dataView.getUint8(start + i));452 }453 return String.fromCharCode(...chars);454 }455 }456457 // DOM Elements458 const imageInput = document.getElementById("imageInput");459 const dropArea = document.getElementById("dropArea");460 const previewContainer = document.getElementById("previewContainer");461 const imagePreview = document.getElementById("imagePreview");462 const exifDataDisplay = document.getElementById("exifDataDisplay");463 const loaderContainer = document.getElementById("loaderContainer");464465 // Initialize EXIF service466 const exifService = new ExifService();467468 // Format tag names for display (convert camelCase to Title Case with spaces)469 function formatTagName(tag) {470 // Handle special cases471 if (tag === "FNumber") return "F-Number";472 if (tag === "ISOSpeedRatings") return "ISO";473474 // Add space before capital letters and capitalize first letter475 return tag476 .replace(/([A-Z])/g, " $1")477 .replace(/^./, (str) => str.toUpperCase())478 .trim();479 }480481 // Format tag values based on their type482 function formatTagValue(value, tag) {483 if (value === undefined || value === null) return "Unknown";484485 // Handle arrays486 if (Array.isArray(value)) {487 // For GPS coordinates488 if (tag === "GPSLatitude" || tag === "GPSLongitude") {489 return value.map((v) => v.toFixed(6)).join(", ");490 }491 return value.join(", ");492 }493494 // Handle specific tags495 switch (tag) {496 case "ExposureTime":497 // Format as fraction if less than 1498 if (value < 1) {499 const denominator = Math.round(1 / value);500 return `1/${denominator} sec`;501 }502 return `${value.toFixed(2)} sec`;503504 case "FNumber":505 return `f/${value.toFixed(1)}`;506507 case "FocalLength":508 case "FocalLengthIn35mmFilm":509 return `${Math.round(value)}mm`;510511 case "ExposureBiasValue":512 const sign = value > 0 ? "+" : "";513 return `${sign}${value.toFixed(1)} EV`;514515 default:516 // For numeric values, format with 2 decimal places if needed517 if (typeof value === "number" && !Number.isInteger(value)) {518 return value.toFixed(2);519 }520 return value.toString();521 }522 }523524 // Convert GPS coordinates to decimal degrees525 function convertGPSToDecimal(coordinates, ref) {526 if (!coordinates || coordinates.length !== 3) {527 return null;528 }529530 // Calculate decimal degrees: degrees + minutes/60 + seconds/3600531 let decimal = coordinates[0] + coordinates[1] / 60 + coordinates[2] / 3600;532533 // If south or west, make negative534 if (ref === "S" || ref === "W") {535 decimal = -decimal;536 }537538 return decimal;539 }540541 // Process the selected image542 async function processImage(file) {543 if (!file || !file.type.startsWith("image/")) {544 alert("Please select a valid image file");545 return;546 }547548 // Show loader549 loaderContainer.style.display = "flex";550551 // Display image preview552 const reader = new FileReader();553 reader.onload = (e) => {554 const img = new Image();555 img.src = e.target.result;556 img.onload = () => {557 // Clear previous image558 previewContainer.innerHTML = "";559 previewContainer.appendChild(img);560 imagePreview.classList.remove("hidden");561 };562 };563 reader.readAsDataURL(file);564565 // Process EXIF data566 try {567 const exifData = await exifService.extractExifData(file);568 displayExifData(exifData);569 } catch (error) {570 console.error(error);571 exifDataDisplay.innerHTML = `572 <div class="no-exif-message">573 <p>No EXIF data found or error processing the image.</p>574 <p>Error: ${error.message}</p>575 </div>576 `;577 exifDataDisplay.classList.remove("hidden");578 } finally {579 // Hide loader580 loaderContainer.style.display = "none";581 }582 }583584 // Display EXIF data in the UI585 function displayExifData(data) {586 // Clear previous data587 exifDataDisplay.innerHTML = "";588589 if (!data || Object.keys(data).length === 0) {590 exifDataDisplay.innerHTML = `591 <div class="no-exif-message">592 <p>No EXIF data found in this image.</p>593 </div>594 `;595 exifDataDisplay.classList.remove("hidden");596 return;597 }598599 // Basic image information600 let html = '<div class="exif-group">';601 html += "<h3>Basic Information</h3>";602603 const basicTags = [604 "Make",605 "Model",606 "Software",607 "DateTime",608 "Artist",609 "Copyright",610 "ImageWidth",611 "ImageHeight",612 "Orientation",613 "OrientationDescription",614 ];615616 basicTags.forEach((tag) => {617 if (data[tag] !== undefined) {618 html += `619 <div class="exif-item">620 <div class="exif-label">${formatTagName(tag)}</div>621 <div class="exif-value">${formatTagValue(data[tag], tag)}</div>622 </div>623 `;624 }625 });626627 html += "</div>";628629 // EXIF specific data630 if (data.exif && Object.keys(data.exif).length > 0) {631 html += '<div class="exif-group">';632 html += "<h3>Camera Settings</h3>";633634 const exifTags = [635 "ExposureTime",636 "FNumber",637 "ExposureProgram",638 "ISOSpeedRatings",639 "DateTimeOriginal",640 "DateTimeDigitized",641 "ShutterSpeedValue",642 "ApertureValue",643 "BrightnessValue",644 "ExposureBiasValue",645 "MaxApertureValue",646 "SubjectDistance",647 "MeteringMode",648 "LightSource",649 "Flash",650 "FocalLength",651 "FocalLengthIn35mmFilm",652 "WhiteBalance",653 "SceneCaptureType",654 ];655656 exifTags.forEach((tag) => {657 if (data.exif[tag] !== undefined) {658 html += `659 <div class="exif-item">660 <div class="exif-label">${formatTagName(tag)}</div>661 <div class="exif-value">${formatTagValue(data.exif[tag], tag)}</div>662 </div>663 `;664 }665 });666667 html += "</div>";668 }669670 // GPS data671 if (data.gps && Object.keys(data.gps).length > 0) {672 html += '<div class="exif-group">';673 html += "<h3>GPS Information</h3>";674675 // Process GPS coordinates if available676 let latitude = null;677 let longitude = null;678679 if (data.gps.GPSLatitude && data.gps.GPSLatitudeRef) {680 latitude = convertGPSToDecimal(681 data.gps.GPSLatitude,682 data.gps.GPSLatitudeRef683 );684 }685686 if (data.gps.GPSLongitude && data.gps.GPSLongitudeRef) {687 longitude = convertGPSToDecimal(688 data.gps.GPSLongitude,689 data.gps.GPSLongitudeRef690 );691 }692693 // Display GPS coordinates in a user-friendly format694 if (latitude !== null && longitude !== null) {695 html += `696 <div class="exif-item">697 <div class="exif-label">Coordinates</div>698 <div class="exif-value">${latitude.toFixed(6)}, ${longitude.toFixed(6)}</div>699 </div>700 `;701 }702703 // Display other GPS information704 for (const [tag, value] of Object.entries(data.gps)) {705 // Skip latitude and longitude as we've already displayed them in a combined format706 if (707 [708 "GPSLatitude",709 "GPSLatitudeRef",710 "GPSLongitude",711 "GPSLongitudeRef",712 ].includes(tag)713 ) {714 continue;715 }716717 html += `718 <div class="exif-item">719 <div class="exif-label">${formatTagName(tag)}</div>720 <div class="exif-value">${formatTagValue(value, tag)}</div>721 </div>722 `;723 }724725 html += "</div>";726 }727728 // Set the HTML content and show the display729 exifDataDisplay.innerHTML = html;730 exifDataDisplay.classList.remove("hidden");731 }732733 // Add event listeners734 document.addEventListener("DOMContentLoaded", () => {735 // File input change event736 imageInput.addEventListener("change", (e) => {737 if (e.target.files && e.target.files[0]) {738 processImage(e.target.files[0]);739 }740 });741742 // Drag and drop events743 dropArea.addEventListener("dragover", (e) => {744 e.preventDefault();745 dropArea.classList.add("drag-over");746 });747748 dropArea.addEventListener("dragleave", () => {749 dropArea.classList.remove("drag-over");750 });751752 dropArea.addEventListener("drop", (e) => {753 e.preventDefault();754 dropArea.classList.remove("drag-over");755756 if (e.dataTransfer.files && e.dataTransfer.files[0]) {757 processImage(e.dataTransfer.files[0]);758 }759 });760761 // Click on drop area to trigger file input762 dropArea.addEventListener("click", () => {763 imageInput.click();764 });765 });
This code is a JavaScript implementation of an EXIF data extraction service for images. EXIF (Exchangeable Image File Format) is metadata embedded in image files like JPEGs and TIFFs that contains information about the camera settings, date/time, location, and other details of when the photo was taken.
Here's a breakdown of how this code works:
1. ExifService Class
This is the main class responsible for extracting and parsing EXIF data from image files. It contains:
- A constructor that initializes properties and a comprehensive dictionary of EXIF tags with their meanings
- Methods to extract and parse EXIF data from image files
- Helper functions to read different data types from binary data
2. DOM Elements and UI Management
The code defines various DOM elements for the user interface:
- File input for selecting images
- Drop area for drag-and-drop functionality
- Preview container to display the selected image
- Display area for showing the extracted EXIF data
- Loading indicator
3. Data Processing and Display Functions
Several helper functions format and display the extracted EXIF data:
formatTagName()
- Converts technical tag names to user-friendly display namesformatTagValue()
- Formats tag values based on their type (e.g., exposure time as fractions)convertGPSToDecimal()
- Converts GPS coordinates to decimal degreesprocessImage()
- Main function that handles the selected imagedisplayExifData()
- Creates HTML to show the extracted EXIF data
How It Works
-
Loading an Image: The user can select an image by clicking the drop area (which triggers a file input) or by dragging and dropping an image.
-
EXIF Extraction Process:
- The image file is read as an ArrayBuffer using FileReader
- The binary data is analyzed to find the EXIF APP1 marker (0xFFE1)
- The code determines the endianness (byte order) of the data
- It reads the Image File Directory (IFD) structures containing the EXIF tags
- Special handling is applied for sub-directories like the EXIF IFD and GPS IFD
-
Data Display:
- The extracted data is organized into sections: Basic Information, Camera Settings, and GPS Information
- Values are formatted appropriately (e.g., exposure time as fractions, f-numbers with "f/" prefix)
- GPS coordinates are converted to decimal format for easy readability
Technical Details
The code handles several complex aspects of EXIF parsing:
- Endianness Detection: EXIF data can be in either big-endian (0x4D4D) or little-endian (0x4949) format
- Data Type Handling: EXIF uses various data types (BYTE, ASCII, SHORT, LONG, RATIONAL, etc.)
- IFD Parsing: Image File Directories contain tag entries with different data types and sizes
- Value Reading: Values can be stored directly in the tag or referenced via an offset depending on size
Usage Flow
- User selects or drops an image file
- A preview of the image is displayed
- The EXIF service extracts metadata from the image
- The metadata is formatted and displayed in a user-friendly way
- If no EXIF data is found or an error occurs, an appropriate message is shown
This implementation is entirely in JavaScript and runs in the browser, making it suitable for client-side image analysis without requiring server processing for the EXIF extraction.
🧾 Full Code
Below is the complete code for your JavaScript-based EXIF metadata parser.
1 <!DOCTYPE html>2 <html lang="en">3 <head>4 <meta charset="UTF-8" />5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />6 <title>EXIF Data Viewer</title>7 <style>8 :root {9 --primary-color: #3498db;10 --secondary-color: #2980b9;11 --success-color: #2ecc71;12 --danger-color: #e74c3c;13 --text-color: #333;14 --light-bg: #f5f5f5;15 --border-color: #ddd;16 }1718 * {19 box-sizing: border-box;20 margin: 0;21 padding: 0;22 }2324 body {25 font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;26 line-height: 1.6;27 color: var(--text-color);28 background-color: var(--light-bg);29 padding: 20px;30 }3132 .container {33 max-width: 1000px;34 margin: 0 auto;35 background-color: white;36 padding: 30px;37 border-radius: 8px;38 box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);39 }4041 h1 {42 text-align: center;43 margin-bottom: 30px;44 color: var(--primary-color);45 }4647 .drop-area {48 border: 3px dashed var(--border-color);49 border-radius: 8px;50 padding: 50px 20px;51 text-align: center;52 margin-bottom: 30px;53 background-color: var(--light-bg);54 transition: all 0.3s ease;55 cursor: pointer;56 }5758 .drop-area.drag-over {59 border-color: var(--primary-color);60 background-color: rgba(52, 152, 219, 0.1);61 }6263 .drop-area p {64 font-size: 18px;65 margin-bottom: 15px;66 }6768 .drop-area .icon {69 font-size: 48px;70 color: var(--primary-color);71 margin-bottom: 15px;72 }7374 .file-input-wrapper {75 position: relative;76 display: inline-block;77 }7879 .file-input {80 position: absolute;81 left: 0;82 top: 0;83 opacity: 0;84 width: 100%;85 height: 100%;86 cursor: pointer;87 }8889 .browse-button {90 display: inline-block;91 background-color: var(--primary-color);92 color: white;93 padding: 10px 25px;94 border-radius: 4px;95 cursor: pointer;96 font-weight: 500;97 transition: background-color 0.3s;98 }99100 .browse-button:hover {101 background-color: var(--secondary-color);102 }103104 .image-preview {105 display: flex;106 justify-content: space-between;107 margin-bottom: 30px;108 height: 300px;109 }110111 .preview-container {112 flex: 0 0 48%;113 height: 100%;114 border-radius: 8px;115 overflow: hidden;116 background-color: var(--light-bg);117 position: relative;118 display: flex;119 align-items: center;120 justify-content: center;121 }122123 .preview-container img {124 max-width: 100%;125 max-height: 100%;126 object-fit: contain;127 }128129 .exif-data {130 flex: 0 0 48%;131 height: 100%;132 overflow-y: auto;133 padding: 15px;134 border: 1px solid var(--border-color);135 border-radius: 8px;136 }137138 .exif-data.hidden,139 .image-preview.hidden {140 display: none;141 }142143 .exif-group {144 margin-bottom: 15px;145 }146147 .exif-group h3 {148 margin-bottom: 8px;149 padding-bottom: 5px;150 border-bottom: 1px solid var(--border-color);151 color: var(--primary-color);152 }153154 .exif-item {155 display: flex;156 justify-content: space-between;157 margin-bottom: 5px;158 padding: 5px 0;159 border-bottom: 1px dashed var(--border-color);160 }161162 .exif-label {163 font-weight: 500;164 flex: 0 0 50%;165 }166167 .exif-value {168 flex: 0 0 50%;169 text-align: right;170 word-break: break-word;171 }172173 /* Loader styles */174 .loader-container {175 display: none;176 position: fixed;177 top: 0;178 left: 0;179 width: 100%;180 height: 100%;181 background-color: rgba(255, 255, 255, 0.7);182 z-index: 1000;183 justify-content: center;184 align-items: center;185 }186187 .loader {188 border: 5px solid var(--light-bg);189 border-top: 5px solid var(--primary-color);190 border-radius: 50%;191 width: 50px;192 height: 50px;193 animation: spin 1s linear infinite;194 }195196 .loader-text {197 margin-left: 15px;198 font-size: 18px;199 font-weight: 500;200 }201202 @keyframes spin {203 0% {204 transform: rotate(0deg);205 }206 100% {207 transform: rotate(360deg);208 }209 }210211 .no-exif-message {212 text-align: center;213 padding: 20px;214 color: var(--danger-color);215 font-weight: 500;216 }217218 .gps-map {219 height: 200px;220 margin-top: 20px;221 background-color: var(--light-bg);222 border-radius: 8px;223 display: flex;224 align-items: center;225 justify-content: center;226 }227228 .section-title {229 margin: 30px 0 15px;230 font-size: 20px;231 color: var(--primary-color);232 }233234 @media (max-width: 768px) {235 .image-preview {236 flex-direction: column;237 height: auto;238 }239240 .preview-container,241 .exif-data {242 flex: 0 0 100%;243 width: 100%;244 margin-bottom: 20px;245 }246247 .preview-container {248 height: 250px;249 }250251 .exif-data {252 height: 300px;253 }254 }255 </style>256 </head>257 <body>258 <div class="container">259 <h1>EXIF Data Viewer</h1>260261 <div class="drop-area" id="dropArea">262 <div class="icon">📁</div>263 <p>Drag & drop an image here</p>264 <p>or</p>265 <div class="file-input-wrapper">266 <span class="browse-button">Browse Files</span>267 <input268 type="file"269 class="file-input"270 id="imageInput"271 accept="image/*"272 />273 </div>274 </div>275276 <div class="image-preview hidden" id="imagePreview">277 <div class="preview-container" id="previewContainer">278 <!-- Image preview will be shown here -->279 </div>280 <div class="exif-data hidden" id="exifDataDisplay">281 <!-- EXIF data will be displayed here -->282 </div>283 </div>284 </div>285286 <!-- Loader -->287 <div class="loader-container" id="loaderContainer">288 <div class="loader"></div>289 <div class="loader-text">Processing EXIF data...</div>290 </div>291292 <script>293 class ExifService {294 constructor() {295 this.tiffHeaderOffset = null;296 this.bigEndian = null;297 this.tags = {298 // EXIF tags and their meanings299 0x0100: "ImageWidth",300 0x0101: "ImageHeight",301 0x0102: "BitsPerSample",302 0x0103: "Compression",303 0x010e: "ImageDescription",304 0x010f: "Make",305 0x0110: "Model",306 0x0112: "Orientation",307 0x011a: "XResolution",308 0x011b: "YResolution",309 0x0128: "ResolutionUnit",310 0x0131: "Software",311 0x0132: "DateTime",312 0x013b: "Artist",313 0x0213: "YCbCrPositioning",314 0x8298: "Copyright",315 0x8769: "ExifIFDPointer",316 0x8825: "GPSInfoIFDPointer",317318 // EXIF IFD tags319 0x829a: "ExposureTime",320 0x829d: "FNumber",321 0x8822: "ExposureProgram",322 0x8827: "ISOSpeedRatings",323 0x9000: "ExifVersion",324 0x9003: "DateTimeOriginal",325 0x9004: "DateTimeDigitized",326 0x9201: "ShutterSpeedValue",327 0x9202: "ApertureValue",328 0x9203: "BrightnessValue",329 0x9204: "ExposureBiasValue",330 0x9205: "MaxApertureValue",331 0x9206: "SubjectDistance",332 0x9207: "MeteringMode",333 0x9208: "LightSource",334 0x9209: "Flash",335 0x920a: "FocalLength",336 0x927c: "MakerNote",337 0x9286: "UserComment",338 0xa000: "FlashpixVersion",339 0xa001: "ColorSpace",340 0xa002: "PixelXDimension",341 0xa003: "PixelYDimension",342 0xa004: "RelatedSoundFile",343 0xa005: "InteroperabilityIFDPointer",344 0xa20e: "FocalPlaneXResolution",345 0xa20f: "FocalPlaneYResolution",346 0xa210: "FocalPlaneResolutionUnit",347 0xa217: "SensingMethod",348 0xa300: "FileSource",349 0xa301: "SceneType",350 0xa302: "CFAPattern",351 0xa401: "CustomRendered",352 0xa402: "ExposureMode",353 0xa403: "WhiteBalance",354 0xa404: "DigitalZoomRatio",355 0xa405: "FocalLengthIn35mmFilm",356 0xa406: "SceneCaptureType",357 0xa407: "GainControl",358 0xa408: "Contrast",359 0xa409: "Saturation",360 0xa40a: "Sharpness",361 0xa40b: "DeviceSettingDescription",362 0xa40c: "SubjectDistanceRange",363364 // GPS tags365 0x0000: "GPSVersionID",366 0x0001: "GPSLatitudeRef",367 0x0002: "GPSLatitude",368 0x0003: "GPSLongitudeRef",369 0x0004: "GPSLongitude",370 0x0005: "GPSAltitudeRef",371 0x0006: "GPSAltitude",372 0x0007: "GPSTimeStamp",373 0x0008: "GPSSatellites",374 0x0009: "GPSStatus",375 0x000a: "GPSMeasureMode",376 0x000b: "GPSDOP",377 0x000c: "GPSSpeedRef",378 0x000d: "GPSSpeed",379 0x000e: "GPSTrackRef",380 0x000f: "GPSTrack",381 0x0010: "GPSImgDirectionRef",382 0x0011: "GPSImgDirection",383 0x0012: "GPSMapDatum",384 0x0013: "GPSDestLatitudeRef",385 0x0014: "GPSDestLatitude",386 0x0015: "GPSDestLongitudeRef",387 0x0016: "GPSDestLongitude",388 0x0017: "GPSDestBearingRef",389 0x0018: "GPSDestBearing",390 0x0019: "GPSDestDistanceRef",391 0x001a: "GPSDestDistance",392 0x001b: "GPSProcessingMethod",393 0x001c: "GPSAreaInformation",394 0x001d: "GPSDateStamp",395 0x001e: "GPSDifferential",396 };397398 // Orientation values399 this.orientationDescriptions = {400 1: "Normal",401 2: "Mirrored horizontally",402 3: "Rotated 180°",403 4: "Mirrored vertically",404 5: "Mirrored horizontally and rotated 90° CCW",405 6: "Rotated 90° CW",406 7: "Mirrored horizontally and rotated 90° CW",407 8: "Rotated 90° CCW",408 };409 }410411 /**412 * Process an image file and extract its EXIF data413 * @param {File|Blob} imageFile - The image file to process414 * @returns {Promise<Object>} - A promise that resolves to an object containing EXIF data415 */416 extractExifData(imageFile) {417 return new Promise((resolve, reject) => {418 if (419 !imageFile ||420 !["image/jpeg", "image/jpg", "image/tiff"].includes(421 imageFile.type422 )423 ) {424 reject(425 new Error(426 "Unsupported file type. Only JPEG and TIFF formats support EXIF."427 )428 );429 return;430 }431432 const reader = new FileReader();433434 reader.onload = (e) => {435 try {436 const buffer = e.target.result;437 const dataView = new DataView(buffer);438439 // Check for JPEG format440 if (441 dataView.getUint8(0) !== 0xff ||442 dataView.getUint8(1) !== 0xd8443 ) {444 reject(new Error("Not a valid JPEG"));445 return;446 }447448 const exifData = this.parseExifData(dataView);449 resolve(exifData);450 } catch (error) {451 reject(452 new Error(`Error processing EXIF data: ${error.message}`)453 );454 }455 };456457 reader.onerror = () => {458 reject(new Error("Error reading file"));459 };460461 reader.readAsArrayBuffer(imageFile);462 });463 }464465 /**466 * Parse EXIF data from a DataView object467 * @param {DataView} dataView - The DataView containing the image data468 * @returns {Object} - An object containing the parsed EXIF data469 */470 parseExifData(dataView) {471 const length = dataView.byteLength;472 let offset = 2; // Skip the first two bytes (JPEG marker)473 const exifData = {};474475 // Search for the EXIF APP1 marker (0xFFE1)476 while (offset < length) {477 if (dataView.getUint8(offset) !== 0xff) {478 offset++;479 continue;480 }481482 const marker = dataView.getUint8(offset + 1);483484 // If we found the EXIF APP1 marker485 if (marker === 0xe1) {486 const exifOffset = offset + 4; // Skip marker and size487 const exifLength = dataView.getUint16(offset + 2, false);488489 // Check for "Exif\0\0" header490 if (491 this.getStringFromCharCodes(dataView, exifOffset, 6) ===492 "Exif\0\0"493 ) {494 return this.readExifTags(495 dataView,496 exifOffset + 6,497 exifLength - 6498 );499 }500 } else if (marker === 0xda) {501 // Start of Scan marker - no more metadata502 break;503 }504505 // Move to the next marker506 offset += 2 + dataView.getUint16(offset + 2, false);507 }508509 return exifData;510 }511512 /**513 * Read EXIF tags from the specified offset514 * @param {DataView} dataView - The DataView containing the image data515 * @param {number} start - The offset to start reading from516 * @param {number} length - The length of data to read517 * @returns {Object} - An object containing the parsed EXIF tags518 */519 readExifTags(dataView, start, length) {520 const result = {};521522 // Determine endianness523 if (dataView.getUint16(start) === 0x4949) {524 this.bigEndian = false; // Little endian525 } else if (dataView.getUint16(start) === 0x4d4d) {526 this.bigEndian = true; // Big endian527 } else {528 throw new Error("Invalid TIFF data: endianness marker not found");529 }530531 // Check for TIFF marker532 if (dataView.getUint16(start + 2, !this.bigEndian) !== 0x002a) {533 throw new Error("Invalid TIFF data: TIFF marker not found");534 }535536 // Get offset to first IFD (Image File Directory)537 const firstIFDOffset = dataView.getUint32(start + 4, !this.bigEndian);538 this.tiffHeaderOffset = start;539540 // Read the main IFD541 const ifdData = this.readIFD(542 dataView,543 this.tiffHeaderOffset + firstIFDOffset544 );545 Object.assign(result, ifdData);546547 // Check for and read Exif sub-IFD if it exists548 if (ifdData.ExifIFDPointer) {549 const exifData = this.readIFD(550 dataView,551 this.tiffHeaderOffset + ifdData.ExifIFDPointer552 );553 result.exif = exifData;554 delete result.ExifIFDPointer;555 }556557 // Check for and read GPS sub-IFD if it exists558 if (ifdData.GPSInfoIFDPointer) {559 const gpsData = this.readIFD(560 dataView,561 this.tiffHeaderOffset + ifdData.GPSInfoIFDPointer562 );563 result.gps = gpsData;564 delete result.GPSInfoIFDPointer;565 }566567 // Add orientation description if orientation exists568 if (result.Orientation) {569 result.OrientationDescription =570 this.orientationDescriptions[result.Orientation] || "Unknown";571 }572573 return result;574 }575576 /**577 * Read an Image File Directory578 * @param {DataView} dataView - The DataView containing the image data579 * @param {number} offset - The offset to start reading from580 * @returns {Object} - An object containing the parsed IFD entries581 */582 readIFD(dataView, offset) {583 const result = {};584 const numEntries = dataView.getUint16(offset, !this.bigEndian);585586 for (let i = 0; i < numEntries; i++) {587 const entryOffset = offset + 2 + i * 12; // 12 bytes per entry588 const tagNumber = dataView.getUint16(entryOffset, !this.bigEndian);589 const dataType = dataView.getUint16(590 entryOffset + 2,591 !this.bigEndian592 );593 const numValues = dataView.getUint32(594 entryOffset + 4,595 !this.bigEndian596 );597 const valueOffset = dataView.getUint32(598 entryOffset + 8,599 !this.bigEndian600 );601602 // Get tag name if known, otherwise use hex code603 const tagName =604 this.tags[tagNumber] || `0x${tagNumber.toString(16)}`;605606 // Get tag value607 let tagValue;608 const totalSize = this.getDataTypeSize(dataType) * numValues;609610 if (totalSize <= 4) {611 // Value is stored in the offset field itself612 tagValue = this.readTagValue(613 dataView,614 entryOffset + 8,615 dataType,616 numValues617 );618 } else {619 // Value is stored at the specified offset620 tagValue = this.readTagValue(621 dataView,622 this.tiffHeaderOffset + valueOffset,623 dataType,624 numValues625 );626 }627628 result[tagName] = tagValue;629 }630631 return result;632 }633634 /**635 * Get the size in bytes of a specific data type636 * @param {number} dataType - The EXIF data type637 * @returns {number} - The size in bytes638 */639 getDataTypeSize(dataType) {640 const sizes = {641 1: 1, // BYTE642 2: 1, // ASCII643 3: 2, // SHORT644 4: 4, // LONG645 5: 8, // RATIONAL646 6: 1, // SBYTE647 7: 1, // UNDEFINED648 8: 2, // SSHORT649 9: 4, // SLONG650 10: 8, // SRATIONAL651 11: 4, // FLOAT652 12: 8, // DOUBLE653 };654655 return sizes[dataType] || 0;656 }657658 /**659 * Read a tag value based on its data type660 * @param {DataView} dataView - The DataView containing the image data661 * @param {number} offset - The offset to start reading from662 * @param {number} dataType - The data type of the value663 * @param {number} numValues - The number of values to read664 * @returns {*} - The parsed tag value665 */666 readTagValue(dataView, offset, dataType, numValues) {667 let values = [];668 let i, size;669670 switch (dataType) {671 case 1: // BYTE672 case 6: // SBYTE673 case 7: // UNDEFINED674 for (i = 0; i < numValues; i++) {675 values.push(dataView.getUint8(offset + i));676 }677 break;678679 case 2: // ASCII680 // Read null-terminated ASCII string681 const chars = [];682 for (i = 0; i < numValues; i++) {683 const charCode = dataView.getUint8(offset + i);684 if (charCode === 0) break;685 chars.push(charCode);686 }687 return String.fromCharCode(...chars);688689 case 3: // SHORT690 size = 2;691 for (i = 0; i < numValues; i++) {692 values.push(693 dataView.getUint16(offset + i * size, !this.bigEndian)694 );695 }696 break;697698 case 8: // SSHORT699 size = 2;700 for (i = 0; i < numValues; i++) {701 values.push(702 dataView.getInt16(offset + i * size, !this.bigEndian)703 );704 }705 break;706707 case 4: // LONG708 size = 4;709 for (i = 0; i < numValues; i++) {710 values.push(711 dataView.getUint32(offset + i * size, !this.bigEndian)712 );713 }714 break;715716 case 9: // SLONG717 size = 4;718 for (i = 0; i < numValues; i++) {719 values.push(720 dataView.getInt32(offset + i * size, !this.bigEndian)721 );722 }723 break;724725 case 5: // RATIONAL726 size = 8;727 for (i = 0; i < numValues; i++) {728 const numerator = dataView.getUint32(729 offset + i * size,730 !this.bigEndian731 );732 const denominator = dataView.getUint32(733 offset + i * size + 4,734 !this.bigEndian735 );736 values.push(numerator / denominator);737 }738 break;739740 case 10: // SRATIONAL741 size = 8;742 for (i = 0; i < numValues; i++) {743 const numerator = dataView.getInt32(744 offset + i * size,745 !this.bigEndian746 );747 const denominator = dataView.getInt32(748 offset + i * size + 4,749 !this.bigEndian750 );751 values.push(numerator / denominator);752 }753 break;754755 default:756 values = [dataType, numValues]; // Return the data type and count for unknown types757 }758759 // If there's only one value, return it directly instead of an array760 return numValues === 1 ? values[0] : values;761 }762763 /**764 * Convert array of character codes to a string765 * @param {DataView} dataView - The DataView containing the data766 * @param {number} start - The start offset767 * @param {number} length - The number of characters to read768 * @returns {string} - The resulting string769 */770 getStringFromCharCodes(dataView, start, length) {771 const chars = [];772 for (let i = 0; i < length; i++) {773 chars.push(dataView.getUint8(start + i));774 }775 return String.fromCharCode(...chars);776 }777 }778779 // DOM Elements780 const imageInput = document.getElementById("imageInput");781 const dropArea = document.getElementById("dropArea");782 const previewContainer = document.getElementById("previewContainer");783 const imagePreview = document.getElementById("imagePreview");784 const exifDataDisplay = document.getElementById("exifDataDisplay");785 const loaderContainer = document.getElementById("loaderContainer");786787 // Initialize EXIF service788 const exifService = new ExifService();789790 // Format tag names for display (convert camelCase to Title Case with spaces)791 function formatTagName(tag) {792 // Handle special cases793 if (tag === "FNumber") return "F-Number";794 if (tag === "ISOSpeedRatings") return "ISO";795796 // Add space before capital letters and capitalize first letter797 return tag798 .replace(/([A-Z])/g, " $1")799 .replace(/^./, (str) => str.toUpperCase())800 .trim();801 }802803 // Format tag values based on their type804 function formatTagValue(value, tag) {805 if (value === undefined || value === null) return "Unknown";806807 // Handle arrays808 if (Array.isArray(value)) {809 // For GPS coordinates810 if (tag === "GPSLatitude" || tag === "GPSLongitude") {811 return value.map((v) => v.toFixed(6)).join(", ");812 }813 return value.join(", ");814 }815816 // Handle specific tags817 switch (tag) {818 case "ExposureTime":819 // Format as fraction if less than 1820 if (value < 1) {821 const denominator = Math.round(1 / value);822 return `1/${denominator} sec`;823 }824 return `${value.toFixed(2)} sec`;825826 case "FNumber":827 return `f/${value.toFixed(1)}`;828829 case "FocalLength":830 case "FocalLengthIn35mmFilm":831 return `${Math.round(value)}mm`;832833 case "ExposureBiasValue":834 const sign = value > 0 ? "+" : "";835 return `${sign}${value.toFixed(1)} EV`;836837 default:838 // For numeric values, format with 2 decimal places if needed839 if (typeof value === "number" && !Number.isInteger(value)) {840 return value.toFixed(2);841 }842 return value.toString();843 }844 }845846 // Convert GPS coordinates to decimal degrees847 function convertGPSToDecimal(coordinates, ref) {848 if (!coordinates || coordinates.length !== 3) {849 return null;850 }851852 // Calculate decimal degrees: degrees + minutes/60 + seconds/3600853 let decimal =854 coordinates[0] + coordinates[1] / 60 + coordinates[2] / 3600;855856 // If south or west, make negative857 if (ref === "S" || ref === "W") {858 decimal = -decimal;859 }860861 return decimal;862 }863864 // Process the selected image865 async function processImage(file) {866 if (!file || !file.type.startsWith("image/")) {867 alert("Please select a valid image file");868 return;869 }870871 // Show loader872 loaderContainer.style.display = "flex";873874 // Display image preview875 const reader = new FileReader();876 reader.onload = (e) => {877 const img = new Image();878 img.src = e.target.result;879 img.onload = () => {880 // Clear previous image881 previewContainer.innerHTML = "";882 previewContainer.appendChild(img);883 imagePreview.classList.remove("hidden");884 };885 };886 reader.readAsDataURL(file);887888 // Process EXIF data889 try {890 const exifData = await exifService.extractExifData(file);891 displayExifData(exifData);892 } catch (error) {893 console.error(error);894 exifDataDisplay.innerHTML = `895896897898899 `;900 exifDataDisplay.classList.remove("hidden");901 } finally {902 // Hide loader903 loaderContainer.style.display = "none";904 }905 }906907 // Display EXIF data in the UI908 function displayExifData(data) {909 // Clear previous data910 exifDataDisplay.innerHTML = "";911912 if (!data || Object.keys(data).length === 0) {913 exifDataDisplay.innerHTML = `914915916917 `;918 exifDataDisplay.classList.remove("hidden");919 return;920 }921922 // Basic image information923 let html = '<div class="exif-group">';924 html += "<h3>Basic Information</h3>";925926 const basicTags = [927 "Make",928 "Model",929 "Software",930 "DateTime",931 "Artist",932 "Copyright",933 "ImageWidth",934 "ImageHeight",935 "Orientation",936 "OrientationDescription",937 ];938939 basicTags.forEach((tag) => {940 if (data[tag] !== undefined) {941 html += `942 <div class="exif-item">943 <div class="exif-label">${formatTagName(tag)}</div>944 <div class="exif-value">${formatTagValue(data[tag], tag)}</div>945 </div>946 `;947 }948 });949950 html += "</div>";951952 // EXIF specific data953 if (data.exif && Object.keys(data.exif).length > 0) {954 html += '<div class="exif-group">';955 html += "<h3>Camera Settings</h3>";956957 const exifTags = [958 "ExposureTime",959 "FNumber",960 "ExposureProgram",961 "ISOSpeedRatings",962 "DateTimeOriginal",963 "DateTimeDigitized",964 "ShutterSpeedValue",965 "ApertureValue",966 "BrightnessValue",967 "ExposureBiasValue",968 "MaxApertureValue",969 "SubjectDistance",970 "MeteringMode",971 "LightSource",972 "Flash",973 "FocalLength",974 "FocalLengthIn35mmFilm",975 "WhiteBalance",976 "SceneCaptureType",977 ];978979 exifTags.forEach((tag) => {980 if (data.exif[tag] !== undefined) {981 html += `982 <div class="exif-item">983 <div class="exif-label">${formatTagName(tag)}</div>984 <div class="exif-value">${formatTagValue(data.exif[tag], tag)}</div>985 </div>986 `;987 }988 });989990 html += "</div>";991 }992993 // GPS data994 if (data.gps && Object.keys(data.gps).length > 0) {995 html += '<div class="exif-group">';996 html += "<h3>GPS Information</h3>";997998 // Process GPS coordinates if available999 let latitude = null;1000 let longitude = null;10011002 if (data.gps.GPSLatitude && data.gps.GPSLatitudeRef) {1003 latitude = convertGPSToDecimal(1004 data.gps.GPSLatitude,1005 data.gps.GPSLatitudeRef1006 );1007 }10081009 if (data.gps.GPSLongitude && data.gps.GPSLongitudeRef) {1010 longitude = convertGPSToDecimal(1011 data.gps.GPSLongitude,1012 data.gps.GPSLongitudeRef1013 );1014 }10151016 // Display GPS coordinates in a user-friendly format1017 if (latitude !== null && longitude !== null) {1018 html += `1019 <div class="exif-item">1020 <div class="exif-label">Coordinates</div>1021 <div class="exif-value">${latitude.toFixed(6)}, ${longitude.toFixed(6)}</div>1022 </div>1023 `;1024 }10251026 // Display other GPS information1027 for (const [tag, value] of Object.entries(data.gps)) {1028 // Skip latitude and longitude as we've already displayed them in a combined format1029 if (1030 [1031 "GPSLatitude",1032 "GPSLatitudeRef",1033 "GPSLongitude",1034 "GPSLongitudeRef",1035 ].includes(tag)1036 ) {1037 continue;1038 }10391040 html += `1041 <div class="exif-item">1042 <div class="exif-label">${formatTagName(tag)}</div>1043 <div class="exif-value">${formatTagValue(value, tag)}</div>1044 </div>1045 `;1046 }10471048 html += "</div>";1049 }10501051 // Set the HTML content and show the display1052 exifDataDisplay.innerHTML = html;1053 exifDataDisplay.classList.remove("hidden");1054 }10551056 // Add event listeners1057 document.addEventListener("DOMContentLoaded", () => {1058 // File input change event1059 imageInput.addEventListener("change", (e) => {1060 if (e.target.files && e.target.files[0]) {1061 processImage(e.target.files[0]);1062 }1063 });10641065 // Drag and drop events1066 dropArea.addEventListener("dragover", (e) => {1067 e.preventDefault();1068 dropArea.classList.add("drag-over");1069 });10701071 dropArea.addEventListener("dragleave", () => {1072 dropArea.classList.remove("drag-over");1073 });10741075 dropArea.addEventListener("drop", (e) => {1076 e.preventDefault();1077 dropArea.classList.remove("drag-over");10781079 if (e.dataTransfer.files && e.dataTransfer.files[0]) {1080 processImage(e.dataTransfer.files[0]);1081 }1082 });10831084 // Click on drop area to trigger file input1085 dropArea.addEventListener("click", () => {1086 imageInput.click();1087 });1088 });1089 </script>1090 </body>1091 </html>
✅ Conclusion
You’ve now built a fully functional EXIF parser in JavaScript that works directly in the browser — with no external libraries. This lightweight approach is ideal for privacy-first apps, offline photo tools, and learning how image metadata works under the hood.