@rockshin/react-image-annotation
Version:
An image annotation tool for ai project that manual annotation for images, easy to use!
144 lines (143 loc) • 5.54 kB
JavaScript
import * as __WEBPACK_EXTERNAL_MODULE_tldraw__ from "tldraw";
const getImage = async (src)=>{
let blob;
if (src.startsWith('data:')) {
const base64Data = src.split(',')[1];
const mimeType = src.split(',')[0].split(':')[1].split(';')[0];
const byteString = atob(base64Data);
const arrayBuffer = new ArrayBuffer(byteString.length);
const uint8Array = new Uint8Array(arrayBuffer);
for(let i = 0; i < byteString.length; i++)uint8Array[i] = byteString.charCodeAt(i);
blob = new Blob([
arrayBuffer
], {
type: mimeType
});
} else blob = await fetch(src).then((res)=>res.blob());
const dataUrl = await __WEBPACK_EXTERNAL_MODULE_tldraw__.FileHelpers.blobToDataUrl(blob);
const { w: width, h: height } = await __WEBPACK_EXTERNAL_MODULE_tldraw__.MediaHelpers.getImageSize(blob);
return {
src: dataUrl,
width,
height,
type: blob.type
};
};
function makeSureShapeIsAtBottom(editor, shapeId) {
if (!editor) return;
const shape = editor.getShape(shapeId);
if (!shape) return;
const pageId = editor.getCurrentPageId();
if (shape.parentId !== pageId) editor.moveShapesToPage([
shape
], pageId);
const siblings = editor.getSortedChildIdsForParent(pageId);
const currentBottomShape = editor.getShape(siblings[0]);
if (currentBottomShape.id !== shapeId) editor.sendToBack([
shape
]);
}
const getRectangleAnnotations = (editor)=>{
if (!editor) return [];
const annotations = [];
const shapes = editor.getCurrentPageShapes();
shapes.forEach((shape)=>{
if ('geo' === shape.type && 'rectangle' === shape.props.geo) {
const originalBounds = {
x: shape.x,
y: shape.y,
width: shape.props.w,
height: shape.props.h
};
const annotation = {
id: shape.id,
x: originalBounds.x,
y: originalBounds.y,
width: originalBounds.width,
height: originalBounds.height,
rotation: shape.rotation || 0,
type: 'rectangle',
label: shape.props.text || '',
timestamp: Date.now(),
metadata: {
color: shape.props.color || 'default',
createdBy: 'user',
modifiedAt: Date.now(),
version: 1,
tags: [],
isVerified: false
}
};
annotations.push(annotation);
}
});
return annotations;
};
const IMAGE_LOAD_TIMEOUT = 30000;
const loadImageWithTimeout = async (src)=>{
try {
if (src.startsWith('data:image/')) {
const [header, content] = src.split(',');
if (!header.includes('base64') || !content) throw new Error('Invalid base64 image format');
} else new URL(src);
} catch (error) {
if (error instanceof Error && 'Invalid base64 image format' === error.message) throw error;
throw new Error('Invalid image URL');
}
const timeoutPromise = new Promise((_, reject)=>{
setTimeout(()=>reject(new Error('Image load timed out')), IMAGE_LOAD_TIMEOUT);
});
try {
const result = await Promise.race([
getImage(src),
timeoutPromise
]);
return result;
} catch (error) {
if (error instanceof Error) {
if ('Image load timed out' === error.message) throw new Error(`Failed to load image within ${IMAGE_LOAD_TIMEOUT / 1000} seconds`);
if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) throw new Error('Network error: Unable to load image');
if (error.message.includes('base64')) throw new Error('Invalid base64 image data');
}
throw new Error('Failed to load image');
}
};
const loadImageForIndex = async ({ imageIndex, images, setIsLoading, setImageLoadError, setImage, setUsedNumbers, setDeletedNumbers, onImageLoadError })=>{
try {
setIsLoading(true);
setImageLoadError(null);
const base64Image = await loadImageWithTimeout(images[imageIndex].src);
setImage({
...base64Image,
id: images[imageIndex].id || (0, __WEBPACK_EXTERNAL_MODULE_tldraw__.uniqueId)()
});
const newUsedNumbers = new Set();
images[imageIndex].annotations.forEach((annotation)=>{
if (annotation.label) {
const num = parseInt(annotation.label);
if (!isNaN(num)) newUsedNumbers.add(num);
}
});
setUsedNumbers(newUsedNumbers);
setDeletedNumbers([]);
} catch (error) {
const err = error instanceof Error ? error : new Error('Failed to load image');
console.error('Failed to load image:', err);
setImageLoadError(err);
onImageLoadError?.(err, {
id: images[imageIndex].id || '',
src: images[imageIndex].src,
index: imageIndex
});
} finally{
setIsLoading(false);
}
};
const cleanUpEditor = (editor)=>{
editor?.run(()=>{
editor?.deleteShapes(Array.from(editor.getCurrentPageShapeIds()));
}, {
ignoreShapeLock: true
});
};
export { cleanUpEditor, getImage, getRectangleAnnotations, loadImageForIndex, loadImageWithTimeout, makeSureShapeIsAtBottom };