image-annotation-usage

Use when integrating @frank17008/image-annotation React component into web applications, including basic setup, controlled/uncontrolled mode, custom image upload, styling, or debugging annotation rendering issues, canvas blank problems, or annotations not updating.

Audits

Malicious

Install

openclaw skills install image-annotation-usage

Image Annotation Component Usage

Overview

@frank17008/image-annotation is a React + TypeScript image annotation component with Canvas 2D. Supports rectangle, circle, arrow, text, and freehand tools with undo/redo, drag, export, keyboard shortcuts, zoom/pan, and DPR-aware rendering.

Package location: packages/image-annotation/src/ Published path: @frank17008/image-annotation

When to Use

Trigger phrases:

  • "How to integrate image annotation component"
  • "Add annotation to my React app"
  • "Canvas blank even though image loads"
  • "Annotations not saving"
  • "Custom image upload with annotation"
  • "Controlled mode not working"
  • "HiDPI/Retina display blurry"

Installation

pnpm add @frank17008/image-annotation
# or
npm install @frank17008/image-annotation

Basic Integration

Minimal Setup

import { useState } from 'react';
import { ImageAnnotation, type Annotation } from '@frank17008/image-annotation';
import '@frank17008/image-annotation/dist/index.css';

export default function App() {
  const [annotations, setAnnotations] = useState<Annotation[]>([]);

  return (
    <div style={{ height: 600 }}>
      <ImageAnnotation
        src="https://example.com/image.jpg"
        onChange={setAnnotations}
      />
    </div>
  );
}

Critical: Parent container MUST have explicit height. Without it, canvas has 0×0 dimensions → blank canvas.

Type Import

Always use explicit type import for TypeScript:

import type { Annotation, ToolType } from '@frank17008/image-annotation';
// or
import { type Annotation, type ToolType } from '@frank17008/image-annotation';

Controlled vs Uncontrolled Mode

Uncontrolled (Default)

<ImageAnnotation
  src="image.jpg"
  onChange={(annotations) => console.log(annotations)}
/>

The component manages its own state internally.

Controlled (External State)

const [myAnnotations, setMyAnnotations] = useState<Annotation[]>([]);

<ImageAnnotation
  src="image.jpg"
  value={myAnnotations}
  onChange={setMyAnnotations}
/>

Rules:

  1. value → external annotations (read-only for component)
  2. onChange → called when annotations change
  3. If both value and onChange provided → controlled mode
  4. If neither → uncontrolled mode
  5. If only value without onChange → React warning

Sync Controlled to Uncontrolled

<ImageAnnotation
  src={src}
  value={annotations}
  onChange={(newAnnotations) => {
    setAnnotations(newAnnotations);
    // Save to backend, localStorage, etc.
    saveToBackend(newAnnotations);
  }}
/>

Custom Image Upload

Pattern 1: External Button

import { useState, useRef } from 'react';
import { ImageAnnotation } from '@frank17008/image-annotation';

export function ImageUploader() {
  const [imageSrc, setImageSrc] = useState<string | null>(null);
  const fileInputRef = useRef<HTMLInputElement>(null);

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = (event) => {
      const result = event.target?.result;
      if (typeof result === 'string') {
        setImageSrc(result); // base64 string
      }
    };
    reader.readAsDataURL(file);
    e.target.value = ''; // reset for same file re-select
  };

  return (
    <div style={{ height: 600 }}>
      <input
        ref={fileInputRef}
        type="file"
        accept="image/*"
        onChange={handleFileChange}
        style={{ display: 'none' }}
      />
      <button onClick={() => fileInputRef.current?.click()}>
        Upload Image
      </button>
      {imageSrc && (
        <ImageAnnotation
          src={imageSrc}
          onChange={() => {}}
        />
      )}
    </div>
  );
}

Pattern 2: Component's Upload Button

const handleUpload = () => fileInputRef.current?.click();

<ImageAnnotation
  src={imageSrc}
  onChange={setAnnotations}
  onUpload={handleUpload}
/>

The component displays an upload button in toolbar when onUpload is provided.

Annotation Types Reference

// Rectangle annotation
{ type: 'rectangle', x: number, y: number, width: number, height: number, color: string, lineWidth?: number }

// Circle annotation
{ type: 'circle', x: number, y: number, radius: number, color: string, lineWidth?: number }

// Arrow annotation
{ type: 'arrow', x: number, y: number, toX: number, toY: number, color: string, lineWidth?: number }

// Text annotation
{ type: 'text', x: number, y: number, text: string, color: string, lineWidth?: number }

// Freehand annotation
{ type: 'freehand', points: Array<{ x: number, y: number }>, color: string, lineWidth?: number }

Type Export

import type {
  Annotation,
  RectangleAnnotation,
  CircleAnnotation,
  ArrowAnnotation,
  TextAnnotation,
  FreehandAnnotation,
  ToolType,
  Point,
} from '@frank17008/image-annotation';

Props API

PropTypeRequiredDescription
srcstringImage URL or base64
valueAnnotation[]Controlled annotations
onChange(annotations: Annotation[]) => voidAnnotation change callback
classNamestringContainer className
onUpload() => voidUpload button callback

Zoom and Pan

Programmatic Zoom

const handleZoomIn = () => {
  // Component doesn't expose zoom API directly
  // Use keyboard shortcuts: Ctrl++
};

// Or via keyboard shortcuts (user must press):
// - Ctrl++ : Zoom in
// - Ctrl+- : Zoom out
// - Ctrl+0 : Reset zoom

Space + Drag Pan

Hold Space + drag to pan the canvas.

Zoom Ratio Display

The toolbar shows current zoom percentage (e.g., "100%", "150%").

Exporting Annotations

On Change Callback

<ImageAnnotation
  src={src}
  onChange={(annotations) => {
    // Save to database
    await saveAnnotations(imageId, annotations);

    // Or export as JSON
    const json = JSON.stringify(annotations, null, 2);
    download(json, 'annotations.json', 'application/json');
  }}
/>

Export Image with Annotations

Use the toolbar's export button, or programmatically:

import { download } from '@frank17008/image-annotation';
// Not currently exported - use toolbar button

Keyboard Shortcuts

ShortcutAction
DeleteDelete selected annotation
EscapeCancel text input
Ctrl+ZUndo
Ctrl+Y / Ctrl+Shift+ZRedo
Ctrl++ / Ctrl+=Zoom in
Ctrl+-Zoom out
Ctrl+0Reset zoom
Space + DragPan canvas

Styling Customization

CSS Variables

:root {
  --annotation-primary: #FF0000;
  --annotation-toolbar-bg: #FFFFFF;
}

Container Styling

<ImageAnnotation
  src={src}
  className="my-annotation-container"
/>
.my-annotation-container {
  border: 1px solid #ddd;
  border-radius: 8px;
}

Debugging Common Issues

Issue 1: Canvas Completely Blank

Symptoms:

  • Image loads successfully (network request completes)
  • Canvas area is empty/white

Root Causes and Solutions:

CauseDiagnosisFix
Parent has no heightContainer has height: auto or no height setSet explicit height: style={{ height: 600 }}
DPR mismatchRetina/HiDPI display, canvas looks tinyComponent handles DPR automatically
Image load errorCheck browser console for CORS errorsAdd crossOrigin="anonymous" to server
Zero canvas sizeCheck canvas element dimensionsEnsure parent has explicit dimensions

Issue 2: Annotations Not Saving

Symptoms:

  • Draw annotations but array remains empty
  • onChange callback never fires

Root Causes and Solutions:

CauseDiagnosisFix
Stale stateUsing prev => prev incorrectlyPass new array directly: setAnnotations(newValue)
Missing onChangeNo callback providedAdd onChange={setAnnotations}
Controlled conflictBoth value and internal stateProvide onChange with value

Correct pattern:

// WRONG - stale closure
onChange={(annotations) => {
  setAnnotations((prev) => [...prev, ...annotations]);
}}

// CORRECT - direct assignment
onChange={setAnnotations}

Issue 3: Blurry on HiDPI/Retina Displays

Diagnosis:

  • Canvas appears blurry or pixelated
  • Only happens on Retina displays

Root Cause: Missing devicePixelRatio scaling

Fix: The component handles DPR automatically since v1.x. Ensure:

  1. CSS import is included
  2. Parent container has proper dimensions

Issue 4: High-DPI Canvas Blank

Symptoms:

  • Works on standard displays
  • Blank on Retina/HiDPI

Root Cause: Canvas dimensions not scaled by devicePixelRatio

Diagnosis:

// In browser console
const canvas = document.querySelector('canvas');
console.log('Canvas size:', canvas.width, canvas.height);
console.log('Display size:', canvas.style.width, canvas.style.height);
console.log('DPR:', window.devicePixelRatio);

If canvas.width === canvas.style.width (not multiplied by DPR), this is the bug.

Issue 5: Text Annotation Not Updating

Symptoms:

  • Text tool selected
  • Click to add text
  • Text doesn't appear or doesn't save

Solution: Press Enter or click outside to commit text input.

Issue 6: Undo/Redo Not Working

Symptoms:

  • Press Ctrl+Z but nothing happens

Root Cause: No history to undo (first annotation can't be undone)

Issue 7: SSR/Next.js Issues

Symptoms:

  • "window is not defined"
  • Canvas only renders client-side

Solution:

'use client';
import dynamic from 'next/dynamic';

const ImageAnnotation = dynamic(
  () => import('@frank17008/image-annotation').then((mod) => mod.default),
  { ssr: false }
);

Build Commands

pnpm build:lib      # Build package to dist/
pnpm dev:lib       # Watch mode for library
pnpm start         # Run demo app
pnpm build:all    # Build everything

Dependencies

  • React 18+
  • TypeScript 4.5+
  • Canvas 2D API

Common Mistakes

❌ Forgetting CSS Import

// MISSING - component won't render correctly
import { ImageAnnotation } from '@frank17008/image-annotation';

// REQUIRED
import '@frank17008/image-annotation/dist/index.css';

❌ Parent Without Height

// WRONG - canvas has 0x0
<div><ImageAnnotation src="..." /></div>

// CORRECT - explicit height
<div style={{ height: 600 }}><ImageAnnotation src="..." /></div>

❌ Wrong Type Import

// WRONG
import { Annotation } from '@frank17008/image-annotation';

// CORRECT
import type { Annotation } from '@frank17008/image-annotation';

❌ Not Linking onChange with value

// WRONG - controlled without onChange (React warning)
<ImageAnnotation src="..." value={annotations} />

// CORRECT - controlled mode
<ImageAnnotation
  src="..."
  value={annotations}
  onChange={setAnnotations}
/>

Performance Tips

Large Images

For images > 5MB:

  1. Resize before passing to component
  2. Use appropriate container dimensions
  3. Consider lazy loading

Many Annotations (>100)

The component redraws on each change. For performance:

  1. Reduce lineWidth
  2. Simplify freehand paths (reduce point count)