Annotation Dialog
The annotation dialog lets users place interactive pins on any image inside the Uhuu Editor — without touching the source image. It opens a full-screen annotation editor where users can add markers, text labels, and callout cards at precise positions.
Why SVG Overlay Instead of Merging Into the Image
Most annotation tools burn annotations directly into a rasterized copy of the image. Uhuu takes a different approach: annotations are rendered as an SVG layer on top of the base image, and saved as structured data.
This matters for three reasons:
Non-destructive editing. Because annotations are a separate data record, not pixels baked into the image, users can always reposition, relabel, recolor, or delete individual pins. Changing the underlying photo does not erase the annotation layout — the pins simply reappear on the new image.
Crisp print output. Uhuu templates produce PDFs. Rasterized annotations degrade at high DPI because they were created at screen resolution. SVG annotations are mathematical shapes — circles, lines, text — that the PDF renderer reproduces at full vector quality at any print size, with no blurring or pixelation.
Annotations are data. The saved payload is a structured JSON record (schema: "uhuu.annotation.v1"), not an image blob. This means annotations can be read, transformed, re-rendered, or exported as a standalone SVG overlay by the template — for instance to composite the base photo and the annotation layer separately during PDF generation.
Annotation Types
The editor has two annotation tools:
Callout
A rich card connected to an anchor point by a line. The most expressive type.
- Modes:
minimal(compact badge) ordetailed(full card with title, description, optional icon/image, and stat rows) - Connector: a line from the card to the anchor point; can be toggled per callout
- Visual: attach an icon from the gallery to the card
Badge
A floating text label attached to a point.
- Variants:
pill,rounded,rectangle - Sizes:
sm,md,lg - Modes:
filloroutline
Parameters
$uhuu.editDialog({
path: 'page.heroAnnotation',
type: 'annotation',
image: 'https://example.com/photo-1200.jpg',
config: {
visualGallery: [
'https://cdn.example.com/icons/hospital.svg',
'https://cdn.example.com/icons/school.svg',
]
},
value: payload.page?.heroAnnotation
})| Parameter | Type | Description |
|---|---|---|
type | string | 'annotation' |
path | string | Payload path where annotation data is saved |
image | string | URL of the background image shown in the editor. Use a reasonably sized version (e.g. 1200 × 1200px) — not a full-resolution original |
config.visualGallery | string[] | Optional array of SVG icon URLs shown as selectable tiles in the visual picker. Defaults to a set of Phosphor icons. Pass an empty array to disable |
value | object | Current annotation data. If omitted the editor starts empty |
Saved Data Format
{
"schema": "uhuu.annotation.v1",
"annotations": [
{
"id": "abc123",
"type": "callout",
"anchorX": 42.5,
"anchorY": 31.0,
"cardX": 55.0,
"cardY": 20.0,
"mode": "detailed",
"variant": "rounded",
"color": "#3b82f6",
"scale": 1,
"content": {
"title": "Main entrance",
"description": "Access from north side",
"visual": "https://cdn.example.com/icons/building.svg"
},
"connector": { "enabled": true }
},
{
"id": "def456",
"type": "label",
"x": 68.0,
"y": 55.5,
"text": "Parking",
"variant": "pill",
"size": "sm",
"mode": "fill",
"color": "#0f172a"
}
],
"annotationSvg": "<svg>…</svg>"
}All positions (x, y, anchorX/Y, cardX/Y) are percentages (0–100) relative to the image dimensions. annotationSvg is a pre-rendered SVG overlay — use it directly in templates without re-rendering.
Using with ImageBlock
ImageBlock from uhuu-components opens the annotation dialog and draws the saved annotations over the image, so a template needs no wiring of its own. Pass the photo's dialog as dialog and the annotation as annotation:
import { ImageBlock, imageUrl } from 'uhuu-components'
<ImageBlock
src={imageUrl(page.heroImage)}
dialog={{ path: 'page.heroImage', type: 'image' }}
annotation={{
path: 'page.heroAnnotation',
value: page.heroAnnotation,
config: {
visualGallery: [
'https://cdn.example.com/icons/hospital.svg',
'https://cdn.example.com/icons/school.svg',
]
}
}}
/>- In the editor, clicking the image opens its image dialog, and a menu on the image offers Edit image and Annotate. Without
dialog, the menu offers only Annotate. - On the page and in the PDF,
ImageBlockdrawsvalue.annotationSvgover the image when it exists. The overlay is cropped like the image at its defaultobject-fit: cover, so pins stay on their spot. - The photo and its annotations are saved at their own payload paths: replacing the photo keeps the annotations.
ImageBlock fills in the dialog's type, its image (the block's src) and the saved annotations. Other keys you add to annotation, such as config, go to the dialog as they are. When src is a large original, pass a smaller copy as annotation.image for the editor background.
Rendering Annotations Without ImageBlock
For an image the template renders itself, open the dialog with $uhuu.editDialog (see Parameters) and draw annotationSvg over the image:
const svg = page.heroAnnotation?.annotationSvg
<div className="relative">
<img src={imageUrl} className="block w-full h-full object-cover" alt="" />
{svg && (
<div
className="absolute inset-0 [&>svg]:w-full [&>svg]:h-full"
aria-hidden="true"
dangerouslySetInnerHTML={{ __html: svg }}
/>
)}
</div>The overlay must cover the image box exactly. If the image is cropped with object-cover, also set preserveAspectRatio="xMidYMid slice" on the <svg> so the overlay is cropped the same way. ImageBlock does both for you.
The SVG overlay renders at full vector quality in PDFs: shapes, text, and connector lines stay sharp at any print size.
Notes
annotationSvgis only present when at least one annotation exists.ImageBlockhandles its absence; a template that renders the overlay itself guards against it, as above.- The
imageURL in the dialog config is the editor background only. The image displayed in the template is managed separately. - Positions are percentages, so annotations stay correctly placed if the image block changes size across breakpoints.
visualGalleryicons must be publicly accessible SVG URLs, not local paths.