mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-05-05 13:57:10 -04:00
feat(new tool): Image EXIF Reader
- Read EXIF/IPTC/XMP and other metadata from image files - Estimate JPEG Quality factor
This commit is contained in:
parent
670f735501
commit
d7c207cc52
6 changed files with 208 additions and 4 deletions
|
@ -57,11 +57,13 @@
|
|||
"date-fns": "^2.29.3",
|
||||
"dompurify": "^3.0.6",
|
||||
"emojilib": "^3.0.10",
|
||||
"exifreader": "^4.20.0",
|
||||
"figue": "^1.2.0",
|
||||
"fuse.js": "^6.6.2",
|
||||
"highlight.js": "^11.7.0",
|
||||
"iarna-toml-esm": "^3.0.5",
|
||||
"ibantools": "^4.3.3",
|
||||
"jpeg-quality-estimator": "^1.0.1",
|
||||
"json5": "^2.2.3",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"libphonenumber-js": "^1.10.28",
|
||||
|
|
30
pnpm-lock.yaml
generated
30
pnpm-lock.yaml
generated
|
@ -71,6 +71,9 @@ dependencies:
|
|||
emojilib:
|
||||
specifier: ^3.0.10
|
||||
version: 3.0.10
|
||||
exifreader:
|
||||
specifier: ^4.20.0
|
||||
version: 4.20.0
|
||||
figue:
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.0
|
||||
|
@ -86,6 +89,9 @@ dependencies:
|
|||
ibantools:
|
||||
specifier: ^4.3.3
|
||||
version: 4.3.3
|
||||
jpeg-quality-estimator:
|
||||
specifier: ^1.0.1
|
||||
version: 1.0.1
|
||||
json5:
|
||||
specifier: ^2.2.3
|
||||
version: 2.2.3
|
||||
|
@ -3374,7 +3380,7 @@ packages:
|
|||
dependencies:
|
||||
'@unhead/dom': 0.5.1
|
||||
'@unhead/schema': 0.5.1
|
||||
'@vueuse/shared': 10.6.1(vue@3.3.4)
|
||||
'@vueuse/shared': 10.7.2(vue@3.3.4)
|
||||
unhead: 0.5.1
|
||||
vue: 3.3.4
|
||||
transitivePeerDependencies:
|
||||
|
@ -4016,8 +4022,8 @@ packages:
|
|||
- vue
|
||||
dev: false
|
||||
|
||||
/@vueuse/shared@10.6.1(vue@3.3.4):
|
||||
resolution: {integrity: sha512-TECVDTIedFlL0NUfHWncf3zF9Gc4VfdxfQc8JFwoVZQmxpONhLxFrlm0eHQeidHj4rdTPL3KXJa0TZCk1wnc5Q==}
|
||||
/@vueuse/shared@10.7.2(vue@3.3.4):
|
||||
resolution: {integrity: sha512-qFbXoxS44pi2FkgFjPvF4h7c9oMDutpyBdcJdMYIMg9XyXli2meFMuaKn+UMgsClo//Th6+beeCgqweT/79BVA==}
|
||||
dependencies:
|
||||
vue-demi: 0.14.6(vue@3.3.4)
|
||||
transitivePeerDependencies:
|
||||
|
@ -4025,6 +4031,13 @@ packages:
|
|||
- vue
|
||||
dev: false
|
||||
|
||||
/@xmldom/xmldom@0.8.10:
|
||||
resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@zhead/schema@1.0.0-beta.13:
|
||||
resolution: {integrity: sha512-P1A1vRGFBhITco8Iw4/hvnDYoE/SoVrd71dW1pBFdXJb3vP+pBtoOuhbEKy0ROJGOyzQuqvFibcwzyLlWMqNiQ==}
|
||||
dev: false
|
||||
|
@ -5567,6 +5580,13 @@ packages:
|
|||
strip-final-newline: 2.0.0
|
||||
dev: true
|
||||
|
||||
/exifreader@4.20.0:
|
||||
resolution: {integrity: sha512-C28BhOHe5svd0Jj/5DGSIXD3PnPp46gfvHN4OkRfvHYZHkcJMhxeUxlwsgJ6Yl62zlZRtmfN+9suZFg0fv4hgg==}
|
||||
requiresBuild: true
|
||||
optionalDependencies:
|
||||
'@xmldom/xmldom': 0.8.10
|
||||
dev: false
|
||||
|
||||
/extend-shallow@2.0.1:
|
||||
resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
@ -6490,6 +6510,10 @@ packages:
|
|||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/jpeg-quality-estimator@1.0.1:
|
||||
resolution: {integrity: sha512-Znaq+msIqs8Gmhg9JSdDjxUAZMOwYXWIURrfluimn5u2yJ4QAEDhf0tnTMkv3ikcHJoJysG5ewxfbqUXyw/Djg==}
|
||||
dev: false
|
||||
|
||||
/js-beautify@1.14.6:
|
||||
resolution: {integrity: sha512-GfofQY5zDp+cuHc+gsEXKPpNw2KbPddreEo35O6jT6i0RVK6LhsoYBhq5TvK4/n74wnA0QbK8gGd+jUZwTMKJw==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
155
src/tools/image-exif-reader/image-exif-reader.vue
Normal file
155
src/tools/image-exif-reader/image-exif-reader.vue
Normal file
|
@ -0,0 +1,155 @@
|
|||
<script setup lang="ts">
|
||||
import ExifReader from 'exifreader';
|
||||
import getJpegQuality from 'jpeg-quality-estimator';
|
||||
import { formatBytes } from '@/utils/convert';
|
||||
|
||||
interface Tag {
|
||||
id: number
|
||||
name: string
|
||||
value: any
|
||||
description: string
|
||||
}
|
||||
|
||||
interface TagsSection {
|
||||
[name: string]: Tag
|
||||
}
|
||||
|
||||
const tagsSections = ref<{ name: string; title: string }[]>([
|
||||
{ name: 'file', title: 'File Tags' },
|
||||
{ name: 'jfif', title: 'JFIF Tags' },
|
||||
{ name: 'pngFile', title: 'PNG File Tags' },
|
||||
{ name: 'pngText', title: 'PNG Text Tags' },
|
||||
{ name: 'png', title: 'PNG Tags' },
|
||||
{ name: 'exif', title: 'EXIF Tags' },
|
||||
{ name: 'iptc', title: 'IPTC Tags' },
|
||||
{ name: 'xmp', title: 'XMP Tags' },
|
||||
{ name: 'icc', title: 'ICC Tags' },
|
||||
{ name: 'riff', title: 'RIFF Tags' },
|
||||
{ name: 'gif', title: 'GIF Tags' },
|
||||
{ name: 'Thumbnail', title: 'Thumbnail Tags' },
|
||||
{ name: 'photoshop', title: 'Photoshop Tags' },
|
||||
]);
|
||||
|
||||
const errorMessage = ref<string>('');
|
||||
const tags = ref<ExifReader.ExpandedTags>({});
|
||||
const status = ref<'idle' | 'parsed' | 'error' | 'loading'>('idle');
|
||||
const file = ref<File | null>(null);
|
||||
const quality = ref<number>(-1);
|
||||
|
||||
const openStreetMapUrl = computed(
|
||||
() => {
|
||||
const gpsLatitude = tags.value.gps?.Latitude;
|
||||
const gpsLongitude = tags.value.gps?.Longitude;
|
||||
return gpsLatitude && gpsLongitude ? `https://www.openstreetmap.org/?mlat=${gpsLatitude}&mlon=${gpsLongitude}#map=18/${gpsLatitude}/${gpsLongitude}` : undefined;
|
||||
},
|
||||
);
|
||||
|
||||
async function onImageUploaded(uploadedFile: File) {
|
||||
file.value = uploadedFile;
|
||||
|
||||
const fileBuffer = await uploadedFile.arrayBuffer();
|
||||
status.value = 'loading';
|
||||
try {
|
||||
quality.value = getJpegQuality(new Uint8Array(fileBuffer));
|
||||
}
|
||||
catch (e) {
|
||||
quality.value = -1;
|
||||
}
|
||||
try {
|
||||
tags.value = await ExifReader.load(fileBuffer, { expanded: true });
|
||||
status.value = 'parsed';
|
||||
}
|
||||
catch (e: any) {
|
||||
errorMessage.value = e.toString();
|
||||
status.value = 'error';
|
||||
}
|
||||
}
|
||||
function getSection(sectionName: string): TagsSection | null {
|
||||
const sections = tags.value as { [name: string]: TagsSection };
|
||||
return sections[sectionName] ? sections[sectionName] : null;
|
||||
}
|
||||
const addSpacesToTagNames = (label: string) => label.replace(/([A-Z][a-z])/g, ' $1').trim();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="flex: 0 0 100%">
|
||||
<div mx-auto max-w-600px>
|
||||
<c-file-upload title="Drag and drop a Image file here, or click to select a file" @file-upload="onImageUploaded" />
|
||||
|
||||
<c-card v-if="file" mt-4 flex gap-2>
|
||||
<div font-bold>
|
||||
{{ file.name }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ formatBytes(file.size) }}
|
||||
</div>
|
||||
|
||||
<div v-if="tags.Thumbnail">
|
||||
<img :src="`data:image/jpeg;base64,${tags.Thumbnail.base64}`" max-w-200px>
|
||||
</div>
|
||||
</c-card>
|
||||
|
||||
<div v-if="status === 'error'">
|
||||
<c-alert mt-4>
|
||||
Error parsing image file: {{ errorMessage }}
|
||||
</c-alert>
|
||||
</div>
|
||||
|
||||
<c-card v-if="quality >= 0" title="JPEG Quality" mt-4>
|
||||
<input-copyable
|
||||
label="JPEG Quality (%)"
|
||||
label-position="left"
|
||||
label-width="150px"
|
||||
label-align="right"
|
||||
mb-2
|
||||
:value="quality"
|
||||
/>
|
||||
</c-card>
|
||||
|
||||
<c-card v-if="status === 'parsed' && openStreetMapUrl" title="GPS Infos" mt-4>
|
||||
<div flex gap-2>
|
||||
<c-label label="Latitude">
|
||||
{{ tags.gps?.Latitude?.toFixed(4) }}
|
||||
</c-label>
|
||||
<c-label label="Longitude">
|
||||
{{ tags.gps?.Longitude?.toFixed(4) }}
|
||||
</c-label>
|
||||
<c-label label="Altitude">
|
||||
{{ tags.gps?.Altitude?.toFixed(4) }}
|
||||
</c-label>
|
||||
</div>
|
||||
<c-button :href="openStreetMapUrl" target="_blank" mt-4>
|
||||
Localize on Open Street Map
|
||||
</c-button>
|
||||
</c-card>
|
||||
<c-card v-if="status === 'parsed' && !openStreetMapUrl" mt-4>
|
||||
No GPS Information
|
||||
</c-card>
|
||||
|
||||
<div v-if="status === 'parsed'">
|
||||
<div v-for="section in tagsSections" :key="section.name">
|
||||
<c-card v-if="getSection(section.name)" :title="section.title" mt-4>
|
||||
<input-copyable
|
||||
v-for="({ description }, tagName) in getSection(section.name)"
|
||||
:key="tagName"
|
||||
:label="addSpacesToTagNames(String(tagName))"
|
||||
label-position="left"
|
||||
label-width="150px"
|
||||
label-align="right"
|
||||
mb-2
|
||||
disabled="disabled"
|
||||
:value="description ?? '<binary>'"
|
||||
/>
|
||||
</c-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="status === 'parsed'" style="flex: 0 0 100%" mt-5 flex flex-col gap-4 />
|
||||
|
||||
<div font-size-3>
|
||||
Made with <a href="https://github.com/mattiasw/ExifReader" target="_blank">ExifReader</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
12
src/tools/image-exif-reader/index.ts
Normal file
12
src/tools/image-exif-reader/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { FileInfo } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'Image EXIF/Metadata/GPS/JPEG Quality reader',
|
||||
path: '/image-exif-reader',
|
||||
description: 'Read EXIF, IPTC, XMP, GPS and other metadata, JPEG Quality, and other infos from images files',
|
||||
keywords: ['image', 'exif', 'reader', 'iptc', 'gps', 'xmp', 'jpeg', 'quality'],
|
||||
component: () => import('./image-exif-reader.vue'),
|
||||
icon: FileInfo,
|
||||
createdAt: new Date('2024-01-09'),
|
||||
});
|
4
src/tools/image-exif-reader/jpeg-quality-estimator.d.ts
vendored
Normal file
4
src/tools/image-exif-reader/jpeg-quality-estimator.d.ts
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
declare module 'jpeg-quality-estimator' {
|
||||
const getJpegQuality: (file: Uint8Array) => number;
|
||||
export default getJpegQuality;
|
||||
}
|
|
@ -75,6 +75,7 @@ import { tool as urlParser } from './url-parser';
|
|||
import { tool as uuidGenerator } from './uuid-generator';
|
||||
import { tool as macAddressLookup } from './mac-address-lookup';
|
||||
import { tool as xmlFormatter } from './xml-formatter';
|
||||
import { tool as imageExifReader } from './image-exif-reader';
|
||||
|
||||
export const toolsByCategory: ToolCategory[] = [
|
||||
{
|
||||
|
@ -124,7 +125,13 @@ export const toolsByCategory: ToolCategory[] = [
|
|||
},
|
||||
{
|
||||
name: 'Images and videos',
|
||||
components: [qrCodeGenerator, wifiQrCodeGenerator, svgPlaceholderGenerator, cameraRecorder],
|
||||
components: [
|
||||
qrCodeGenerator,
|
||||
wifiQrCodeGenerator,
|
||||
svgPlaceholderGenerator,
|
||||
cameraRecorder,
|
||||
imageExifReader,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Development',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue