Skip to content

Commit d7c207c

Browse files
committed
feat(new tool): Image EXIF Reader
- Read EXIF/IPTC/XMP and other metadata from image files - Estimate JPEG Quality factor
1 parent 670f735 commit d7c207c

File tree

6 files changed

+208
-4
lines changed

6 files changed

+208
-4
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,13 @@
5757
"date-fns": "^2.29.3",
5858
"dompurify": "^3.0.6",
5959
"emojilib": "^3.0.10",
60+
"exifreader": "^4.20.0",
6061
"figue": "^1.2.0",
6162
"fuse.js": "^6.6.2",
6263
"highlight.js": "^11.7.0",
6364
"iarna-toml-esm": "^3.0.5",
6465
"ibantools": "^4.3.3",
66+
"jpeg-quality-estimator": "^1.0.1",
6567
"json5": "^2.2.3",
6668
"jwt-decode": "^3.1.2",
6769
"libphonenumber-js": "^1.10.28",

pnpm-lock.yaml

Lines changed: 27 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<script setup lang="ts">
2+
import ExifReader from 'exifreader';
3+
import getJpegQuality from 'jpeg-quality-estimator';
4+
import { formatBytes } from '@/utils/convert';
5+
6+
interface Tag {
7+
id: number
8+
name: string
9+
value: any
10+
description: string
11+
}
12+
13+
interface TagsSection {
14+
[name: string]: Tag
15+
}
16+
17+
const tagsSections = ref<{ name: string; title: string }[]>([
18+
{ name: 'file', title: 'File Tags' },
19+
{ name: 'jfif', title: 'JFIF Tags' },
20+
{ name: 'pngFile', title: 'PNG File Tags' },
21+
{ name: 'pngText', title: 'PNG Text Tags' },
22+
{ name: 'png', title: 'PNG Tags' },
23+
{ name: 'exif', title: 'EXIF Tags' },
24+
{ name: 'iptc', title: 'IPTC Tags' },
25+
{ name: 'xmp', title: 'XMP Tags' },
26+
{ name: 'icc', title: 'ICC Tags' },
27+
{ name: 'riff', title: 'RIFF Tags' },
28+
{ name: 'gif', title: 'GIF Tags' },
29+
{ name: 'Thumbnail', title: 'Thumbnail Tags' },
30+
{ name: 'photoshop', title: 'Photoshop Tags' },
31+
]);
32+
33+
const errorMessage = ref<string>('');
34+
const tags = ref<ExifReader.ExpandedTags>({});
35+
const status = ref<'idle' | 'parsed' | 'error' | 'loading'>('idle');
36+
const file = ref<File | null>(null);
37+
const quality = ref<number>(-1);
38+
39+
const openStreetMapUrl = computed(
40+
() => {
41+
const gpsLatitude = tags.value.gps?.Latitude;
42+
const gpsLongitude = tags.value.gps?.Longitude;
43+
return gpsLatitude && gpsLongitude ? `https://www.openstreetmap.org/?mlat=${gpsLatitude}&mlon=${gpsLongitude}#map=18/${gpsLatitude}/${gpsLongitude}` : undefined;
44+
},
45+
);
46+
47+
async function onImageUploaded(uploadedFile: File) {
48+
file.value = uploadedFile;
49+
50+
const fileBuffer = await uploadedFile.arrayBuffer();
51+
status.value = 'loading';
52+
try {
53+
quality.value = getJpegQuality(new Uint8Array(fileBuffer));
54+
}
55+
catch (e) {
56+
quality.value = -1;
57+
}
58+
try {
59+
tags.value = await ExifReader.load(fileBuffer, { expanded: true });
60+
status.value = 'parsed';
61+
}
62+
catch (e: any) {
63+
errorMessage.value = e.toString();
64+
status.value = 'error';
65+
}
66+
}
67+
function getSection(sectionName: string): TagsSection | null {
68+
const sections = tags.value as { [name: string]: TagsSection };
69+
return sections[sectionName] ? sections[sectionName] : null;
70+
}
71+
const addSpacesToTagNames = (label: string) => label.replace(/([A-Z][a-z])/g, ' $1').trim();
72+
</script>
73+
74+
<template>
75+
<div style="flex: 0 0 100%">
76+
<div mx-auto max-w-600px>
77+
<c-file-upload title="Drag and drop a Image file here, or click to select a file" @file-upload="onImageUploaded" />
78+
79+
<c-card v-if="file" mt-4 flex gap-2>
80+
<div font-bold>
81+
{{ file.name }}
82+
</div>
83+
84+
<div>
85+
{{ formatBytes(file.size) }}
86+
</div>
87+
88+
<div v-if="tags.Thumbnail">
89+
<img :src="`data:image/jpeg;base64,${tags.Thumbnail.base64}`" max-w-200px>
90+
</div>
91+
</c-card>
92+
93+
<div v-if="status === 'error'">
94+
<c-alert mt-4>
95+
Error parsing image file: {{ errorMessage }}
96+
</c-alert>
97+
</div>
98+
99+
<c-card v-if="quality >= 0" title="JPEG Quality" mt-4>
100+
<input-copyable
101+
label="JPEG Quality (%)"
102+
label-position="left"
103+
label-width="150px"
104+
label-align="right"
105+
mb-2
106+
:value="quality"
107+
/>
108+
</c-card>
109+
110+
<c-card v-if="status === 'parsed' && openStreetMapUrl" title="GPS Infos" mt-4>
111+
<div flex gap-2>
112+
<c-label label="Latitude">
113+
{{ tags.gps?.Latitude?.toFixed(4) }}
114+
</c-label>
115+
<c-label label="Longitude">
116+
{{ tags.gps?.Longitude?.toFixed(4) }}
117+
</c-label>
118+
<c-label label="Altitude">
119+
{{ tags.gps?.Altitude?.toFixed(4) }}
120+
</c-label>
121+
</div>
122+
<c-button :href="openStreetMapUrl" target="_blank" mt-4>
123+
Localize on Open Street Map
124+
</c-button>
125+
</c-card>
126+
<c-card v-if="status === 'parsed' && !openStreetMapUrl" mt-4>
127+
No GPS Information
128+
</c-card>
129+
130+
<div v-if="status === 'parsed'">
131+
<div v-for="section in tagsSections" :key="section.name">
132+
<c-card v-if="getSection(section.name)" :title="section.title" mt-4>
133+
<input-copyable
134+
v-for="({ description }, tagName) in getSection(section.name)"
135+
:key="tagName"
136+
:label="addSpacesToTagNames(String(tagName))"
137+
label-position="left"
138+
label-width="150px"
139+
label-align="right"
140+
mb-2
141+
disabled="disabled"
142+
:value="description ?? '<binary>'"
143+
/>
144+
</c-card>
145+
</div>
146+
</div>
147+
148+
<div v-if="status === 'parsed'" style="flex: 0 0 100%" mt-5 flex flex-col gap-4 />
149+
150+
<div font-size-3>
151+
Made with <a href="https://github.com/mattiasw/ExifReader" target="_blank">ExifReader</a>
152+
</div>
153+
</div>
154+
</div>
155+
</template>

src/tools/image-exif-reader/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { FileInfo } from '@vicons/tabler';
2+
import { defineTool } from '../tool';
3+
4+
export const tool = defineTool({
5+
name: 'Image EXIF/Metadata/GPS/JPEG Quality reader',
6+
path: '/image-exif-reader',
7+
description: 'Read EXIF, IPTC, XMP, GPS and other metadata, JPEG Quality, and other infos from images files',
8+
keywords: ['image', 'exif', 'reader', 'iptc', 'gps', 'xmp', 'jpeg', 'quality'],
9+
component: () => import('./image-exif-reader.vue'),
10+
icon: FileInfo,
11+
createdAt: new Date('2024-01-09'),
12+
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
declare module 'jpeg-quality-estimator' {
2+
const getJpegQuality: (file: Uint8Array) => number;
3+
export default getJpegQuality;
4+
}

src/tools/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import { tool as urlParser } from './url-parser';
7575
import { tool as uuidGenerator } from './uuid-generator';
7676
import { tool as macAddressLookup } from './mac-address-lookup';
7777
import { tool as xmlFormatter } from './xml-formatter';
78+
import { tool as imageExifReader } from './image-exif-reader';
7879

7980
export const toolsByCategory: ToolCategory[] = [
8081
{
@@ -124,7 +125,13 @@ export const toolsByCategory: ToolCategory[] = [
124125
},
125126
{
126127
name: 'Images and videos',
127-
components: [qrCodeGenerator, wifiQrCodeGenerator, svgPlaceholderGenerator, cameraRecorder],
128+
components: [
129+
qrCodeGenerator,
130+
wifiQrCodeGenerator,
131+
svgPlaceholderGenerator,
132+
cameraRecorder,
133+
imageExifReader,
134+
],
128135
},
129136
{
130137
name: 'Development',

0 commit comments

Comments
 (0)