scribe_canvas 0.3.0 copy "scribe_canvas: ^0.3.0" to clipboard
scribe_canvas: ^0.3.0 copied to clipboard

A high-performance Flutter multi-page canvas library for handwriting, drawing, and annotation with customizable tools and PDF export.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:scribe/scribe.dart';
import 'package:file_picker/file_picker.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Scribe',
      theme: ThemeData(colorScheme: .fromSeed(seedColor: Colors.deepPurple)),
      debugShowCheckedModeBanner: false,
      home: const MyHomePage(title: 'Scribe'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final GlobalKey<ScribeCanvasState> _canvasKey =
      GlobalKey<ScribeCanvasState>();
  bool _isEraser = false;
  bool _isPanMode = false;
  double _strokeWidth = 4.0;
  double _eraserWidth = 30.0;
  String? _savedStrokes;
  String? _savedLegacyData;

  void _saveToMemory() {
    final data = _canvasKey.currentState?.getEncodedData();
    if (data != null && data.isNotEmpty) {
      debugPrint("Stroke Data Length -> ${data.length}, $data");
      setState(() {
        _savedStrokes = data;
      });
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Saved to memory (${data.length} chars)')),
      );
    }
  }

  void _reloadFromMemory() {
    if (_savedStrokes != null) {
      _canvasKey.currentState?.loadEncodedData(_savedStrokes!);
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Reloaded strokes from memory')),
      );
    } else {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('No saved strokes in memory')),
      );
    }
  }

  void _saveLegacyToMemory() {
    final data = _canvasKey.currentState?.getLegacyEncodedData();
    if (data != null && data.isNotEmpty) {
      debugPrint("Legacy Data Length -> ${data.length}");
      setState(() {
        _savedLegacyData = data;
      });
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('Saved Legacy to memory (${data.length} chars)'),
        ),
      );
    }
  }

  void _reloadLegacyFromMemory() {
    if (_savedLegacyData != null) {
      _canvasKey.currentState?.loadLegacyEncodedData(_savedLegacyData!);
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Reloaded Legacy strokes from memory')),
      );
    } else {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('No saved Legacy strokes in memory')),
      );
    }
  }

  void _clearCanvas() {
    _canvasKey.currentState?.clear();
  }

  void _toggleEraser() {
    setState(() {
      _isEraser = !_isEraser;
      if (_isEraser) _isPanMode = false; // Disable pan if switching to eraser
    });
  }

  void _togglePanMode() {
    setState(() {
      _isPanMode = !_isPanMode;
      if (_isPanMode) _isEraser = false; // Disable eraser if switching to pan
    });
  }

  void _savePdf() {
    _canvasKey.currentState?.exportToPdf();
  }

  void _resetView() {
    _canvasKey.currentState?.resetView();
  }

  void _undo() {
    _canvasKey.currentState?.undo();
  }

  void _redo() {
    _canvasKey.currentState?.redo();
  }

  Future<void> _pickBackground() async {
    final result = await FilePicker.platform.pickFiles(
      type: FileType.custom,
      allowedExtensions: ['pdf', 'png', 'jpg', 'jpeg'],
      withData: true,
    );

    if (result != null && result.files.single.bytes != null) {
      final file = result.files.single;
      if (file.extension == 'pdf') {
        await _canvasKey.currentState?.loadPdfBackground(file.bytes!);
      } else {
        await _canvasKey.currentState?.setBackgroundImage(
          0,
          file.bytes!,
          clearOthers: true,
        );
      }
      setState(() {});
    }
  }

  Future<void> _pickHeader() async {
    final result = await FilePicker.platform.pickFiles(
      type: FileType.image,
      withData: true,
    );
    if (result != null && result.files.single.bytes != null) {
      await _canvasKey.currentState?.setHeaderImage(result.files.single.bytes!);
      setState(() {});
    }
  }

  Future<void> _pickFooter() async {
    final result = await FilePicker.platform.pickFiles(
      type: FileType.image,
      withData: true,
    );
    if (result != null && result.files.single.bytes != null) {
      await _canvasKey.currentState?.setFooterImage(result.files.single.bytes!);
      setState(() {});
    }
  }

  Future<void> _addNetworkHeader() async {
    final TextEditingController controller = TextEditingController();
    final String? url = await showDialog<String>(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Add Network Header'),
        content: TextField(
          controller: controller,
          decoration: const InputDecoration(hintText: 'Enter Image URL'),
          autofocus: true,
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('Cancel'),
          ),
          TextButton(
            onPressed: () => Navigator.pop(context, controller.text),
            child: const Text('Add'),
          ),
        ],
      ),
    );

    if (url != null && url.isNotEmpty) {
      await _canvasKey.currentState?.setNetworkHeaderImage(url.trim());
      setState(() {});
    }
  }

  Future<void> _addNetworkFooter() async {
    final TextEditingController controller = TextEditingController();
    final String? url = await showDialog<String>(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Add Network Footer'),
        content: TextField(
          controller: controller,
          decoration: const InputDecoration(hintText: 'Enter Image URL'),
          autofocus: true,
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('Cancel'),
          ),
          TextButton(
            onPressed: () => Navigator.pop(context, controller.text),
            child: const Text('Add'),
          ),
        ],
      ),
    );

    if (url != null && url.isNotEmpty) {
      await _canvasKey.currentState?.setNetworkFooterImage(url.trim());
      setState(() {});
    }
  }

  Future<void> _addNetworkBackground() async {
    final TextEditingController controller = TextEditingController();
    final String? url = await showDialog<String>(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Add Network Background'),
        content: TextField(
          controller: controller,
          decoration: const InputDecoration(hintText: 'Enter Image URL'),
          autofocus: true,
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('Cancel'),
          ),
          TextButton(
            onPressed: () => Navigator.pop(context, controller.text),
            child: const Text('Add'),
          ),
        ],
      ),
    );

    if (url != null && url.isNotEmpty) {
      await _canvasKey.currentState?.setNetworkBackgroundImage(
        0,
        url.trim(),
        clearOthers: true,
      );
      setState(() {});
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
        actions: [
          IconButton(
            onPressed: _resetView,
            icon: const Icon(Icons.home_outlined),
            tooltip: 'Reset View (Home)',
          ),
          const VerticalDivider(width: 1),
          IconButton(
            onPressed: _canvasKey.currentState?.canUndo ?? false ? _undo : null,
            icon: const Icon(Icons.undo),
            color: _canvasKey.currentState?.canUndo ?? false
                ? Colors.black
                : Colors.grey,
            tooltip: 'Undo (Ctrl+Z)',
          ),
          IconButton(
            onPressed: _canvasKey.currentState?.canRedo ?? false ? _redo : null,
            icon: const Icon(Icons.redo),
            color: _canvasKey.currentState?.canRedo ?? false
                ? Colors.black
                : Colors.grey,
            tooltip: 'Redo (Ctrl+Y)',
          ),
          const VerticalDivider(width: 1),
          IconButton(
            onPressed: _toggleEraser,
            icon: Icon(_isEraser ? Icons.edit : Icons.auto_fix_normal),
            tooltip: _isEraser ? 'Switch to Pen' : 'Switch to Eraser',
          ),
          PopupMenuButton<String>(
            icon: const Icon(Icons.more_vert),
            onSelected: (value) {
              if (value == 'pan') _togglePanMode();
              if (value == 'bg') _pickBackground();
              if (value == 'net_bg') _addNetworkBackground();
              if (value == 'header') _pickHeader();
              if (value == 'net_header') _addNetworkHeader();
              if (value == 'footer') _pickFooter();
              if (value == 'net_footer') _addNetworkFooter();
              if (value == 'pdf') _savePdf();
              if (value == 'save_mem') _saveToMemory();
              if (value == 'load_mem') _reloadFromMemory();
              if (value == 'save_legacy') _saveLegacyToMemory();
              if (value == 'load_legacy') _reloadLegacyFromMemory();
              if (value == 'clear') _clearCanvas();
            },
            itemBuilder: (context) => [
              PopupMenuItem(
                value: 'pan',
                child: ListTile(
                  leading: Icon(
                    _isPanMode ? Icons.pan_tool : Icons.pan_tool_outlined,
                    color: _isPanMode ? Colors.blue : null,
                  ),
                  title: Text(
                    _isPanMode ? 'Switch to Pen' : 'Pan Mode',
                    style: TextStyle(color: _isPanMode ? Colors.blue : null),
                  ),
                  contentPadding: EdgeInsets.zero,
                ),
              ),
              const PopupMenuItem(
                value: 'bg',
                child: ListTile(
                  leading: Icon(Icons.add_photo_alternate_outlined),
                  title: Text('Add Local Background'),
                  contentPadding: EdgeInsets.zero,
                ),
              ),
              const PopupMenuItem(
                value: 'net_bg',
                child: ListTile(
                  leading: Icon(Icons.cloud_download_outlined),
                  title: Text('Add Network Background'),
                  contentPadding: EdgeInsets.zero,
                ),
              ),
              const PopupMenuDivider(),
              const PopupMenuItem(
                value: 'header',
                child: ListTile(
                  leading: Icon(Icons.vertical_align_top),
                  title: Text('Set Header (Local)'),
                  contentPadding: EdgeInsets.zero,
                ),
              ),
              const PopupMenuItem(
                value: 'net_header',
                child: ListTile(
                  leading: Icon(Icons.cloud_upload),
                  title: Text('Set Header (Network)'),
                  contentPadding: EdgeInsets.zero,
                ),
              ),
              const PopupMenuDivider(),
              const PopupMenuItem(
                value: 'footer',
                child: ListTile(
                  leading: Icon(Icons.vertical_align_bottom),
                  title: Text('Set Footer (Local)'),
                  contentPadding: EdgeInsets.zero,
                ),
              ),
              const PopupMenuItem(
                value: 'net_footer',
                child: ListTile(
                  leading: Icon(Icons.cloud_queue),
                  title: Text('Set Footer (Network)'),
                  contentPadding: EdgeInsets.zero,
                ),
              ),
              const PopupMenuItem(
                value: 'pdf',
                child: ListTile(
                  leading: Icon(Icons.picture_as_pdf),
                  title: Text('Save as PDF'),
                  contentPadding: EdgeInsets.zero,
                ),
              ),
              const PopupMenuDivider(),
              const PopupMenuItem(
                value: 'load_mem',
                child: ListTile(
                  leading: Icon(Icons.refresh),
                  title: Text('Reload from Memory'),
                  contentPadding: EdgeInsets.zero,
                ),
              ),
              const PopupMenuItem(
                value: 'save_legacy',
                child: ListTile(
                  leading: Icon(Icons.history_edu),
                  title: Text('Save Legacy Format'),
                  contentPadding: EdgeInsets.zero,
                ),
              ),
              const PopupMenuItem(
                value: 'load_legacy',
                child: ListTile(
                  leading: Icon(Icons.history),
                  title: Text('Load Legacy Format'),
                  contentPadding: EdgeInsets.zero,
                ),
              ),
              const PopupMenuDivider(),
              const PopupMenuItem(
                value: 'clear',
                child: ListTile(
                  leading: Icon(Icons.clear, color: Colors.red),
                  title: Text(
                    'Clear Canvas',
                    style: TextStyle(color: Colors.red),
                  ),
                  contentPadding: EdgeInsets.zero,
                ),
              ),
            ],
          ),
        ],
      ),
      body: Container(
        color: Colors.grey[200],
        child: ScribeCanvas(
          key: _canvasKey,
          color: Colors.blueAccent,
          strokeWidth: _strokeWidth,
          strokeSizes: const [2.0, 6.0, 12.0, 24.0],
          isEraser: _isEraser,
          eraserWidth: _eraserWidth,
          eraserSizes: const [20.0, 30.0, 40.0, 60.0, 80.0],
          isPanMode: _isPanMode,
          onStrokeEnd: () => setState(() {}),
          onUndo: () => setState(() {}),
          onRedo: () => setState(() {}),
          onStrokeWidthChanged: (width) => setState(() => _strokeWidth = width),
          onEraserWidthChanged: (width) => setState(() => _eraserWidth = width),
          onToggleEraser: (val) => setState(() {
            _isEraser = val;
            if (_isEraser) _isPanMode = false;
          }),
          multiPage: true,
        ),
      ),
    );
  }
}

// TODO

// add header image as file/network image
// add footer image as file/network image

// multiple stroke size,
// multiple eraser size,
// separate icon for eraser and pen,
// tap on pen/eraser to increase size
// multi color option
3
likes
0
points
281
downloads

Publisher

unverified uploader

Weekly Downloads

A high-performance Flutter multi-page canvas library for handwriting, drawing, and annotation with customizable tools and PDF export.

Homepage

License

unknown (license)

Dependencies

file_picker, flutter, pdf, pdfx, printing

More

Packages that depend on scribe_canvas