JavaScript Development Space

Extract Metadata from Images in the Browser Using JavaScript and EXIF

JavaScript EXIF Info Parser

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, and DataView
  • 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:

bash
1 js-exif-parser/
2 ├── index.html
3 ├── style.css
4 └── 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.

html
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>
12
13 <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 <input
20 type="file"
21 class="file-input"
22 id="imageInput"
23 accept="image/*"
24 />
25 </div>
26 </div>
27
28 <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>
37
38 <!-- Loader -->
39 <div class="loader-container" id="loaderContainer">
40 <div class="loader"></div>
41 <div class="loader-text">Processing EXIF data...</div>
42 </div>
43
44 <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)

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 }
10
11 * {
12 box-sizing: border-box;
13 margin: 0;
14 padding: 0;
15 }
16
17 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 }
24
25 .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 }
33
34 h1 {
35 text-align: center;
36 margin-bottom: 30px;
37 color: var(--primary-color);
38 }
39
40 .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 }
50
51 .drop-area.drag-over {
52 border-color: var(--primary-color);
53 background-color: rgba(52, 152, 219, 0.1);
54 }
55
56 .drop-area p {
57 font-size: 18px;
58 margin-bottom: 15px;
59 }
60
61 .drop-area .icon {
62 font-size: 48px;
63 color: var(--primary-color);
64 margin-bottom: 15px;
65 }
66
67 .file-input-wrapper {
68 position: relative;
69 display: inline-block;
70 }
71
72 .file-input {
73 position: absolute;
74 left: 0;
75 top: 0;
76 opacity: 0;
77 width: 100%;
78 height: 100%;
79 cursor: pointer;
80 }
81
82 .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 }
92
93 .browse-button:hover {
94 background-color: var(--secondary-color);
95 }
96
97 .image-preview {
98 display: flex;
99 justify-content: space-between;
100 margin-bottom: 30px;
101 height: 300px;
102 }
103
104 .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 }
115
116 .preview-container img {
117 max-width: 100%;
118 max-height: 100%;
119 object-fit: contain;
120 }
121
122 .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 }
130
131 .exif-data.hidden,
132 .image-preview.hidden {
133 display: none;
134 }
135
136 .exif-group {
137 margin-bottom: 15px;
138 }
139
140 .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 }
146
147 .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 }
154
155 .exif-label {
156 font-weight: 500;
157 flex: 0 0 50%;
158 }
159
160 .exif-value {
161 flex: 0 0 50%;
162 text-align: right;
163 word-break: break-word;
164 }
165
166 /* 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 }
179
180 .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 }
188
189 .loader-text {
190 margin-left: 15px;
191 font-size: 18px;
192 font-weight: 500;
193 }
194
195 @keyframes spin {
196 0% {
197 transform: rotate(0deg);
198 }
199 100% {
200 transform: rotate(360deg);
201 }
202 }
203
204 .no-exif-message {
205 text-align: center;
206 padding: 20px;
207 color: var(--danger-color);
208 font-weight: 500;
209 }
210
211 .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 }
220
221 .section-title {
222 margin: 30px 0 15px;
223 font-size: 20px;
224 color: var(--primary-color);
225 }
226
227 @media (max-width: 768px) {
228 .image-preview {
229 flex-direction: column;
230 height: auto;
231 }
232
233 .preview-container,
234 .exif-data {
235 flex: 0 0 100%;
236 width: 100%;
237 margin-bottom: 20px;
238 }
239
240 .preview-container {
241 height: 250px;
242 }
243
244 .exif-data {
245 height: 300px;
246 }
247 }

⚙️ Step 4: Write the JavaScript (script.js)

js
1 class ExifService {
2 constructor() {
3 this.tiffHeaderOffset = null;
4 this.bigEndian = null;
5 this.tags = {
6 // EXIF tags and their meanings
7 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",
25
26 // EXIF IFD tags
27 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",
71
72 // GPS tags
73 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 };
105
106 // Orientation values
107 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 }
118
119 /**
120 * Process an image file and extract its EXIF data
121 * @param {File|Blob} imageFile - The image file to process
122 * @returns {Promise<Object>} - A promise that resolves to an object containing EXIF data
123 */
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 }
137
138 const reader = new FileReader();
139
140 reader.onload = (e) => {
141 try {
142 const buffer = e.target.result;
143 const dataView = new DataView(buffer);
144
145 // Check for JPEG format
146 if (dataView.getUint8(0) !== 0xff || dataView.getUint8(1) !== 0xd8) {
147 reject(new Error("Not a valid JPEG"));
148 return;
149 }
150
151 const exifData = this.parseExifData(dataView);
152 resolve(exifData);
153 } catch (error) {
154 reject(new Error(`Error processing EXIF data: ${error.message}`));
155 }
156 };
157
158 reader.onerror = () => {
159 reject(new Error("Error reading file"));
160 };
161
162 reader.readAsArrayBuffer(imageFile);
163 });
164 }
165
166 /**
167 * Parse EXIF data from a DataView object
168 * @param {DataView} dataView - The DataView containing the image data
169 * @returns {Object} - An object containing the parsed EXIF data
170 */
171 parseExifData(dataView) {
172 const length = dataView.byteLength;
173 let offset = 2; // Skip the first two bytes (JPEG marker)
174 const exifData = {};
175
176 // Search for the EXIF APP1 marker (0xFFE1)
177 while (offset < length) {
178 if (dataView.getUint8(offset) !== 0xff) {
179 offset++;
180 continue;
181 }
182
183 const marker = dataView.getUint8(offset + 1);
184
185 // If we found the EXIF APP1 marker
186 if (marker === 0xe1) {
187 const exifOffset = offset + 4; // Skip marker and size
188 const exifLength = dataView.getUint16(offset + 2, false);
189
190 // Check for "Exif\0\0" header
191 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 metadata
198 break;
199 }
200
201 // Move to the next marker
202 offset += 2 + dataView.getUint16(offset + 2, false);
203 }
204
205 return exifData;
206 }
207
208 /**
209 * Read EXIF tags from the specified offset
210 * @param {DataView} dataView - The DataView containing the image data
211 * @param {number} start - The offset to start reading from
212 * @param {number} length - The length of data to read
213 * @returns {Object} - An object containing the parsed EXIF tags
214 */
215 readExifTags(dataView, start, length) {
216 const result = {};
217
218 // Determine endianness
219 if (dataView.getUint16(start) === 0x4949) {
220 this.bigEndian = false; // Little endian
221 } else if (dataView.getUint16(start) === 0x4d4d) {
222 this.bigEndian = true; // Big endian
223 } else {
224 throw new Error("Invalid TIFF data: endianness marker not found");
225 }
226
227 // Check for TIFF marker
228 if (dataView.getUint16(start + 2, !this.bigEndian) !== 0x002a) {
229 throw new Error("Invalid TIFF data: TIFF marker not found");
230 }
231
232 // Get offset to first IFD (Image File Directory)
233 const firstIFDOffset = dataView.getUint32(start + 4, !this.bigEndian);
234 this.tiffHeaderOffset = start;
235
236 // Read the main IFD
237 const ifdData = this.readIFD(
238 dataView,
239 this.tiffHeaderOffset + firstIFDOffset
240 );
241 Object.assign(result, ifdData);
242
243 // Check for and read Exif sub-IFD if it exists
244 if (ifdData.ExifIFDPointer) {
245 const exifData = this.readIFD(
246 dataView,
247 this.tiffHeaderOffset + ifdData.ExifIFDPointer
248 );
249 result.exif = exifData;
250 delete result.ExifIFDPointer;
251 }
252
253 // Check for and read GPS sub-IFD if it exists
254 if (ifdData.GPSInfoIFDPointer) {
255 const gpsData = this.readIFD(
256 dataView,
257 this.tiffHeaderOffset + ifdData.GPSInfoIFDPointer
258 );
259 result.gps = gpsData;
260 delete result.GPSInfoIFDPointer;
261 }
262
263 // Add orientation description if orientation exists
264 if (result.Orientation) {
265 result.OrientationDescription =
266 this.orientationDescriptions[result.Orientation] || "Unknown";
267 }
268
269 return result;
270 }
271
272 /**
273 * Read an Image File Directory
274 * @param {DataView} dataView - The DataView containing the image data
275 * @param {number} offset - The offset to start reading from
276 * @returns {Object} - An object containing the parsed IFD entries
277 */
278 readIFD(dataView, offset) {
279 const result = {};
280 const numEntries = dataView.getUint16(offset, !this.bigEndian);
281
282 for (let i = 0; i < numEntries; i++) {
283 const entryOffset = offset + 2 + i * 12; // 12 bytes per entry
284 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);
288
289 // Get tag name if known, otherwise use hex code
290 const tagName = this.tags[tagNumber] || `0x${tagNumber.toString(16)}`;
291
292 // Get tag value
293 let tagValue;
294 const totalSize = this.getDataTypeSize(dataType) * numValues;
295
296 if (totalSize <= 4) {
297 // Value is stored in the offset field itself
298 tagValue = this.readTagValue(
299 dataView,
300 entryOffset + 8,
301 dataType,
302 numValues
303 );
304 } else {
305 // Value is stored at the specified offset
306 tagValue = this.readTagValue(
307 dataView,
308 this.tiffHeaderOffset + valueOffset,
309 dataType,
310 numValues
311 );
312 }
313
314 result[tagName] = tagValue;
315 }
316
317 return result;
318 }
319
320 /**
321 * Get the size in bytes of a specific data type
322 * @param {number} dataType - The EXIF data type
323 * @returns {number} - The size in bytes
324 */
325 getDataTypeSize(dataType) {
326 const sizes = {
327 1: 1, // BYTE
328 2: 1, // ASCII
329 3: 2, // SHORT
330 4: 4, // LONG
331 5: 8, // RATIONAL
332 6: 1, // SBYTE
333 7: 1, // UNDEFINED
334 8: 2, // SSHORT
335 9: 4, // SLONG
336 10: 8, // SRATIONAL
337 11: 4, // FLOAT
338 12: 8, // DOUBLE
339 };
340
341 return sizes[dataType] || 0;
342 }
343
344 /**
345 * Read a tag value based on its data type
346 * @param {DataView} dataView - The DataView containing the image data
347 * @param {number} offset - The offset to start reading from
348 * @param {number} dataType - The data type of the value
349 * @param {number} numValues - The number of values to read
350 * @returns {*} - The parsed tag value
351 */
352 readTagValue(dataView, offset, dataType, numValues) {
353 let values = [];
354 let i, size;
355
356 switch (dataType) {
357 case 1: // BYTE
358 case 6: // SBYTE
359 case 7: // UNDEFINED
360 for (i = 0; i < numValues; i++) {
361 values.push(dataView.getUint8(offset + i));
362 }
363 break;
364
365 case 2: // ASCII
366 // Read null-terminated ASCII string
367 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);
374
375 case 3: // SHORT
376 size = 2;
377 for (i = 0; i < numValues; i++) {
378 values.push(dataView.getUint16(offset + i * size, !this.bigEndian));
379 }
380 break;
381
382 case 8: // SSHORT
383 size = 2;
384 for (i = 0; i < numValues; i++) {
385 values.push(dataView.getInt16(offset + i * size, !this.bigEndian));
386 }
387 break;
388
389 case 4: // LONG
390 size = 4;
391 for (i = 0; i < numValues; i++) {
392 values.push(dataView.getUint32(offset + i * size, !this.bigEndian));
393 }
394 break;
395
396 case 9: // SLONG
397 size = 4;
398 for (i = 0; i < numValues; i++) {
399 values.push(dataView.getInt32(offset + i * size, !this.bigEndian));
400 }
401 break;
402
403 case 5: // RATIONAL
404 size = 8;
405 for (i = 0; i < numValues; i++) {
406 const numerator = dataView.getUint32(
407 offset + i * size,
408 !this.bigEndian
409 );
410 const denominator = dataView.getUint32(
411 offset + i * size + 4,
412 !this.bigEndian
413 );
414 values.push(numerator / denominator);
415 }
416 break;
417
418 case 10: // SRATIONAL
419 size = 8;
420 for (i = 0; i < numValues; i++) {
421 const numerator = dataView.getInt32(
422 offset + i * size,
423 !this.bigEndian
424 );
425 const denominator = dataView.getInt32(
426 offset + i * size + 4,
427 !this.bigEndian
428 );
429 values.push(numerator / denominator);
430 }
431 break;
432
433 default:
434 values = [dataType, numValues]; // Return the data type and count for unknown types
435 }
436
437 // If there's only one value, return it directly instead of an array
438 return numValues === 1 ? values[0] : values;
439 }
440
441 /**
442 * Convert array of character codes to a string
443 * @param {DataView} dataView - The DataView containing the data
444 * @param {number} start - The start offset
445 * @param {number} length - The number of characters to read
446 * @returns {string} - The resulting string
447 */
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 }
456
457 // DOM Elements
458 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");
464
465 // Initialize EXIF service
466 const exifService = new ExifService();
467
468 // Format tag names for display (convert camelCase to Title Case with spaces)
469 function formatTagName(tag) {
470 // Handle special cases
471 if (tag === "FNumber") return "F-Number";
472 if (tag === "ISOSpeedRatings") return "ISO";
473
474 // Add space before capital letters and capitalize first letter
475 return tag
476 .replace(/([A-Z])/g, " $1")
477 .replace(/^./, (str) => str.toUpperCase())
478 .trim();
479 }
480
481 // Format tag values based on their type
482 function formatTagValue(value, tag) {
483 if (value === undefined || value === null) return "Unknown";
484
485 // Handle arrays
486 if (Array.isArray(value)) {
487 // For GPS coordinates
488 if (tag === "GPSLatitude" || tag === "GPSLongitude") {
489 return value.map((v) => v.toFixed(6)).join(", ");
490 }
491 return value.join(", ");
492 }
493
494 // Handle specific tags
495 switch (tag) {
496 case "ExposureTime":
497 // Format as fraction if less than 1
498 if (value < 1) {
499 const denominator = Math.round(1 / value);
500 return `1/${denominator} sec`;
501 }
502 return `${value.toFixed(2)} sec`;
503
504 case "FNumber":
505 return `f/${value.toFixed(1)}`;
506
507 case "FocalLength":
508 case "FocalLengthIn35mmFilm":
509 return `${Math.round(value)}mm`;
510
511 case "ExposureBiasValue":
512 const sign = value > 0 ? "+" : "";
513 return `${sign}${value.toFixed(1)} EV`;
514
515 default:
516 // For numeric values, format with 2 decimal places if needed
517 if (typeof value === "number" && !Number.isInteger(value)) {
518 return value.toFixed(2);
519 }
520 return value.toString();
521 }
522 }
523
524 // Convert GPS coordinates to decimal degrees
525 function convertGPSToDecimal(coordinates, ref) {
526 if (!coordinates || coordinates.length !== 3) {
527 return null;
528 }
529
530 // Calculate decimal degrees: degrees + minutes/60 + seconds/3600
531 let decimal = coordinates[0] + coordinates[1] / 60 + coordinates[2] / 3600;
532
533 // If south or west, make negative
534 if (ref === "S" || ref === "W") {
535 decimal = -decimal;
536 }
537
538 return decimal;
539 }
540
541 // Process the selected image
542 async function processImage(file) {
543 if (!file || !file.type.startsWith("image/")) {
544 alert("Please select a valid image file");
545 return;
546 }
547
548 // Show loader
549 loaderContainer.style.display = "flex";
550
551 // Display image preview
552 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 image
558 previewContainer.innerHTML = "";
559 previewContainer.appendChild(img);
560 imagePreview.classList.remove("hidden");
561 };
562 };
563 reader.readAsDataURL(file);
564
565 // Process EXIF data
566 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 loader
580 loaderContainer.style.display = "none";
581 }
582 }
583
584 // Display EXIF data in the UI
585 function displayExifData(data) {
586 // Clear previous data
587 exifDataDisplay.innerHTML = "";
588
589 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 }
598
599 // Basic image information
600 let html = '<div class="exif-group">';
601 html += "<h3>Basic Information</h3>";
602
603 const basicTags = [
604 "Make",
605 "Model",
606 "Software",
607 "DateTime",
608 "Artist",
609 "Copyright",
610 "ImageWidth",
611 "ImageHeight",
612 "Orientation",
613 "OrientationDescription",
614 ];
615
616 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 });
626
627 html += "</div>";
628
629 // EXIF specific data
630 if (data.exif && Object.keys(data.exif).length > 0) {
631 html += '<div class="exif-group">';
632 html += "<h3>Camera Settings</h3>";
633
634 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 ];
655
656 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 });
666
667 html += "</div>";
668 }
669
670 // GPS data
671 if (data.gps && Object.keys(data.gps).length > 0) {
672 html += '<div class="exif-group">';
673 html += "<h3>GPS Information</h3>";
674
675 // Process GPS coordinates if available
676 let latitude = null;
677 let longitude = null;
678
679 if (data.gps.GPSLatitude && data.gps.GPSLatitudeRef) {
680 latitude = convertGPSToDecimal(
681 data.gps.GPSLatitude,
682 data.gps.GPSLatitudeRef
683 );
684 }
685
686 if (data.gps.GPSLongitude && data.gps.GPSLongitudeRef) {
687 longitude = convertGPSToDecimal(
688 data.gps.GPSLongitude,
689 data.gps.GPSLongitudeRef
690 );
691 }
692
693 // Display GPS coordinates in a user-friendly format
694 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 }
702
703 // Display other GPS information
704 for (const [tag, value] of Object.entries(data.gps)) {
705 // Skip latitude and longitude as we've already displayed them in a combined format
706 if (
707 [
708 "GPSLatitude",
709 "GPSLatitudeRef",
710 "GPSLongitude",
711 "GPSLongitudeRef",
712 ].includes(tag)
713 ) {
714 continue;
715 }
716
717 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 }
724
725 html += "</div>";
726 }
727
728 // Set the HTML content and show the display
729 exifDataDisplay.innerHTML = html;
730 exifDataDisplay.classList.remove("hidden");
731 }
732
733 // Add event listeners
734 document.addEventListener("DOMContentLoaded", () => {
735 // File input change event
736 imageInput.addEventListener("change", (e) => {
737 if (e.target.files && e.target.files[0]) {
738 processImage(e.target.files[0]);
739 }
740 });
741
742 // Drag and drop events
743 dropArea.addEventListener("dragover", (e) => {
744 e.preventDefault();
745 dropArea.classList.add("drag-over");
746 });
747
748 dropArea.addEventListener("dragleave", () => {
749 dropArea.classList.remove("drag-over");
750 });
751
752 dropArea.addEventListener("drop", (e) => {
753 e.preventDefault();
754 dropArea.classList.remove("drag-over");
755
756 if (e.dataTransfer.files && e.dataTransfer.files[0]) {
757 processImage(e.dataTransfer.files[0]);
758 }
759 });
760
761 // Click on drop area to trigger file input
762 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 names
  • formatTagValue() - Formats tag values based on their type (e.g., exposure time as fractions)
  • convertGPSToDecimal() - Converts GPS coordinates to decimal degrees
  • processImage() - Main function that handles the selected image
  • displayExifData() - Creates HTML to show the extracted EXIF data

How It Works

  1. 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.

  2. 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
  3. 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

  1. User selects or drops an image file
  2. A preview of the image is displayed
  3. The EXIF service extracts metadata from the image
  4. The metadata is formatted and displayed in a user-friendly way
  5. 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.

html
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 }
17
18 * {
19 box-sizing: border-box;
20 margin: 0;
21 padding: 0;
22 }
23
24 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 }
31
32 .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 }
40
41 h1 {
42 text-align: center;
43 margin-bottom: 30px;
44 color: var(--primary-color);
45 }
46
47 .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 }
57
58 .drop-area.drag-over {
59 border-color: var(--primary-color);
60 background-color: rgba(52, 152, 219, 0.1);
61 }
62
63 .drop-area p {
64 font-size: 18px;
65 margin-bottom: 15px;
66 }
67
68 .drop-area .icon {
69 font-size: 48px;
70 color: var(--primary-color);
71 margin-bottom: 15px;
72 }
73
74 .file-input-wrapper {
75 position: relative;
76 display: inline-block;
77 }
78
79 .file-input {
80 position: absolute;
81 left: 0;
82 top: 0;
83 opacity: 0;
84 width: 100%;
85 height: 100%;
86 cursor: pointer;
87 }
88
89 .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 }
99
100 .browse-button:hover {
101 background-color: var(--secondary-color);
102 }
103
104 .image-preview {
105 display: flex;
106 justify-content: space-between;
107 margin-bottom: 30px;
108 height: 300px;
109 }
110
111 .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 }
122
123 .preview-container img {
124 max-width: 100%;
125 max-height: 100%;
126 object-fit: contain;
127 }
128
129 .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 }
137
138 .exif-data.hidden,
139 .image-preview.hidden {
140 display: none;
141 }
142
143 .exif-group {
144 margin-bottom: 15px;
145 }
146
147 .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 }
153
154 .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 }
161
162 .exif-label {
163 font-weight: 500;
164 flex: 0 0 50%;
165 }
166
167 .exif-value {
168 flex: 0 0 50%;
169 text-align: right;
170 word-break: break-word;
171 }
172
173 /* 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 }
186
187 .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 }
195
196 .loader-text {
197 margin-left: 15px;
198 font-size: 18px;
199 font-weight: 500;
200 }
201
202 @keyframes spin {
203 0% {
204 transform: rotate(0deg);
205 }
206 100% {
207 transform: rotate(360deg);
208 }
209 }
210
211 .no-exif-message {
212 text-align: center;
213 padding: 20px;
214 color: var(--danger-color);
215 font-weight: 500;
216 }
217
218 .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 }
227
228 .section-title {
229 margin: 30px 0 15px;
230 font-size: 20px;
231 color: var(--primary-color);
232 }
233
234 @media (max-width: 768px) {
235 .image-preview {
236 flex-direction: column;
237 height: auto;
238 }
239
240 .preview-container,
241 .exif-data {
242 flex: 0 0 100%;
243 width: 100%;
244 margin-bottom: 20px;
245 }
246
247 .preview-container {
248 height: 250px;
249 }
250
251 .exif-data {
252 height: 300px;
253 }
254 }
255 </style>
256 </head>
257 <body>
258 <div class="container">
259 <h1>EXIF Data Viewer</h1>
260
261 <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 <input
268 type="file"
269 class="file-input"
270 id="imageInput"
271 accept="image/*"
272 />
273 </div>
274 </div>
275
276 <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>
285
286 <!-- Loader -->
287 <div class="loader-container" id="loaderContainer">
288 <div class="loader"></div>
289 <div class="loader-text">Processing EXIF data...</div>
290 </div>
291
292 <script>
293 class ExifService {
294 constructor() {
295 this.tiffHeaderOffset = null;
296 this.bigEndian = null;
297 this.tags = {
298 // EXIF tags and their meanings
299 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",
317
318 // EXIF IFD tags
319 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",
363
364 // GPS tags
365 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 };
397
398 // Orientation values
399 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 }
410
411 /**
412 * Process an image file and extract its EXIF data
413 * @param {File|Blob} imageFile - The image file to process
414 * @returns {Promise<Object>} - A promise that resolves to an object containing EXIF data
415 */
416 extractExifData(imageFile) {
417 return new Promise((resolve, reject) => {
418 if (
419 !imageFile ||
420 !["image/jpeg", "image/jpg", "image/tiff"].includes(
421 imageFile.type
422 )
423 ) {
424 reject(
425 new Error(
426 "Unsupported file type. Only JPEG and TIFF formats support EXIF."
427 )
428 );
429 return;
430 }
431
432 const reader = new FileReader();
433
434 reader.onload = (e) => {
435 try {
436 const buffer = e.target.result;
437 const dataView = new DataView(buffer);
438
439 // Check for JPEG format
440 if (
441 dataView.getUint8(0) !== 0xff ||
442 dataView.getUint8(1) !== 0xd8
443 ) {
444 reject(new Error("Not a valid JPEG"));
445 return;
446 }
447
448 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 };
456
457 reader.onerror = () => {
458 reject(new Error("Error reading file"));
459 };
460
461 reader.readAsArrayBuffer(imageFile);
462 });
463 }
464
465 /**
466 * Parse EXIF data from a DataView object
467 * @param {DataView} dataView - The DataView containing the image data
468 * @returns {Object} - An object containing the parsed EXIF data
469 */
470 parseExifData(dataView) {
471 const length = dataView.byteLength;
472 let offset = 2; // Skip the first two bytes (JPEG marker)
473 const exifData = {};
474
475 // Search for the EXIF APP1 marker (0xFFE1)
476 while (offset < length) {
477 if (dataView.getUint8(offset) !== 0xff) {
478 offset++;
479 continue;
480 }
481
482 const marker = dataView.getUint8(offset + 1);
483
484 // If we found the EXIF APP1 marker
485 if (marker === 0xe1) {
486 const exifOffset = offset + 4; // Skip marker and size
487 const exifLength = dataView.getUint16(offset + 2, false);
488
489 // Check for "Exif\0\0" header
490 if (
491 this.getStringFromCharCodes(dataView, exifOffset, 6) ===
492 "Exif\0\0"
493 ) {
494 return this.readExifTags(
495 dataView,
496 exifOffset + 6,
497 exifLength - 6
498 );
499 }
500 } else if (marker === 0xda) {
501 // Start of Scan marker - no more metadata
502 break;
503 }
504
505 // Move to the next marker
506 offset += 2 + dataView.getUint16(offset + 2, false);
507 }
508
509 return exifData;
510 }
511
512 /**
513 * Read EXIF tags from the specified offset
514 * @param {DataView} dataView - The DataView containing the image data
515 * @param {number} start - The offset to start reading from
516 * @param {number} length - The length of data to read
517 * @returns {Object} - An object containing the parsed EXIF tags
518 */
519 readExifTags(dataView, start, length) {
520 const result = {};
521
522 // Determine endianness
523 if (dataView.getUint16(start) === 0x4949) {
524 this.bigEndian = false; // Little endian
525 } else if (dataView.getUint16(start) === 0x4d4d) {
526 this.bigEndian = true; // Big endian
527 } else {
528 throw new Error("Invalid TIFF data: endianness marker not found");
529 }
530
531 // Check for TIFF marker
532 if (dataView.getUint16(start + 2, !this.bigEndian) !== 0x002a) {
533 throw new Error("Invalid TIFF data: TIFF marker not found");
534 }
535
536 // Get offset to first IFD (Image File Directory)
537 const firstIFDOffset = dataView.getUint32(start + 4, !this.bigEndian);
538 this.tiffHeaderOffset = start;
539
540 // Read the main IFD
541 const ifdData = this.readIFD(
542 dataView,
543 this.tiffHeaderOffset + firstIFDOffset
544 );
545 Object.assign(result, ifdData);
546
547 // Check for and read Exif sub-IFD if it exists
548 if (ifdData.ExifIFDPointer) {
549 const exifData = this.readIFD(
550 dataView,
551 this.tiffHeaderOffset + ifdData.ExifIFDPointer
552 );
553 result.exif = exifData;
554 delete result.ExifIFDPointer;
555 }
556
557 // Check for and read GPS sub-IFD if it exists
558 if (ifdData.GPSInfoIFDPointer) {
559 const gpsData = this.readIFD(
560 dataView,
561 this.tiffHeaderOffset + ifdData.GPSInfoIFDPointer
562 );
563 result.gps = gpsData;
564 delete result.GPSInfoIFDPointer;
565 }
566
567 // Add orientation description if orientation exists
568 if (result.Orientation) {
569 result.OrientationDescription =
570 this.orientationDescriptions[result.Orientation] || "Unknown";
571 }
572
573 return result;
574 }
575
576 /**
577 * Read an Image File Directory
578 * @param {DataView} dataView - The DataView containing the image data
579 * @param {number} offset - The offset to start reading from
580 * @returns {Object} - An object containing the parsed IFD entries
581 */
582 readIFD(dataView, offset) {
583 const result = {};
584 const numEntries = dataView.getUint16(offset, !this.bigEndian);
585
586 for (let i = 0; i < numEntries; i++) {
587 const entryOffset = offset + 2 + i * 12; // 12 bytes per entry
588 const tagNumber = dataView.getUint16(entryOffset, !this.bigEndian);
589 const dataType = dataView.getUint16(
590 entryOffset + 2,
591 !this.bigEndian
592 );
593 const numValues = dataView.getUint32(
594 entryOffset + 4,
595 !this.bigEndian
596 );
597 const valueOffset = dataView.getUint32(
598 entryOffset + 8,
599 !this.bigEndian
600 );
601
602 // Get tag name if known, otherwise use hex code
603 const tagName =
604 this.tags[tagNumber] || `0x${tagNumber.toString(16)}`;
605
606 // Get tag value
607 let tagValue;
608 const totalSize = this.getDataTypeSize(dataType) * numValues;
609
610 if (totalSize <= 4) {
611 // Value is stored in the offset field itself
612 tagValue = this.readTagValue(
613 dataView,
614 entryOffset + 8,
615 dataType,
616 numValues
617 );
618 } else {
619 // Value is stored at the specified offset
620 tagValue = this.readTagValue(
621 dataView,
622 this.tiffHeaderOffset + valueOffset,
623 dataType,
624 numValues
625 );
626 }
627
628 result[tagName] = tagValue;
629 }
630
631 return result;
632 }
633
634 /**
635 * Get the size in bytes of a specific data type
636 * @param {number} dataType - The EXIF data type
637 * @returns {number} - The size in bytes
638 */
639 getDataTypeSize(dataType) {
640 const sizes = {
641 1: 1, // BYTE
642 2: 1, // ASCII
643 3: 2, // SHORT
644 4: 4, // LONG
645 5: 8, // RATIONAL
646 6: 1, // SBYTE
647 7: 1, // UNDEFINED
648 8: 2, // SSHORT
649 9: 4, // SLONG
650 10: 8, // SRATIONAL
651 11: 4, // FLOAT
652 12: 8, // DOUBLE
653 };
654
655 return sizes[dataType] || 0;
656 }
657
658 /**
659 * Read a tag value based on its data type
660 * @param {DataView} dataView - The DataView containing the image data
661 * @param {number} offset - The offset to start reading from
662 * @param {number} dataType - The data type of the value
663 * @param {number} numValues - The number of values to read
664 * @returns {*} - The parsed tag value
665 */
666 readTagValue(dataView, offset, dataType, numValues) {
667 let values = [];
668 let i, size;
669
670 switch (dataType) {
671 case 1: // BYTE
672 case 6: // SBYTE
673 case 7: // UNDEFINED
674 for (i = 0; i < numValues; i++) {
675 values.push(dataView.getUint8(offset + i));
676 }
677 break;
678
679 case 2: // ASCII
680 // Read null-terminated ASCII string
681 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);
688
689 case 3: // SHORT
690 size = 2;
691 for (i = 0; i < numValues; i++) {
692 values.push(
693 dataView.getUint16(offset + i * size, !this.bigEndian)
694 );
695 }
696 break;
697
698 case 8: // SSHORT
699 size = 2;
700 for (i = 0; i < numValues; i++) {
701 values.push(
702 dataView.getInt16(offset + i * size, !this.bigEndian)
703 );
704 }
705 break;
706
707 case 4: // LONG
708 size = 4;
709 for (i = 0; i < numValues; i++) {
710 values.push(
711 dataView.getUint32(offset + i * size, !this.bigEndian)
712 );
713 }
714 break;
715
716 case 9: // SLONG
717 size = 4;
718 for (i = 0; i < numValues; i++) {
719 values.push(
720 dataView.getInt32(offset + i * size, !this.bigEndian)
721 );
722 }
723 break;
724
725 case 5: // RATIONAL
726 size = 8;
727 for (i = 0; i < numValues; i++) {
728 const numerator = dataView.getUint32(
729 offset + i * size,
730 !this.bigEndian
731 );
732 const denominator = dataView.getUint32(
733 offset + i * size + 4,
734 !this.bigEndian
735 );
736 values.push(numerator / denominator);
737 }
738 break;
739
740 case 10: // SRATIONAL
741 size = 8;
742 for (i = 0; i < numValues; i++) {
743 const numerator = dataView.getInt32(
744 offset + i * size,
745 !this.bigEndian
746 );
747 const denominator = dataView.getInt32(
748 offset + i * size + 4,
749 !this.bigEndian
750 );
751 values.push(numerator / denominator);
752 }
753 break;
754
755 default:
756 values = [dataType, numValues]; // Return the data type and count for unknown types
757 }
758
759 // If there's only one value, return it directly instead of an array
760 return numValues === 1 ? values[0] : values;
761 }
762
763 /**
764 * Convert array of character codes to a string
765 * @param {DataView} dataView - The DataView containing the data
766 * @param {number} start - The start offset
767 * @param {number} length - The number of characters to read
768 * @returns {string} - The resulting string
769 */
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 }
778
779 // DOM Elements
780 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");
786
787 // Initialize EXIF service
788 const exifService = new ExifService();
789
790 // Format tag names for display (convert camelCase to Title Case with spaces)
791 function formatTagName(tag) {
792 // Handle special cases
793 if (tag === "FNumber") return "F-Number";
794 if (tag === "ISOSpeedRatings") return "ISO";
795
796 // Add space before capital letters and capitalize first letter
797 return tag
798 .replace(/([A-Z])/g, " $1")
799 .replace(/^./, (str) => str.toUpperCase())
800 .trim();
801 }
802
803 // Format tag values based on their type
804 function formatTagValue(value, tag) {
805 if (value === undefined || value === null) return "Unknown";
806
807 // Handle arrays
808 if (Array.isArray(value)) {
809 // For GPS coordinates
810 if (tag === "GPSLatitude" || tag === "GPSLongitude") {
811 return value.map((v) => v.toFixed(6)).join(", ");
812 }
813 return value.join(", ");
814 }
815
816 // Handle specific tags
817 switch (tag) {
818 case "ExposureTime":
819 // Format as fraction if less than 1
820 if (value < 1) {
821 const denominator = Math.round(1 / value);
822 return `1/${denominator} sec`;
823 }
824 return `${value.toFixed(2)} sec`;
825
826 case "FNumber":
827 return `f/${value.toFixed(1)}`;
828
829 case "FocalLength":
830 case "FocalLengthIn35mmFilm":
831 return `${Math.round(value)}mm`;
832
833 case "ExposureBiasValue":
834 const sign = value > 0 ? "+" : "";
835 return `${sign}${value.toFixed(1)} EV`;
836
837 default:
838 // For numeric values, format with 2 decimal places if needed
839 if (typeof value === "number" && !Number.isInteger(value)) {
840 return value.toFixed(2);
841 }
842 return value.toString();
843 }
844 }
845
846 // Convert GPS coordinates to decimal degrees
847 function convertGPSToDecimal(coordinates, ref) {
848 if (!coordinates || coordinates.length !== 3) {
849 return null;
850 }
851
852 // Calculate decimal degrees: degrees + minutes/60 + seconds/3600
853 let decimal =
854 coordinates[0] + coordinates[1] / 60 + coordinates[2] / 3600;
855
856 // If south or west, make negative
857 if (ref === "S" || ref === "W") {
858 decimal = -decimal;
859 }
860
861 return decimal;
862 }
863
864 // Process the selected image
865 async function processImage(file) {
866 if (!file || !file.type.startsWith("image/")) {
867 alert("Please select a valid image file");
868 return;
869 }
870
871 // Show loader
872 loaderContainer.style.display = "flex";
873
874 // Display image preview
875 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 image
881 previewContainer.innerHTML = "";
882 previewContainer.appendChild(img);
883 imagePreview.classList.remove("hidden");
884 };
885 };
886 reader.readAsDataURL(file);
887
888 // Process EXIF data
889 try {
890 const exifData = await exifService.extractExifData(file);
891 displayExifData(exifData);
892 } catch (error) {
893 console.error(error);
894 exifDataDisplay.innerHTML = `
895 <div class="no-exif-message">
896 <p>No EXIF data found or error processing the image.</p>
897 <p>Error: ${error.message}</p>
898 </div>
899 `;
900 exifDataDisplay.classList.remove("hidden");
901 } finally {
902 // Hide loader
903 loaderContainer.style.display = "none";
904 }
905 }
906
907 // Display EXIF data in the UI
908 function displayExifData(data) {
909 // Clear previous data
910 exifDataDisplay.innerHTML = "";
911
912 if (!data || Object.keys(data).length === 0) {
913 exifDataDisplay.innerHTML = `
914 <div class="no-exif-message">
915 <p>No EXIF data found in this image.</p>
916 </div>
917 `;
918 exifDataDisplay.classList.remove("hidden");
919 return;
920 }
921
922 // Basic image information
923 let html = '<div class="exif-group">';
924 html += "<h3>Basic Information</h3>";
925
926 const basicTags = [
927 "Make",
928 "Model",
929 "Software",
930 "DateTime",
931 "Artist",
932 "Copyright",
933 "ImageWidth",
934 "ImageHeight",
935 "Orientation",
936 "OrientationDescription",
937 ];
938
939 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 });
949
950 html += "</div>";
951
952 // EXIF specific data
953 if (data.exif && Object.keys(data.exif).length > 0) {
954 html += '<div class="exif-group">';
955 html += "<h3>Camera Settings</h3>";
956
957 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 ];
978
979 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 });
989
990 html += "</div>";
991 }
992
993 // GPS data
994 if (data.gps && Object.keys(data.gps).length > 0) {
995 html += '<div class="exif-group">';
996 html += "<h3>GPS Information</h3>";
997
998 // Process GPS coordinates if available
999 let latitude = null;
1000 let longitude = null;
1001
1002 if (data.gps.GPSLatitude && data.gps.GPSLatitudeRef) {
1003 latitude = convertGPSToDecimal(
1004 data.gps.GPSLatitude,
1005 data.gps.GPSLatitudeRef
1006 );
1007 }
1008
1009 if (data.gps.GPSLongitude && data.gps.GPSLongitudeRef) {
1010 longitude = convertGPSToDecimal(
1011 data.gps.GPSLongitude,
1012 data.gps.GPSLongitudeRef
1013 );
1014 }
1015
1016 // Display GPS coordinates in a user-friendly format
1017 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 }
1025
1026 // Display other GPS information
1027 for (const [tag, value] of Object.entries(data.gps)) {
1028 // Skip latitude and longitude as we've already displayed them in a combined format
1029 if (
1030 [
1031 "GPSLatitude",
1032 "GPSLatitudeRef",
1033 "GPSLongitude",
1034 "GPSLongitudeRef",
1035 ].includes(tag)
1036 ) {
1037 continue;
1038 }
1039
1040 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 }
1047
1048 html += "</div>";
1049 }
1050
1051 // Set the HTML content and show the display
1052 exifDataDisplay.innerHTML = html;
1053 exifDataDisplay.classList.remove("hidden");
1054 }
1055
1056 // Add event listeners
1057 document.addEventListener("DOMContentLoaded", () => {
1058 // File input change event
1059 imageInput.addEventListener("change", (e) => {
1060 if (e.target.files && e.target.files[0]) {
1061 processImage(e.target.files[0]);
1062 }
1063 });
1064
1065 // Drag and drop events
1066 dropArea.addEventListener("dragover", (e) => {
1067 e.preventDefault();
1068 dropArea.classList.add("drag-over");
1069 });
1070
1071 dropArea.addEventListener("dragleave", () => {
1072 dropArea.classList.remove("drag-over");
1073 });
1074
1075 dropArea.addEventListener("drop", (e) => {
1076 e.preventDefault();
1077 dropArea.classList.remove("drag-over");
1078
1079 if (e.dataTransfer.files && e.dataTransfer.files[0]) {
1080 processImage(e.dataTransfer.files[0]);
1081 }
1082 });
1083
1084 // Click on drop area to trigger file input
1085 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.

JavaScript Development Space

JSDev Space – Your go-to hub for JavaScript development. Explore expert guides, best practices, and the latest trends in web development, React, Node.js, and more. Stay ahead with cutting-edge tutorials, tools, and insights for modern JS developers. 🚀

Join our growing community of developers! Follow us on social media for updates, coding tips, and exclusive content. Stay connected and level up your JavaScript skills with us! 🔥

© 2025 JavaScript Development Space - Master JS and NodeJS. All rights reserved.