scribe_canvas

A high-performance Flutter multi-page canvas library for handwriting, drawing, and annotation. Scribe handles multiple A4-sized pages with smooth variable-width strokes, customizable tools, PDF export, and a clean Controller-based API.

Features

  • O(1) High-Performance Rendering — Uses a vector-based picture caching system to ensure 60fps performance even with thousands of strokes.
  • Multi-page Canvas — Multiple A4-sized pages in a single scrollable view, with dynamic add/insert/delete support.
  • Intelligent Ink — Authentic ink-like strokes with dynamic velocity-based width sampling and Catmull-Rom spline smoothing.
  • Smart Stroke Eraser — Object-based erasing with live highlighting. Touch any part of a stroke to remove the entire line instantly.
  • Customizable Tools — Cycling pen/eraser sizes, multi-color palette, and an integrated PageHeader tool bar.
  • Backgrounds & Templates — Load local or network images, or render a full PDF as per-page background templates.
  • Headers & Footers — Persistent banner images applied to every page on export.
  • Navigation — Pinch-to-zoom and pan with dynamic constraints. Supports both Continuous vertical scrolling and Discrete page-by-page navigation with a built-in sidebar.
  • Undo / Redo — Global and per-page undo/redo stacks.
  • Serialization — Save and restore canvas state as a compact JSON string (variable-width preserved).
  • PDF Export — Export the full multi-page drawing with backgrounds, headers, and footers.

Installation

dependencies:
  scribe_canvas: ^0.6.3

Quick Start

All interactions with the canvas are done through a ScribeCanvasController. This follows Flutter's standard controller pattern (like TextEditingController or ScrollController).

import 'package:flutter/material.dart';
import 'package:scribe_canvas/scribe_canvas.dart';

class DrawingPage extends StatefulWidget {
  const DrawingPage({super.key});
  @override
  State<DrawingPage> createState() => _DrawingPageState();
}

class _DrawingPageState extends State<DrawingPage> {
  final _controller = ScribeCanvasController();
  bool _isEraser = false;

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        actions: [
          IconButton(icon: const Icon(Icons.undo), onPressed: _controller.undo),
          IconButton(icon: const Icon(Icons.redo), onPressed: _controller.redo),
          IconButton(
            icon: Icon(_isEraser ? Icons.edit : Icons.auto_fix_normal),
            onPressed: () => setState(() => _isEraser = !_isEraser),
          ),
          IconButton(
            icon: const Icon(Icons.picture_as_pdf),
            onPressed: _controller.exportToPdf,
          ),
        ],
      ),
      body: ScribeCanvas(
        controller: _controller,  // attach the controller
        isEraser: _isEraser,
        onToggleEraser: (val) => setState(() => _isEraser = val),
        onStrokeEnd: () => setState(() {}),
        onUndo: () => setState(() {}),
        onRedo: () => setState(() {}),
      ),
    );
  }
}

ScribeCanvas Properties

Property Type Default Description
controller ScribeCanvasController? Required to call any imperative API.
color Color Colors.black Initial pen color.
strokeWidth double 4.0 Initial pen stroke width.
strokeSizes List<double> [2,4,8,16,32] Sizes to cycle through in the header.
isEraser bool false Whether eraser mode is active.
eraserWidth double 30.0 Initial eraser width.
eraserSizes List<double> [10,20,30,60,100] Eraser sizes to cycle through.
isPanMode bool false When true, disables drawing and enables panning only.
multiPage bool true Enables multi-page mode. When false, the canvas is a single fixed page.
initialColor Color Colors.black Initial color selected in the palette.
colors List<Color> (6 colors) Colors shown in the palette.
onStrokeStart VoidCallback? Called when a stroke begins.
onStrokeEnd VoidCallback? Called when a stroke is finalized. Good place to call setState to refresh undo/redo button states.
onUndo VoidCallback? Called after an undo operation.
onRedo VoidCallback? Called after a redo operation.
onStrokeWidthChanged ValueChanged<double>? Called when the user cycles the pen size.
onEraserWidthChanged ValueChanged<double>? Called when the user cycles the eraser size.
onToggleEraser ValueChanged<bool>? Called when pen/eraser is toggled from the header.
onColorChanged ValueChanged<Color>? Called when the user picks a color.
scrollMode ScribeScrollMode continuous Switch between continuous and discrete (page-by-page) scrolling.
initialPageIndex int 0 The page to show on initial load.
eraserIcon IconData? Custom icon for the eraser tool in the header.
eraserIconSize double? Custom size for the eraser icon.
eraserActiveColor Color? Icon color when eraser is active.
eraserInactiveColor Color? Icon color when eraser is inactive.

ScribeCanvasController API

Undo & Redo

_controller.undo();             // Undo last global stroke
_controller.redo();             // Redo last global stroke
_controller.undoPage(0);        // Undo last stroke on page 0
_controller.redoPage(0);        // Redo last stroke on page 0

bool canUndo = _controller.canUndo;   // Check before enabling undo button
bool canRedo = _controller.canRedo;

Canvas Operations

_controller.clear();            // Clear all data (strokes & history)
_controller.clearBackgrounds(); // Remove background images/PDF
_controller.resetView();        // Reset zoom/pan to initial state (on Page 1)

Page Management

_controller.addPage();          // Append a new page at the end
_controller.insertPage(1);      // Insert a blank page at index 1
_controller.deletePage(1);      // Delete the page at index 1

Backgrounds & Templates

// Local image bytes as a background for page 0
final bytes = await rootBundle.load('assets/template.png');
await _controller.setBackgroundImage(0, bytes.buffer.asUint8List(), clearOthers: true);

// Network image as background for page 0
await _controller.setNetworkBackgroundImage(0, 'https://example.com/bg.png');

// PDF rendered as per-page background templates
await _controller.loadPdfBackground(pdfBytes);

// Remove all backgrounds (keeps drawings)
_controller.clearBackgrounds();

Headers & Footers

Headers and footers are rendered on every page during PDF export.

// From local file
await _controller.setHeaderImage(headerBytes);
await _controller.setFooterImage(footerBytes);

// From network URL
await _controller.setNetworkHeaderImage('https://example.com/logo.png');
await _controller.setNetworkFooterImage('https://example.com/footer.png');

Serialization (Save & Load)

// Save the current drawing state
final String data = _controller.getEncodedData();
await prefs.setString('canvas', data);

// Restore a previously saved drawing
final String? saved = prefs.getString('canvas');
if (saved != null) _controller.loadEncodedData(saved);

// Legacy format (no variable-width data)
final String legacy = _controller.getLegacyEncodedData();
_controller.loadLegacyEncodedData(legacy);

PDF Export

// 1. Standard export (shares/prints and returns bytes)
final Uint8List? bytes = await _controller.exportToPdf(fileName: 'drawing.pdf');

// 2. Silent export (returns bytes for server upload without opening share dialog)
final Uint8List? bytes = await _controller.exportToPdf(share: false);
if (bytes != null) {
  // Use bytes to upload to your remote DB
}

Performance

Scribe is designed for professional-grade note-taking and sketching. It uses a Vector Caching Architecture:

  • Active Drawing: Only the single active stroke is processed every frame.
  • Committed Drawing: Once a stroke is finished, it is "baked" into a ui.Picture (a vector snapshot).
  • Rendering Complexity: Drawing 1 stroke or 10,000 strokes has the exact same cost on the GPU (O(1) blit).

This ensures that even on older hardware, the canvas remains responsive during long writing sessions.

Additional Information

Scribe uses InteractiveViewer for navigation and CustomPainter for rendering. It leverages the pdf and printing packages for cross-platform PDF generation, and pdfx for rendering PDF background templates.

Libraries

scribe_canvas