photo_markup 1.0.2 copy "photo_markup: ^1.0.2" to clipboard
photo_markup: ^1.0.2 copied to clipboard

Draw boxes, arrows, freehand strokes and text over a photo, read it back as resolution-independent vector data, and flatten it to a JPEG or PNG.

example/lib/main.dart

import 'dart:math' as math;
import 'dart:typed_data';

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

void main() => runApp(const ExampleApp());

/// The bundled photo every screen in this demo annotates.
const AssetImage kSamplePhoto = AssetImage('assets/sample_photo.jpg');

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'photo_markup example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF3B82F6)),
      ),
      home: const HomePage(),
    );
  }
}

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  /// The persisted form of the markup. In a real app this is the string you
  /// would write to a database column, a file, or an API — it is the source of
  /// truth, not the flattened image.
  String? _storedJson;

  /// Derived artifact: photo + markup baked into JPEG bytes.
  Uint8List? _flattened;

  /// Decoded view of [_storedJson]. Re-decoding on every build is what proves
  /// the round-trip actually works.
  MarkupDocument get _document => _storedJson == null
      ? MarkupDocument.empty(4 / 3)
      : MarkupCodec.decodeJson(_storedJson!, fallbackAspectRatio: 4 / 3);

  Future<void> _openEditor() async {
    final result = await Navigator.of(context).push<MarkupResult>(
      MaterialPageRoute(
        builder: (_) => PhotoMarkupEditor(
          image: kSamplePhoto,
          // Re-open with whatever was drawn last time.
          initialAnnotations: _document.annotations,
          // Keep the exported image small enough to show inline.
          targetWidth: 1200,
        ),
      ),
    );
    if (result == null) return; // user cancelled

    setState(() {
      _storedJson = MarkupCodec.encodeJson(result.document);
      _flattened = result.flattenedImage;
    });
  }

  void _reset() => setState(() {
    _storedJson = null;
    _flattened = null;
  });

  @override
  Widget build(BuildContext context) {
    final doc = _document;
    return Scaffold(
      appBar: AppBar(
        title: const Text('photo_markup'),
        actions: [
          IconButton(
            onPressed: _storedJson == null ? null : _reset,
            icon: const Icon(Icons.restart_alt),
            tooltip: 'Reset',
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: _openEditor,
        icon: const Icon(Icons.edit),
        label: Text(doc.isEmpty ? 'Mark up' : 'Edit markup'),
      ),
      body: ListView(
        padding: const EdgeInsets.fromLTRB(16, 16, 16, 96),
        children: [
          const _SectionTitle(
            'PhotoMarkupViewer',
            'Read-only overlay, rendered from the decoded JSON.',
          ),
          ClipRRect(
            borderRadius: BorderRadius.circular(12),
            child: PhotoMarkupViewer(image: kSamplePhoto, document: doc),
          ),
          if (doc.isEmpty)
            const Padding(
              padding: EdgeInsets.only(top: 12),
              child: Text(
                'Nothing drawn yet — tap "Mark up" to open the editor.',
                style: TextStyle(fontStyle: FontStyle.italic),
              ),
            ),

          if (doc.isNotEmpty) ...[
            const _SectionTitle(
              'Resolution independence',
              'The same document painted at three sizes. Coordinates are '
                  'normalized, so strokes scale with the image instead of '
                  'staying a fixed pixel width.',
            ),
            LayoutBuilder(
              builder: (context, constraints) {
                const widths = <double>[64, 120, 200];
                const gap = 12.0;
                final natural =
                    widths.reduce((a, b) => a + b) + gap * (widths.length - 1);
                // Narrow phones can't fit 420px of previews, so shrink the
                // whole set proportionally rather than overflow the Row.
                final scale = math.min(1.0, constraints.maxWidth / natural);
                return Row(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    for (final (index, width) in widths.indexed)
                      Padding(
                        padding: EdgeInsets.only(
                          left: index == 0 ? 0 : gap * scale,
                        ),
                        child: SizedBox(
                          width: width * scale,
                          child: ClipRRect(
                            borderRadius: BorderRadius.circular(6),
                            child: PhotoMarkupViewer(
                              image: kSamplePhoto,
                              document: doc,
                            ),
                          ),
                        ),
                      ),
                  ],
                );
              },
            ),
          ],

          if (_flattened != null) ...[
            _SectionTitle(
              'Flattened export',
              'JPEG bytes returned by MarkupResult.flattenedImage '
                  '(${(_flattened!.lengthInBytes / 1024).toStringAsFixed(1)} kB). '
                  'A derived file — the JSON below is the source of truth.',
            ),
            ClipRRect(
              borderRadius: BorderRadius.circular(12),
              child: Image.memory(_flattened!),
            ),
          ],

          if (_storedJson != null) ...[
            _SectionTitle(
              'Persisted JSON',
              '${doc.annotations.length} annotation(s), '
                  '${_storedJson!.length} chars. This is what you store.',
            ),
            Container(
              width: double.infinity,
              padding: const EdgeInsets.all(12),
              decoration: BoxDecoration(
                color: Theme.of(context).colorScheme.surfaceContainerHighest,
                borderRadius: BorderRadius.circular(12),
              ),
              child: SelectableText(
                _storedJson!,
                style: const TextStyle(fontFamily: 'monospace', fontSize: 11),
              ),
            ),
          ],
        ],
      ),
    );
  }
}

class _SectionTitle extends StatelessWidget {
  const _SectionTitle(this.title, this.subtitle);

  final String title;
  final String subtitle;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Padding(
      padding: const EdgeInsets.only(top: 24, bottom: 8),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(title, style: theme.textTheme.titleMedium),
          const SizedBox(height: 2),
          Text(
            subtitle,
            style: theme.textTheme.bodySmall?.copyWith(
              color: theme.colorScheme.onSurfaceVariant,
            ),
          ),
        ],
      ),
    );
  }
}
1
likes
160
points
124
downloads

Documentation

API reference

Publisher

verified publishervelzosoft.com

Weekly Downloads

Draw boxes, arrows, freehand strokes and text over a photo, read it back as resolution-independent vector data, and flatten it to a JPEG or PNG.

Repository (GitHub)
View/report issues

Topics

#annotation #markup #drawing #image #canvas

License

BSD-3-Clause (license)

Dependencies

flutter, image

More

Packages that depend on photo_markup