Skip to content

Commit 133d8b3

Browse files
committed
Merge branch 'feat/image-exif-reader' into chore/all-my-stuffs
# Conflicts: # package.json # pnpm-lock.yaml # src/tools/index.ts
2 parents f22ea6c + 5751977 commit 133d8b3

File tree

6 files changed

+200
-1
lines changed

6 files changed

+200
-1
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@
158158
"event-cron-parser": "^1.0.34",
159159
"fflate": "^0.8.2",
160160
"figlet": "^1.7.0",
161+
"exifreader": "^4.20.0",
161162
"figue": "^1.2.0",
162163
"flatten-anything": "^4.0.1",
163164
"file-type": "^19.0.0",
@@ -196,6 +197,7 @@
196197
"js-base64": "^3.7.7",
197198
"js-beautify": "^1.15.1",
198199
"image-to-ascii-art": "^0.0.4",
200+
"jpeg-quality-estimator": "^1.0.1",
199201
"json5": "^2.2.3",
200202
"jsonpath": "^1.1.1",
201203
"jsonar-mod": "^1.9.0",

pnpm-lock.yaml

Lines changed: 25 additions & 1 deletion
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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ import { tool as pdfLinearize } from './pdf-linearize';
199199
import { tool as manyUnitsConverter } from './many-units-converter';
200200
import { tool as powerConverter } from './power-converter';
201201
import { tool as dockerComposeConverter } from './docker-compose-converter';
202+
import { tool as imageExifReader } from './image-exif-reader';
202203
import { tool as yamlViewer } from './yaml-viewer';
203204
import { tool as barcodeReader } from './barcode-reader';
204205
import { tool as barcodeGenerator } from './barcode-generator';
@@ -349,6 +350,7 @@ export const toolsByCategory: ToolCategory[] = [
349350
cameraRecorder,
350351
removeExif,
351352
imageToAsciiArt,
353+
imageExifReader,
352354
],
353355
},
354356
{

0 commit comments

Comments
 (0)