feat: added exif cleanup

This commit is contained in:
florian 2024-03-29 13:54:12 +01:00
parent 2ec8dbdc69
commit 885d8d1ec2
5 changed files with 89 additions and 41 deletions

BIN
bun.lockb Executable file

Binary file not shown.

View File

@ -12,7 +12,7 @@
.blob-list .blob {
@apply p-1 hover:bg-neutral-700 rounded-md grid pr-4;
grid-template-columns: 2em auto 6em 10em 7em 1em;
grid-template-columns: 2em auto 6em 10em 7em 3em;
}
.blob-list .blob span {

View File

@ -1,4 +1,4 @@
import { DocumentIcon, ListBulletIcon, PhotoIcon, TrashIcon } from '@heroicons/react/24/outline';
import { ClipboardDocumentIcon, DocumentIcon, ListBulletIcon, PhotoIcon, TrashIcon } from '@heroicons/react/24/outline';
import { BlobDescriptor } from 'blossom-client-sdk';
import { formatDate, formatFileSize } from '../../utils';
import './BlobList.css';
@ -75,49 +75,28 @@ const BlobList = ({ blobs, onDelete, title }: BlobListProps) => {
<span>{formatFileSize(blob.size)}</span>
<span>{blob.type && `${blob.type}`}</span>
<span>{formatDate(blob.created)}</span>
{onDelete && (
<span>
<a onClick={() => onDelete(blob)}>
<TrashIcon />
<div>
<span>
<a onClick={() => {navigator.clipboard.writeText(blob.url)}}>
<ClipboardDocumentIcon />
</a>
</span>
)}
{onDelete && (
<span>
<a onClick={() => onDelete(blob)}>
<TrashIcon />
</a>
</span> )}
</div>
</div>
))}
</div>
)}
</>
);
/*
)}
} else {
return (
<div className="blob-list">
{blobs.map((blob: BlobDescriptor) => (
<div className="blob" key={blob.sha256}>
<span>
<DocumentIcon />
</span>
<span>
<a href={blob.url} target="_blank">
{blob.sha256}
</a>
</span>
<span>{formatFileSize(blob.size)}</span>
<span>{blob.type && `${blob.type}`}</span>
<span>{formatDate(blob.created)}</span>
{onDelete && (
<span>
<a onClick={() => onDelete(blob)}>
<TrashIcon />
</a>
</span>
)}
</div>
))}
</div>
);
}*/
};
export default BlobList;

View File

@ -6,6 +6,7 @@ import { useServerInfo } from '../utils/useServerInfo';
import { useQueryClient } from '@tanstack/react-query';
import { ArrowUpOnSquareIcon } from '@heroicons/react/24/outline';
import ProgressBar from '../components/ProgressBar/ProgressBar';
import { removeExifData } from '../exif';
type TransferStats = {
enabled: boolean;
@ -20,11 +21,21 @@ function Upload() {
const queryClient = useQueryClient();
const [transfers, setTransfers] = useState<{ [key: string]: TransferStats }>({});
const [files, setFiles] = useState<File[]>([]);
const [cleanPrivateData, setCleanPrivateData] = useState(true);
const upload = async () => {
if (files && files.length) {
const filesToUpload: File[] = [];
for (const f of files) {
if (cleanPrivateData) {
filesToUpload.push(await removeExifData(f));
} else {
filesToUpload.push(f);
}
}
if (filesToUpload && filesToUpload.length) {
// sum files sizes
const totalSize = files.reduce((acc, f) => acc + f.size, 0);
const totalSize = filesToUpload.reduce((acc, f) => acc + f.size, 0);
// set all entries size to totalSize
setTransfers(ut => {
@ -42,7 +53,7 @@ function Upload() {
continue;
}
const serverUrl = serverInfo[server.name].url;
for (const file of files) {
for (const file of filesToUpload) {
const uploadAuth = await BlossomClient.getUploadAuth(file, signEventTemplate, 'Upload Blob');
const newBlob = await BlossomClient.uploadBlob(serverUrl, file, uploadAuth);
@ -94,7 +105,7 @@ function Upload() {
<ArrowUpOnSquareIcon className="w-8 inline" /> Browse or drag & drop
</label>
<div className="cursor-pointer grid gap-2" style={{ gridTemplateColumns: '1em 20em auto' }}>
<div className="cursor-pointer grid gap-2" style={{ gridTemplateColumns: '1.5em 20em auto' }}>
{servers.map(s => (
<>
<input
@ -118,6 +129,18 @@ function Upload() {
))}
</div>
<div className="cursor-pointer grid gap-2" style={{ gridTemplateColumns: '1.5em 20em' }}>
<input
className="w-5 accent-pink-700 hover:accent-pink-600"
id="cleanData"
type="checkbox"
checked={cleanPrivateData}
onChange={e => setCleanPrivateData(e.currentTarget.checked)}
/>
<label htmlFor="cleanData" className="cursor-pointer">
Clean private data in images (EXIF)
</label>
</div>
<button
className="p-2 px-4 bg-neutral-600 hover:bg-pink-700 text-white rounded-lg w-2/6"
onClick={() => upload()}

46
src/utils/exif.ts Normal file
View File

@ -0,0 +1,46 @@
/* from: https://stackoverflow.com/a/77472484/47324 */
const cleanBuffer = (arrayBuffer: ArrayBuffer) => {
let dataView = new DataView(arrayBuffer);
const exifMarker = 0xffe1;
let offset = 2; // Skip the first two bytes (0xFFD8)
while (offset < dataView.byteLength) {
if (dataView.getUint16(offset) === exifMarker) {
// Found an EXIF marker
const segmentLength = dataView.getUint16(offset + 2, false) + 2;
// Update the arrayBuffer and dataView
arrayBuffer = removeSegment(arrayBuffer, offset, segmentLength);
dataView = new DataView(arrayBuffer);
} else {
// Move to the next marker
offset += 2 + dataView.getUint16(offset + 2, false);
}
}
return arrayBuffer;
};
const removeSegment = (buffer: ArrayBuffer, offset: number, length: number) => {
// Create a new buffer without the specified segment
const modifiedBuffer = new Uint8Array(buffer.byteLength - length);
modifiedBuffer.set(new Uint8Array(buffer.slice(0, offset)), 0);
modifiedBuffer.set(new Uint8Array(buffer.slice(offset + length)), offset);
return modifiedBuffer.buffer;
};
export const removeExifData = (file: File): Promise<File> => {
return new Promise(resolve => {
if (file && file.type.startsWith('image/')) {
const fr = new FileReader();
fr.onload = function (this: FileReader) {
const cleanedBuffer = cleanBuffer(this.result as ArrayBuffer);
const blob = new Blob([cleanedBuffer], { type: file.type });
const newFile = new File([blob], file.name, { type: file.type });
resolve(newFile);
};
fr.readAsArrayBuffer(file);
} else resolve(file);
});
};