Mark a picture up with arrows, shapes, text and freehand, in the picture's own pixels: an annotation is the same size in the file whatever the view is zoomed to.
Initializing in your browser…
Compare two images with a perceptual engine (pixelmatch) that ignores anti-aliasing, or an exact byte engine. Groups changes into regions with bounding boxes, reports SSIM, and handles size mismatches with an explicit policy.
Caption a picture with text that is measured, wrapped and kept inside the frame. The preview canvas is the exported file, pixel for pixel.
Turn an image into a data URL whose payload is decoded again and compared byte for byte with the file, with the media type taken from the file's own signature rather than its extension and the size cost of embedding stated in full.
A rectangle dragged from (100, 80) to (400, 300) of a 1200 by 800 picture, which the tool shows at 74.67 percent.
Input
1200 x 800 picture · rectangle tool · stroke 4 px
What the exported file has
before after ink box x 98 to 405 97 to 402 ink box y 78 to 304 77 to 301 stroke ink, at 75% 8 px 6 px stroke ink, at 125% 8 px 6 px a 4 px stroke is really, in the file 7.2 px 4.0 px
The placement was already right, which is worth saying. What was not is the size: the stroke width was divided by the display scale when the shape was made and multiplied by it again on export, so a 4 px stroke put 7.2 px in the file and the number moved with the zoom. The canvas now works in the picture’s own coordinates with the zoom in the viewport transform, so an annotation is the same size in the file whatever you are looking at.
Mark a picture up with arrows, circles, text, shapes and freehand, every one of them an object you can move, restyle or delete. The annotations live in the picture’s own pixels, so a stroke width means the same number of file pixels whatever the view is zoomed to.
Mark up screenshots to show exactly where an issue occurs.
Add arrows and labels to screenshots for how-to guides.
Annotate mockups with comments and change requests.
Label interface elements at a stroke width that means the same thing in every file.
Press A for the Arrow tool and drag from a caption toward a button; the tool draws a line and a grouped triangular head in your stroke colour. Switch to Rectangle (R) with no fill to box a region, then press T and click to drop editable labels. The download composites everything at the picture’s full resolution.
On a 1200 by 800 picture shown at 74.67 percent, a rectangle dragged from (100, 80) to (400, 300) exports with its ink from x 97 to 402 and its stroke 4 px wide. Zoom to 125 percent and export again: the same numbers. Before this pass the same stroke was 7.2 px of the file and changed with the view.
The Image Annotator is built on Fabric.js v6 and treats every mark as an independent vector object rather than baking it into the picture. What changed in this pass is the coordinate system underneath all of them.
**An annotation is now measured in the PICTURE’s pixels.** The canvas previously worked in DISPLAY pixels: it was created at the picture’s size times the fit-to-container scale, every stroke width was divided by that scale on the way in, and the export multiplied by it again on the way out. A stroke of 4 therefore put 7.2 px in the file at the tool’s own default view of a 1200 by 800 picture, measured as 8 px of ink against the 6 a 4 px stroke really covers, and the number moved with the zoom. Zooming also rescaled every object’s left, top and scale, so the coordinates in the undo history belonged to whichever zoom they were recorded at. The canvas now works in the picture’s coordinates with the zoom in Fabric’s viewport transform: nothing about an annotation changes when you zoom, the stroke width is the number on the control, and the panel reports every annotation’s box in the picture’s own pixels.
**The placement was already right, and that is worth recording.** Measured before any change, a rectangle dragged from (100, 80) to (400, 300) of a 1200 by 800 picture landed at x 98 to 405 and y 78 to 304 in the exported file, which is the drag plus its stroke. The pass could then go to the size, which was not.
**The export is drawn once and synchronously.** It used to render each object to a data URL, assign it to an `Image`, and read the export canvas back on a fixed 100 ms `setTimeout`: anything whose `onload` had not fired by then was simply missing from the file and nothing said so. `FabricObject.render(ctx)` is what the canvas itself calls, so drawing the objects onto a plain context at the picture’s own size needs no timer and cannot drop one. Twelve annotations in one file are asserted in the test suite by counting the edges in the produced PNG.
Thirteen tools are available: Select, Pan, Arrow, Line, Rectangle, Circle (a Fabric Ellipse), Triangle, Star, Polygon, Freehand, Text, Highlight and Eraser. Each shape stays a live object, so it can be re-selected, dragged, resized, rotated, duplicated (Ctrl+D, offset by 20 px), flipped and reordered before export. The Arrow tool draws a line during the drag and appends a triangular head of `max(15, strokeWidth * 3)` on mouse-up, grouping the two. Star and Polygon are generated at a base radius of 50 and scaled during the drag. Highlight is a filled rectangle in the stroke colour at 0.3 opacity. Text is Fabric’s inline-editable IText. The Eraser is object-based: clicking an annotation removes that object and never touches the picture.
Undo and redo keep up to 50 states, serialised with `toDatalessJSON`, committed only when a draw completes so a partial 1 by 1 shape is never recorded. A shape’s cached corner coordinates are now refreshed as it is dragged, which they were not: selection hit-testing and every reading of a shape’s bounding box were of the 1 by 1 it had been created as. The canvas zooms from 25 to 300 percent, and the export is PNG, JPEG or WebP with the type read back off the produced bytes and the size, format and annotation count reported. Everything runs in the browser and the picture is never uploaded.
No, and that is the point of this version. The zoom is a property of the view, not of the drawing: an annotation keeps its position, its size and its stroke width in the picture’s own pixels. Before this pass the stroke width was divided by the display scale and multiplied by it again on export, so a 4 px stroke put 7.2 px in the file.
Yes. Each one is a separate object that can be dragged, resized, rotated, restyled and deleted at any time before you download.
No practical limit, and none of them can be lost on the way out: the export draws every object synchronously rather than waiting a fixed 100 ms for images to decode, which is what the previous version did.
PNG, JPEG and WebP, at the picture’s own resolution. The type is read back off the produced bytes and the extension comes from what the file actually is.
The tool is for annotating an existing picture. For drawing on a blank canvas, use the Drawing Canvas.
Images are decoded, edited, and exported entirely inside this browser tab. No originals, exports, or metadata are uploaded.