certificate_canvas 0.0.1 copy "certificate_canvas: ^0.0.1" to clipboard
certificate_canvas: ^0.0.1 copied to clipboard

A drag-and-drop Flutter widget for designing certificates, resizing text, and overlaying images.

example/lib/main.dart

import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:certificate_canvas/certificate_canvas.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Certificate Designer',
      theme: ThemeData(
        primarySwatch: Colors.indigo,
        scaffoldBackgroundColor: Colors.grey[100],
      ),
      home: const DesignerPage(),
    );
  }
}

class DesignerPage extends StatefulWidget {
  const DesignerPage({super.key});

  @override
  State<DesignerPage> createState() => _DesignerPageState();
}

class _DesignerPageState extends State<DesignerPage> {
  Uint8List? _imageBytes; // The user's uploaded image
  String? _selectedId;
  final FocusNode _focusNode = FocusNode();

  // --- HISTORY FOR UNDO/REDO ---
  List<List<CertificateField>> _undoStack = [];
  List<List<CertificateField>> _redoStack = [];

  final List<Color> _colors = [
    Colors.black,
    Colors.white,
    Colors.blue,
    Colors.red,
    Colors.green,
    Colors.purple
  ];
  final List<String> _fonts = ['Roboto', 'Lato', 'Oswald', 'Dancing Script'];

  // Default fields to show when a user uploads a new template
  List<CertificateField> _fields = [
    CertificateField(
        id: "1",
        text: "Participant Name",
        x: 0.5,
        y: 0.5,
        width: 300, // Added default width for resize logic
        height: 80, // Added default height for resize logic
        fontSize: 30,
        fontName: 'Dancing Script'),
  ];

  // --- UNDO / REDO LOGIC ---
  void _saveCheckpoint() {
    final deepCopy = _fields.map((f) => f.copyWith()).toList();
    _undoStack.add(deepCopy);
    _redoStack.clear();
    if (_undoStack.length > 50) _undoStack.removeAt(0);
  }

  void _undo() {
    if (_undoStack.isEmpty) return;
    setState(() {
      _redoStack.add(_fields.map((f) => f.copyWith()).toList());
      _fields = _undoStack.removeLast();
      _selectedId = null;
    });
  }

  void _redo() {
    if (_redoStack.isEmpty) return;
    setState(() {
      _undoStack.add(_fields.map((f) => f.copyWith()).toList());
      _fields = _redoStack.removeLast();
    });
  }

  // --- ACTIONS ---
  Future<void> _pickImage() async {
    final ImagePicker picker = ImagePicker();
    final XFile? image = await picker.pickImage(source: ImageSource.gallery);

    if (image != null) {
      final Uint8List bytes = await image.readAsBytes();
      setState(() {
        _imageBytes = bytes;
      });
    }
  }

  void _handleAddText() {
    _saveCheckpoint();
    setState(() {
      _fields.add(CertificateField(
        id: DateTime.now().toString(),
        text: "New Text",
      ));
    });
  }

  void _handleEditText(String id) {
    final field = _fields.firstWhere((f) => f.id == id);
    TextEditingController controller = TextEditingController(text: field.text);

    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text("Edit Text"),
        content: TextField(
          controller: controller,
          autofocus: true,
          decoration: const InputDecoration(border: OutlineInputBorder()),
        ),
        actions: [
          TextButton(
              onPressed: () => Navigator.pop(context),
              child: const Text("Cancel")),
          ElevatedButton(
            onPressed: () {
              _saveCheckpoint();
              setState(() {
                final index = _fields.indexWhere((f) => f.id == id);
                if (index != -1)
                  _fields[index] =
                      _fields[index].copyWith(text: controller.text);
              });
              Navigator.pop(context);
            },
            child: const Text("Save"),
          )
        ],
      ),
    );
  }

  // --- UPDATED: HANDLES STYLING ---
  void _updateSelectedField(
      {double? fontSize,
      Color? color,
      String? fontName,
      bool? isBold, // NEW
      bool? isItalic // NEW
      }) {
    if (_selectedId == null) return;
    _saveCheckpoint();
    setState(() {
      final index = _fields.indexWhere((f) => f.id == _selectedId);
      if (index != -1) {
        _fields[index] = _fields[index].copyWith(
            fontSize: fontSize,
            color: color,
            fontName: fontName,
            isBold: isBold, // NEW
            isItalic: isItalic // NEW
            );
      }
    });
  }

  void _deleteSelected() {
    if (_selectedId == null) return;
    _saveCheckpoint();
    setState(() {
      _fields.removeWhere((f) => f.id == _selectedId);
      _selectedId = null;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_imageBytes == null) {
      return Scaffold(
        backgroundColor: Colors.white,
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Icon(Icons.cloud_upload_outlined,
                  size: 80, color: Colors.indigo),
              const SizedBox(height: 20),
              const Text("Upload Certificate Template",
                  style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
              const SizedBox(height: 10),
              const Text(
                  "Upload a blank certificate image (JPG/PNG) to start designing.",
                  style: TextStyle(color: Colors.grey)),
              const SizedBox(height: 30),
              ElevatedButton.icon(
                icon: const Icon(Icons.add_photo_alternate),
                label: const Text("Choose Image"),
                style: ElevatedButton.styleFrom(
                  padding:
                      const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
                  textStyle: const TextStyle(fontSize: 18),
                ),
                onPressed: _pickImage,
              ),
            ],
          ),
        ),
      );
    }

    CertificateField? selectedField;
    if (_selectedId != null) {
      try {
        selectedField = _fields.firstWhere((f) => f.id == _selectedId);
      } catch (e) {
        _selectedId = null;
      }
    }

    return CallbackShortcuts(
      bindings: {
        const SingleActivator(LogicalKeyboardKey.delete): _deleteSelected,
        const SingleActivator(LogicalKeyboardKey.backspace): _deleteSelected,
        const SingleActivator(LogicalKeyboardKey.keyZ, control: true): _undo,
        const SingleActivator(LogicalKeyboardKey.keyY, control: true): _redo,
      },
      child: Focus(
        focusNode: _focusNode,
        autofocus: true,
        child: Scaffold(
          body: Column(
            children: [
              // HEADER
              Container(
                height: 60,
                color: Colors.white,
                padding: const EdgeInsets.symmetric(horizontal: 16),
                child: Row(
                  children: [
                    const Text("CertBuilder",
                        style: TextStyle(
                            fontWeight: FontWeight.bold, fontSize: 18)),
                    const SizedBox(width: 20),
                    TextButton.icon(
                      icon: const Icon(Icons.image, size: 16),
                      label: const Text("Change Template"),
                      onPressed: _pickImage,
                    ),
                    const VerticalDivider(indent: 10, endIndent: 10),
                    IconButton(
                      icon: const Icon(Icons.undo),
                      onPressed: _undoStack.isNotEmpty ? _undo : null,
                      tooltip: "Undo (Ctrl+Z)",
                    ),
                    IconButton(
                      icon: const Icon(Icons.redo),
                      onPressed: _redoStack.isNotEmpty ? _redo : null,
                      tooltip: "Redo (Ctrl+Y)",
                    ),
                    const Spacer(),
                    ElevatedButton.icon(
                      icon: const Icon(Icons.add),
                      label: const Text("Add Text"),
                      onPressed: _handleAddText,
                    ),
                  ],
                ),
              ),

              // --- UPDATED TOOLBAR ---
              if (selectedField != null)
                Container(
                  height: 50,
                  color: Colors.white,
                  // Changed to ListView to prevent overflow with new buttons
                  child: ListView(
                    scrollDirection: Axis.horizontal,
                    padding: const EdgeInsets.symmetric(horizontal: 10),
                    children: [
                      DropdownButton<String>(
                        value: _fonts.contains(selectedField.fontName)
                            ? selectedField.fontName
                            : 'Roboto',
                        items: _fonts
                            .map((f) =>
                                DropdownMenuItem(value: f, child: Text(f)))
                            .toList(),
                        onChanged: (val) {
                          if (val != null) _updateSelectedField(fontName: val);
                        },
                        underline: Container(),
                      ),

                      const VerticalDivider(),

                      // --- NEW BOLD & ITALIC BUTTONS ---
                      IconButton(
                        tooltip: "Bold",
                        icon: Icon(Icons.format_bold,
                            color: selectedField.isBold
                                ? Colors.black
                                : Colors.grey),
                        onPressed: () => _updateSelectedField(
                            isBold: !selectedField!.isBold),
                      ),
                      IconButton(
                        tooltip: "Italic",
                        icon: Icon(Icons.format_italic,
                            color: selectedField.isItalic
                                ? Colors.black
                                : Colors.grey),
                        onPressed: () => _updateSelectedField(
                            isItalic: !selectedField!.isItalic),
                      ),

                      const VerticalDivider(),

                      // Colors
                      ..._colors.map((c) => GestureDetector(
                            onTap: () => _updateSelectedField(color: c),
                            child: Container(
                              margin: const EdgeInsets.symmetric(horizontal: 4),
                              width: 20,
                              height: 20,
                              decoration: BoxDecoration(
                                  color: c,
                                  shape: BoxShape.circle,
                                  border:
                                      Border.all(color: Colors.grey.shade300)),
                            ),
                          )),

                      const VerticalDivider(),

                      IconButton(
                          icon: const Icon(Icons.delete, color: Colors.red),
                          onPressed: _deleteSelected),
                    ],
                  ),
                ),

              // CANVAS
              Expanded(
                child: GestureDetector(
                  onTap: () {
                    setState(() => _selectedId = null);
                    _focusNode.requestFocus();
                  },
                  child: Container(
                    color: Colors.grey[100],
                    child: Center(
                      child: Padding(
                        padding: const EdgeInsets.all(20),
                        child: Container(
                          decoration: BoxDecoration(
                            boxShadow: [
                              BoxShadow(
                                  color: Colors.black12,
                                  blurRadius: 10,
                                  spreadRadius: 2)
                            ],
                          ),
                          child: CertificateCanvas(
                            imageBytes: _imageBytes,
                            fields: _fields,
                            selectedFieldId: _selectedId,
                            onDragStart: _saveCheckpoint,
                            onFieldTap: (id) {
                              setState(() => _selectedId = id);
                              _focusNode.requestFocus();
                            },
                            onFieldDoubleTap: _handleEditText,
                            onFieldUpdate: (id, updatedField) {
                              setState(() {
                                final index =
                                    _fields.indexWhere((f) => f.id == id);
                                if (index != -1) _fields[index] = updatedField;
                              });
                            },
                          ),
                        ),
                      ),
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
2
likes
140
points
130
downloads

Publisher

unverified uploader

Weekly Downloads

A drag-and-drop Flutter widget for designing certificates, resizing text, and overlaying images.

Homepage

Documentation

API reference

License

MIT (license)

Dependencies

flutter, google_fonts

More

Packages that depend on certificate_canvas