flutter_sketchpad

Annotate images in Flutter — pressure-sensitive freehand drawing, text, shapes, layers, and measurement tools, with true vector export.

This is an annotation editor over an image, not a blank sketch pad. Reach for it when you need to mark up something: a diagram, a photo, a screenshot, a clinical body chart, an inspection report.

Features

  • Freehand strokes with real stylus pressure and variable width, so a line thickens and tapers as you press and release. Devices that report no pressure get a taper simulated from stroke velocity instead of a flat ribbon.
  • Text and shapes (circle, box, triangle, diamond, star, arrow, double arrow, line, cross, check) that drag, pinch-scale and rotate, with π/4 rotation snapping, centre alignment guides, duplicate and delete handles.
  • Layers with per-layer visibility and opacity — show one pass of annotations against another on the same background.
  • Measurement tool with real-world calibration: set pixels-per-unit once and distances read out in cm, inches, or whatever you like.
  • Highlighter and eraser. An eraser is confined to its own layer, so it cannot punch a hole through the background.
  • Stylus-only input mode — palm rejection for tablet use.
  • Serializable scenes. A sketch is plain JSON that reopens as live, editable strokes, not a flattened image.
  • Vector export. Output is re-rendered from the vector content at whatever resolution you ask for, rather than screenshotting the widget, so it stays sharp when zoomed in.
  • No assets, no Material dependency, no global side effects. The canvas never changes device orientation or system UI, and it sizes to its constraints — it does not have to be full-screen.

Install

dependencies:
  flutter_sketchpad: ^0.0.1

Quick start

The package does not load images: hand it a dart:ui Image from wherever you like, and tell the controller its size.

import 'package:flutter_sketchpad/flutter_sketchpad.dart';

final controller = SketchpadController()..tool = SketchTool.pen;

// Somewhere after you have decoded your background:
controller.setImageSize(Size(image.width.toDouble(), image.height.toDouble()));

Sketchpad(
  controller: controller,
  background: image,
)

Sketchpad draws the canvas and handles gestures. It ships no toolbar — what tools to offer and how they should look is your app's decision. Drive the controller from your own buttons:

controller.tool = SketchTool.highlighter;
controller.color = 0xFFEF4444;   // ARGB int, not a Color
controller.strokeWidth = 8;

controller.addShape(ShapeKind.arrow, position: centreOfImage);
controller.undo();
controller.clear();

Text editing is likewise yours, so the package imposes no design language. onOverlayDoubleTap fires when a selected annotation is tapped again:

Sketchpad(
  controller: controller,
  background: image,
  onOverlayDoubleTap: (overlay) async {
    if (overlay is! TextOverlay) return;
    final text = await myTextDialog(initial: overlay.text);
    if (text != null) controller.updateText(overlay, text);
  },
)

Saving and reopening

A SketchScene is the whole document. Store the JSON and the sketch stays editable forever:

final json = controller.scene.toJsonString();   // save this

controller.loadScene(
  SketchScene.fromJsonString(json),
  backgroundSize: Size(image.width.toDouble(), image.height.toDouble()),
);

Always pass backgroundSize. Scene coordinates are absolute image pixels, so a sketch authored against a 1024px-wide image would draw at quarter scale, in the wrong place, if the same image were later served at 4096px. backgroundSize rescales the scene onto the image you actually loaded. It is the single most common way to get this wrong.

Store the flattened PNG too if you want cheap thumbnails — but keep the JSON, because that is the part you can still edit.

Export

final png = await controller.exportPng(
  background: image,
  maxDimension: 2048,   // longest edge; supersamples vectors up to 4x
);

Hidden layers are excluded from export, matching what is on screen.

Layers

final id = controller.addLayer(name: 'Follow-up');
controller.setActiveLayer(id);              // new marks go here
controller.setLayerVisible(id, false);      // not undoable — it is a view
controller.setLayerOpacity(id, 0.4);
controller.removeLayer(id);                 // also removes its content

Visibility and opacity deliberately do not enter the undo history: changing how you look at a document is not an edit to it, and undoing back through view toggles is surprising.

Measurement

controller.calibration = SketchCalibration(pixelsPerUnit: 30, unit: 'cm');
controller.tool = SketchTool.measure;   // then drag on the canvas

Establish pixelsPerUnit from a known reference in the image: if a 10 cm scale bar spans 300 pixels, it is 30. Without a calibration, measurements read in pixels.

Coordinate space

Everything in a scene — stroke points, overlay positions, stroke widths — is in image pixels, not screen pixels. That is what makes a scene independent of zoom, device size and orientation, and it is why the same scene renders identically on screen, in a SketchView, and in an export.

Colours are stored as 32-bit ARGB ints rather than Color objects, which keeps the wire format independent of Flutter's colour API — that API has changed representation more than once.

Read-only display

SketchView(scene: savedScene, background: image)

Same renderer as the editor, no gesture handling.

Example

cd example && flutter run

The example generates its background image at runtime — no bundled assets — and exercises every feature, including a 10 cm scale bar you can calibrate the measurement tool against and check by eye.

Status

Pre-release (0.0.1), not yet published. Working and tested:

  • scene model and JSON format (v1), layers, calibration
  • renderer, PNG export, read-only view
  • interactive canvas: drawing, highlighter, eraser, measurement, overlay transforms, pan/zoom, undo/redo, stylus-only mode

Not implemented yet:

  • PDF and SVG export. Planned. dart:ui.Canvas and the pdf package use different drawing APIs, so this needs a small backend abstraction over the geometry rather than a drop-in.
  • Stylus hover preview — showing the brush before the pen touches down.
  • Per-layer picture caching. Committed strokes are currently re-rendered every frame; drawing over a very complex scene will cost more than it should.
  • A built-in default toolbar. For now, see the example's toolbar.

License

MIT

Libraries

flutter_sketchpad
Annotate images in Flutter: pressure-sensitive freehand drawing, text, shapes, layers, measurement tools, and true vector export.