Skip to content

Commit

Permalink
Merge pull request #226 from sunnydanu/feat(new-tool)--image-exif-reader
Browse files Browse the repository at this point in the history
feat(new-tool):-image-exif-reader
  • Loading branch information
sunnydanu authored Nov 3, 2024
2 parents c3a44c1 + 1f3644b commit 035e2c3
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 1 deletion.
1 change: 1 addition & 0 deletions components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ declare module '@vue/runtime-core' {
IconMdiTranslate: typeof import('~icons/mdi/translate')['default']
IconMdiTriangleDown: typeof import('~icons/mdi/triangle-down')['default']
IconMdiVideo: typeof import('~icons/mdi/video')['default']
ImageExifReader: typeof import('./src/tools/image-exif-reader/image-exif-reader.vue')['default']
ImageResizer: typeof import('./src/tools/image-resizer/image-resizer.vue')['default']
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default']
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"emojilib": "^3.0.10",
"fflate": "^0.8.2",
"figlet": "^1.7.0",
"exifreader": "^4.20.0",
"figue": "^1.2.0",
"fuse.js": "^6.6.2",
"hash-wasm": "^4.9.0",
Expand All @@ -78,6 +79,7 @@
"ibantools": "^4.3.3",
"js-base64": "^3.7.7",
"json-editor-vue": "^0.17.2",
"jpeg-quality-estimator": "^1.0.1",
"json5": "^2.2.3",
"jszip": "^3.10.1",
"jwt-decode": "^3.1.2",
Expand Down
25 changes: 25 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

155 changes: 155 additions & 0 deletions src/tools/image-exif-reader/image-exif-reader.vue
Original file line number Diff line number Diff line change
@@ -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 changes: 12 additions & 0 deletions src/tools/image-exif-reader/index.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 4 additions & 0 deletions src/tools/image-exif-reader/jpeg-quality-estimator.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare module 'jpeg-quality-estimator' {
const getJpegQuality: (file: Uint8Array) => number;
export default getJpegQuality;
}
9 changes: 8 additions & 1 deletion src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ import { tool as macAddressLookup } from './mac-address-lookup';
import { tool as xmlFormatter } from './xml-formatter';
import { tool as dockerComposeToDockerRunConverter } from './docker-compose-to-docker-run-converter';
import { tool as dockerComposeConverter } from './docker-compose-converter';
import { tool as imageExifReader } from './image-exif-reader';
import { tool as yamlViewer } from './yaml-viewer';

export const toolsByCategory: ToolCategory[] = [
Expand Down Expand Up @@ -172,7 +173,13 @@ export const toolsByCategory: ToolCategory[] = [
},
{
name: 'Images and videos',
components: [qrCodeGenerator, wifiQrCodeGenerator, svgPlaceholderGenerator, cameraRecorder, imageResizer, ocrImage],
components: [
qrCodeGenerator,
wifiQrCodeGenerator,
svgPlaceholderGenerator,
cameraRecorder, imageResizer, ocrImage,
imageExifReader,
],
},
{
name: 'Development',
Expand Down

0 comments on commit 035e2c3

Please sign in to comment.