Skip to content

Commit 4ea3d21

Browse files
committed
feat: Add new tool imgMetadataViewer
1 parent 1532e8b commit 4ea3d21

6 files changed

Lines changed: 235 additions & 35 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"blurhash": "^2.0.5",
2323
"clsx": "^2.1.1",
2424
"crypto-js": "^4.2.0",
25+
"exifr": "^7.1.3",
2526
"github-markdown-css": "^5.5.1",
2627
"grapesjs": "^0.19.5",
2728
"grapesjs-blocks-basic": "^1.0.2",

pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/App.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const AutoprefixerTool = lazy(() => import("./pages/AutoPrefixer.jsx"));
2626
import StringConverter from './pages/StringConverter.jsx';
2727
const QrCodeGenerator = lazy(() => import('./pages/QRCodeGenrator.jsx'));
2828
const HashGenerator = lazy(() => import('./pages/HashGenerator.jsx'));
29+
const ImageMetadataViewer = lazy(() => import('./pages/ImageMetadataViewer.jsx'));
2930
import Websites from "./pages/Websites.jsx";
3031
// import Test from "./pages/testing/Test.jsx" // Testing purpose
3132

@@ -49,6 +50,7 @@ const routes = [
4950
{ path: "StringConverter", element: <StringConverter /> },
5051
{ path: "QrCodeGenerator", element: <QrCodeGenerator />, isLazy: true },
5152
{ path: "HashGenerator", element: <HashGenerator />, isLazy: true },
53+
{ path: "image-metadata-viewer", element: <ImageMetadataViewer />, isLazy: true },
5254
{ path: "Websites", element: <Websites /> },
5355
// { path: "Test", element: <Test /> },
5456
{ path: "*", element: <NoPage /> },

src/Layout.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ const links = [
1919
{ to: "/StringConverter", text: "StringConverter" },
2020
{ to: "/QrCodeGenerator", text: "QRCodeGenerator" },
2121
{ to: "/HashGenerator", text: "HashGenerator" },
22+
{ to: "/image-metadata-viewer", text: "ImgMetadataViewer" },
2223
{ to: "/Websites", text: "Websites" },
23-
// { to: "/Test", text: "BlurhashGenerator[Test]" },
24+
// { to: "/Test", text: "Test" },
2425
];
2526

2627

src/pages/ImageMetadataViewer.jsx

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { useRef, useReducer } from 'react';
2+
import PropTypes from 'prop-types';
3+
import exifr from 'exifr';
4+
import clsx from 'clsx';
5+
import { Prism as JsonSyntaxHighlighter } from 'react-syntax-highlighter';
6+
import { a11yDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
7+
8+
import ToolBoxLayout from '@/common/ToolBoxLayout';
9+
import ToolBox from '@/common/ToolBox';
10+
import Btn from '@/common/BasicBtn';
11+
12+
const actionTypes = {
13+
UPDATE_VALUE: 'UPDATE_VALUE',
14+
};
15+
16+
const initialState = {
17+
imageUrl: '',
18+
metadata: '',
19+
error: '',
20+
};
21+
22+
const reducer = (state, action) => {
23+
switch (action.type) {
24+
case actionTypes.UPDATE_VALUE: {
25+
return { ...state, [action.field]: action.value };
26+
}
27+
default: {
28+
console.error('Unknown action: ' + action.type);
29+
console.warn('you not added action.type: ' + action.type + ' add and try');
30+
return state;
31+
}
32+
}
33+
};
34+
35+
const ImageMetadataViewer = () => {
36+
const imgInputRef = useRef('');
37+
const [state, dispatch] = useReducer(reducer, initialState);
38+
const {
39+
imageUrl,
40+
metadata,
41+
error,
42+
} = state;
43+
44+
45+
// // Functions for Updating Reducer // //
46+
47+
const updateValues = (values) => {
48+
Object.keys(values).forEach((field) => {
49+
dispatch({ type: actionTypes.UPDATE_VALUE, field, value: values[field] });
50+
});
51+
};
52+
53+
54+
// // Event Handlers // //
55+
56+
const imgInputClick = () => {
57+
if (imgInputRef.current) {
58+
imgInputRef.current.click();
59+
}
60+
};
61+
62+
const handleImageChange = async (event) => {
63+
const file = event.target.files[0];
64+
if (file) {
65+
try {
66+
const imageUrl = URL.createObjectURL(file);
67+
const supportedFormats = ['image/jpeg', 'image/tiff', 'image/webp', 'image/heic', 'image/heif'];
68+
if (!supportedFormats.includes(file.type)) {
69+
throw new Error('Unsupported file format');
70+
}
71+
72+
const exifData = await exifr.parse(file);
73+
74+
if (exifData) {
75+
console.log('EXIF data extracted:', exifData);
76+
updateValues({
77+
metadata: exifData,
78+
error: '',
79+
imageUrl
80+
});
81+
} else {
82+
throw new Error('No EXIF data found in the image');
83+
}
84+
} catch (error) {
85+
console.error('Error extracting metadata:', error.message);
86+
updateValues({
87+
metadata: '',
88+
error: error.message || 'Failed to extract metadata',
89+
imageUrl: '',
90+
});
91+
}
92+
} else {
93+
console.error('No file selected');
94+
updateValues({
95+
metadata: '',
96+
error: 'No file selected',
97+
imageUrl: '',
98+
});
99+
}
100+
};
101+
102+
const tailwind = {
103+
input: {
104+
selectImgBtn: 'mx-auto block !w-44',
105+
imgPreview: 'border-2 border-gray-500 rounded-[10px] h-[91%] flex content-center justify-center flex-col mx-0 my-2.5 p-[5px]',
106+
imgPreviewImg: 'object-contain max-h-full max-w-full',
107+
},
108+
output: "border border-gray-400 rounded-lg h-[96%] overflow-y-auto",
109+
};
110+
111+
return (
112+
<ToolBoxLayout height=''>
113+
<ToolBox title='Input'>
114+
<input
115+
type="file"
116+
ref={imgInputRef}
117+
// accept=".jpeg, .jpg, .tiff, .webp, .heic, .heif"
118+
accept="image/*"
119+
onChange={handleImageChange}
120+
hidden
121+
/>
122+
<Btn
123+
btnText='Select Image'
124+
onClick={imgInputClick}
125+
classNames={tailwind.input.selectImgBtn}
126+
/>
127+
<div className={tailwind.input.imgPreview}>
128+
{imageUrl === '' ? (
129+
<Info text="Image Preview" />
130+
) : (
131+
<img
132+
src={imageUrl}
133+
className={tailwind.input.imgPreviewImg}
134+
/>
135+
)}
136+
</div>
137+
</ToolBox>
138+
139+
<ToolBox title='Output'>
140+
<div className={tailwind.output}>
141+
{error ? (
142+
<Info text={error} colorRed />
143+
) : metadata === "" ? (
144+
<Info text="Select Image" />
145+
) : (
146+
<JsonSyntaxHighlighter
147+
style={a11yDark}
148+
language='json'
149+
>
150+
{JSON.stringify(metadata, '', 2)}
151+
</JsonSyntaxHighlighter>
152+
)}
153+
</div>
154+
</ToolBox>
155+
</ToolBoxLayout>
156+
);
157+
};
158+
159+
const Info = ({ text, colorRed }) => (
160+
<div className={
161+
clsx(
162+
"flex items-center justify-center h-full text-center text-xl",
163+
colorRed && "text-red-500",
164+
)}
165+
>
166+
<p>{text}</p>
167+
</div>
168+
);
169+
170+
Info.propType = {
171+
text: PropTypes.string.isRequired,
172+
colorRed: PropTypes.bool,
173+
};
174+
175+
export default ImageMetadataViewer;

src/styles/output.css

Lines changed: 47 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -862,6 +862,14 @@ video {
862862
margin-right: auto;
863863
}
864864

865+
.mt-1 {
866+
margin-top: 0.25rem;
867+
}
868+
869+
.mt-2 {
870+
margin-top: 0.5rem;
871+
}
872+
865873
.mt-3 {
866874
margin-top: 0.75rem;
867875
}
@@ -878,18 +886,6 @@ video {
878886
margin-top: 1.25rem;
879887
}
880888

881-
.mt-\[5px\] {
882-
margin-top: 5px;
883-
}
884-
885-
.mt-1 {
886-
margin-top: 0.25rem;
887-
}
888-
889-
.mt-2 {
890-
margin-top: 0.5rem;
891-
}
892-
893889
.mt-6 {
894890
margin-top: 1.5rem;
895891
}
@@ -898,6 +894,10 @@ video {
898894
margin-top: 1.75rem;
899895
}
900896

897+
.mt-\[5px\] {
898+
margin-top: 5px;
899+
}
900+
901901
.box-border {
902902
box-sizing: border-box;
903903
}
@@ -942,10 +942,30 @@ video {
942942
display: none;
943943
}
944944

945+
.\!h-10 {
946+
height: 2.5rem !important;
947+
}
948+
949+
.\!h-12 {
950+
height: 3rem !important;
951+
}
952+
953+
.\!h-16 {
954+
height: 4rem !important;
955+
}
956+
957+
.\!h-4 {
958+
height: 1rem !important;
959+
}
960+
945961
.\!h-\[45px\] {
946962
height: 45px !important;
947963
}
948964

965+
.\!h-\[88\%\] {
966+
height: 88% !important;
967+
}
968+
949969
.h-0 {
950970
height: 0px;
951971
}
@@ -1042,6 +1062,10 @@ video {
10421062
height: 745px;
10431063
}
10441064

1065+
.h-\[80\%\] {
1066+
height: 80%;
1067+
}
1068+
10451069
.h-\[var\(--w\)\] {
10461070
height: var(--w);
10471071
}
@@ -1054,24 +1078,16 @@ video {
10541078
height: 100vh;
10551079
}
10561080

1057-
.\!h-\[88\%\] {
1058-
height: 88% !important;
1081+
.h-\[91\%\] {
1082+
height: 91%;
10591083
}
10601084

1061-
.\!h-4 {
1062-
height: 1rem !important;
1085+
.h-96 {
1086+
height: 24rem;
10631087
}
10641088

1065-
.\!h-10 {
1066-
height: 2.5rem !important;
1067-
}
1068-
1069-
.\!h-12 {
1070-
height: 3rem !important;
1071-
}
1072-
1073-
.\!h-16 {
1074-
height: 4rem !important;
1089+
.h-\[96\%\] {
1090+
height: 96%;
10751091
}
10761092

10771093
.max-h-full {
@@ -1222,10 +1238,6 @@ video {
12221238
width: 81%;
12231239
}
12241240

1225-
.w-\[88\%\] {
1226-
width: 88%;
1227-
}
1228-
12291241
.w-fit {
12301242
width: -moz-fit-content;
12311243
width: fit-content;
@@ -1235,10 +1247,6 @@ video {
12351247
width: 100%;
12361248
}
12371249

1238-
.\!w-\[88\%\] {
1239-
width: 88% !important;
1240-
}
1241-
12421250
.min-w-\[1600px\] {
12431251
min-width: 1600px;
12441252
}
@@ -2154,6 +2162,11 @@ video {
21542162
color: rgb(255 255 255 / var(--tw-text-opacity));
21552163
}
21562164

2165+
.text-red-500 {
2166+
--tw-text-opacity: 1;
2167+
color: rgb(239 68 68 / var(--tw-text-opacity));
2168+
}
2169+
21572170
.underline {
21582171
text-decoration-line: underline;
21592172
}

0 commit comments

Comments
 (0)