custom_pdf_editor 0.2.0 copy "custom_pdf_editor: ^0.2.0" to clipboard
custom_pdf_editor: ^0.2.0 copied to clipboard

Flutter PDF viewer and on-canvas text editor for iOS (native PDFKit), plus PDF↔Word conversion. Requires a companion self-hosted backend (included in server/).

example/lib/main.dart

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:custom_pdf_editor/custom_pdf_editor.dart';
import 'package:path_provider/path_provider.dart';
import 'package:file_picker/file_picker.dart';

/// Point this at your own deployed PDF backend (see `server/app.py`).
const String kBackendUrl = 'https://example-123.up.railway.app';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'PDF Editor Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

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

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

class _HomePageState extends State<HomePage> {
  final _customPdfEditorPlugin = CustomPdfEditor();
  String _platformVersion = 'Unknown';

  @override
  void initState() {
    super.initState();
    _initPlatformState();
  }

  Future<void> _initPlatformState() async {
    String platformVersion;
    try {
      platformVersion = await _customPdfEditorPlugin.getPlatformVersion() ??
          'Unknown platform version';
    } on PlatformException {
      platformVersion = 'Failed to get platform version.';
    }

    if (!mounted) return;

    setState(() {
      _platformVersion = platformVersion;
    });
  }

  Future<void> _openSamplePDF() async {
    // For demo purposes, you would load a real PDF here
    // This navigates to the PDF viewer screen
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => const PDFViewerScreen(),
      ),
    );
  }

  Future<void> _pickPDF() async {
    try {
      FilePickerResult? result = await FilePicker.platform.pickFiles(
        type: FileType.custom,
        allowedExtensions: ['pdf'],
      );

      if (result != null && result.files.single.path != null) {
        final file = File(result.files.single.path!);
        
        if (mounted) {
          Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) => PDFViewerScreen(pdfFile: file),
            ),
          );
        }
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Error picking file: $e')),
        );
      }
    }
  }

  /// Pick a PDF, compress it on the backend, and report the size reduction.
  Future<void> _pickForCompress() async {
    try {
      final result = await FilePicker.platform.pickFiles(
        type: FileType.custom,
        allowedExtensions: ['pdf'],
      );
      if (result == null || result.files.single.path == null) return;
      final bytes = await File(result.files.single.path!).readAsBytes();

      if (mounted) {
        showDialog(
          context: context,
          barrierDismissible: false,
          builder: (_) => const AlertDialog(
            content: Row(children: [
              CircularProgressIndicator(),
              SizedBox(width: 16),
              Text('Compressing...'),
            ]),
          ),
        );
      }

      final compressed = await PdfCompressionService(baseUrl: kBackendUrl)
          .compressPdf(bytes, level: CompressionLevel.medium);

      if (!mounted) return;
      Navigator.of(context).pop(); // close progress

      if (compressed == null) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('❌ Compression failed')),
        );
        return;
      }

      // Save the compressed copy next to a temp location.
      final dir = await getTemporaryDirectory();
      final outPath = '${dir.path}/compressed.pdf';
      await File(outPath).writeAsBytes(compressed);

      final before = (bytes.length / 1024).toStringAsFixed(0);
      final after = (compressed.length / 1024).toStringAsFixed(0);
      final saved =
          (100 * (1 - compressed.length / bytes.length)).toStringAsFixed(1);

      if (!mounted) return;
      showDialog(
        context: context,
        builder: (_) => AlertDialog(
          title: const Text('Compression complete'),
          content: Text(
            'Before: $before KB\nAfter: $after KB\nSaved: $saved%\n\n'
            'Saved to:\n$outPath',
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(context),
              child: const Text('OK'),
            ),
          ],
        ),
      );
    } catch (e) {
      if (mounted) {
        Navigator.of(context).maybePop();
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Error: $e')),
        );
      }
    }
  }

  /// Pick a PDF and open the Acrobat-style paragraph editor.
  Future<void> _pickForParagraphMode() async {
    try {
      final result = await FilePicker.platform.pickFiles(
        type: FileType.custom,
        allowedExtensions: ['pdf'],
      );
      if (result == null || result.files.single.path == null) return;
      final bytes = await File(result.files.single.path!).readAsBytes();
      if (!mounted) return;
      Navigator.push(
        context,
        MaterialPageRoute(
          builder: (context) => ParagraphEditorWidget(
            pdfBytes: bytes,
            backendUrl: kBackendUrl,
            onBack: () => Navigator.pop(context),
          ),
        ),
      );
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Error: $e')),
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('PDF Editor Demo'),
        elevation: 2,
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(24.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(
                Icons.picture_as_pdf,
                size: 100,
                color: Theme.of(context).primaryColor,
              ),
              const SizedBox(height: 16),
              Text(
                'Custom PDF Editor',
                style: Theme.of(context).textTheme.headlineMedium,
              ),
              const SizedBox(height: 8),
              Text(
                'Running on: $_platformVersion',
                style: Theme.of(context).textTheme.bodyMedium,
              ),
              const SizedBox(height: 24),
              SizedBox(
                width: double.infinity,
                child: ElevatedButton.icon(
                  onPressed: _openSamplePDF,
                  icon: const Icon(Icons.folder_open),
                  label: const Text('Open Sample PDF'),
                  style: ElevatedButton.styleFrom(
                    padding: const EdgeInsets.all(16),
                  ),
                ),
              ),
              const SizedBox(height: 16),
              SizedBox(
                width: double.infinity,
                child: OutlinedButton.icon(
                  onPressed: _pickPDF,
                  icon: const Icon(Icons.file_upload),
                  label: const Text('Select PDF from Files'),
                  style: OutlinedButton.styleFrom(
                    padding: const EdgeInsets.all(16),
                  ),
                ),
              ),
              const SizedBox(height: 16),
              SizedBox(
                width: double.infinity,
                child: OutlinedButton.icon(
                  onPressed: _pickForParagraphMode,
                  icon: const Icon(Icons.dashboard_customize),
                  label: const Text('Edit Paragraphs (beta)'),
                  style: OutlinedButton.styleFrom(
                    padding: const EdgeInsets.all(16),
                  ),
                ),
              ),
              const SizedBox(height: 8),
              SizedBox(
                width: double.infinity,
                child: OutlinedButton.icon(
                  onPressed: _pickForCompress,
                  icon: const Icon(Icons.compress),
                  label: const Text('Compress PDF'),
                  style: OutlinedButton.styleFrom(
                    padding: const EdgeInsets.all(16),
                  ),
                ),
              ),
              const SizedBox(height: 8),
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        'Features',
                        style: Theme.of(context).textTheme.titleLarge,
                      ),
                      const SizedBox(height: 12),
                      _buildFeatureItem(Icons.zoom_in, 'Zoom & Pan'),
                      _buildFeatureItem(Icons.edit, 'Text Editing'),
                      _buildFeatureItem(Icons.draw, 'Annotations'),
                      _buildFeatureItem(Icons.image, 'Image Insertion'),
                      _buildFeatureItem(Icons.search, 'Text Search'),
                      _buildFeatureItem(Icons.pages, 'Page Management'),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildFeatureItem(IconData icon, String text) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4.0),
      child: Row(
        children: [
          Icon(icon, size: 20, color: Colors.green),
          const SizedBox(width: 12),
          Text(text),
        ],
      ),
    );
  }
}

class PDFViewerScreen extends StatefulWidget {
  final File? pdfFile;
  const PDFViewerScreen({super.key, this.pdfFile});

  @override
  State<PDFViewerScreen> createState() => _PDFViewerScreenState();
}

class _PDFViewerScreenState extends State<PDFViewerScreen> {
  late PdfController _pdfController;
  final PdfConversionService _conversionService =
      PdfConversionService(baseUrl: kBackendUrl);
  bool _isLoading = true;
  String? _error;
  String _currentTool = 'view';
  bool _isInEditMode = false;
  File? _currentDocxFile;

  @override
  void initState() {
    super.initState();
    _pdfController = PdfController(
      config: const PdfEditorConfig(
        enableTextEditing: true,
        enableAnnotations: true,
        enableImageEditing: true,
        enableSearch: true,
        enableThumbnails: true,
        showToolbar: true,
        enableZoom: true,
        minZoom: 0.5,
        maxZoom: 5.0,
        enableUndoRedo: true,
      ),
    );
    _checkPythonSetup();
    _loadSamplePDF();
  }
  
  Future<void> _checkPythonSetup() async {
    final available = await _conversionService.checkPythonAvailability();
    if (!available && mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text(
            '⚠️ Python not found. Edit mode requires Python.\n'
            'Install: pip3 install pdf2docx python-docx reportlab',
          ),
          duration: Duration(seconds: 8),
          backgroundColor: Colors.orange,
        ),
      );
    }
  }
  
  Future<void> _enterAdvancedEditMode() async {
    if (!_pdfController.hasDocument) return;
    
    final pdfBytes = _pdfController.document?.bytes;
    if (pdfBytes == null) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('❌ No PDF data available'),
          backgroundColor: Colors.red,
        ),
      );
      return;
    }
    
    // Navigate to advanced editor
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => AdvancedPdfEditorWidget(
          pdfBytes: pdfBytes,
          backendUrl: kBackendUrl,
          onBack: () => Navigator.pop(context),
        ),
      ),
    );
  }
  
  Future<void> _enterEditMode() async {
    if (!_pdfController.hasDocument) return;
    
    setState(() {
      _isLoading = true;
    });
    
    try {
      // Get PDF bytes
      final pdfBytes = _pdfController.document?.bytes;
      if (pdfBytes == null) {
        throw Exception('No PDF data available');
      }
      
      // Show progress
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
            content: Row(
              children: [
                SizedBox(
                  width: 20,
                  height: 20,
                  child: CircularProgressIndicator(
                    strokeWidth: 2,
                    valueColor: AlwaysStoppedAnimation(Colors.white),
                  ),
                ),
                SizedBox(width: 12),
                Text('Converting PDF to DOCX...'),
              ],
            ),
            duration: Duration(seconds: 30),
          ),
        );
      }
      
      // Convert to DOCX
      final docxFile = await _conversionService.pdfToDocx(
        pdfBytes,
        'document_${DateTime.now().millisecondsSinceEpoch}',
      );
      
      if (docxFile != null) {
        setState(() {
          _currentDocxFile = docxFile;
          _isInEditMode = true;
          _isLoading = false;
        });
        
        if (mounted) {
          ScaffoldMessenger.of(context).clearSnackBars();
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text('✅ Converted successfully! Edit your document.'),
              backgroundColor: Colors.green,
              duration: Duration(seconds: 3),
            ),
          );
        }
      } else {
        throw Exception('Failed to convert PDF to DOCX');
      }
      
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).clearSnackBars();
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('❌ Error: $e'),
            backgroundColor: Colors.red,
            duration: const Duration(seconds: 5),
          ),
        );
      }
      setState(() {
        _isLoading = false;
      });
    }
  }
  
  Future<void> _onPdfGenerated(Uint8List pdfBytes) async {
    // Load the new PDF
    await _pdfController.loadFromBytes(pdfBytes);
    
    setState(() {
      _isInEditMode = false;
      _currentDocxFile = null;
    });
  }

  Future<void> _loadSamplePDF() async {
    try {
      final Uint8List bytes;
      if (widget.pdfFile != null) {
        bytes = await widget.pdfFile!.readAsBytes();
      } else {
        // Load a sample PDF from assets
        final ByteData data = await rootBundle.load('assets/sample.pdf');
        bytes = data.buffer.asUint8List();
      }

      debugPrint('PDF bytes loaded: ${bytes.length} bytes');
      await _pdfController.loadFromBytes(bytes);

      setState(() {
        _isLoading = false;
      });

      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
            content: Text('PDF loaded successfully'),
            backgroundColor: Colors.green,
          ),
        );
      }
    } catch (e) {
      debugPrint('Error loading PDF: $e');
      setState(() {
        _isLoading = false;
        _error = e.toString();
      });

      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Error loading PDF: $e'),
            backgroundColor: Colors.red,
            duration: const Duration(seconds: 5),
          ),
        );
      }
    }
  }

  Future<void> _addHighlight() async {
    final annotation = PdfAnnotation(
      id: DateTime.now().millisecondsSinceEpoch.toString(),
      type: PdfAnnotationType.highlight,
      pageIndex: _pdfController.currentPage,
      bounds: const PdfRect(x: 100, y: 100, width: 200, height: 50),
      color: PdfColor.yellow,
      opacity: 0.5,
      createdAt: DateTime.now(),
    );

    await _pdfController.addAnnotation(annotation);

    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Highlight added')),
      );
    }
  }

  Future<void> _addTextNote() async {
    final textEdit = PdfTextEdit(
      id: DateTime.now().millisecondsSinceEpoch.toString(),
      pageIndex: _pdfController.currentPage,
      bounds: const PdfRect(x: 100, y: 200, width: 300, height: 100),
      text: 'Sample text annotation',
      fontSize: 14.0,
      textColor: PdfColor.black,
      alignment: PdfTextAlignment.left,
    );

    await _pdfController.addText(textEdit);

    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Text added')),
      );
    }
  }

  Future<void> _addSquare() async {
    final annotation = PdfAnnotation(
      id: DateTime.now().millisecondsSinceEpoch.toString(),
      type: PdfAnnotationType.square,
      pageIndex: _pdfController.currentPage,
      bounds: const PdfRect(x: 150, y: 300, width: 150, height: 150),
      color: PdfColor.red,
      opacity: 0.7,
      borderWidth: 2.0,
      createdAt: DateTime.now(),
    );

    await _pdfController.addAnnotation(annotation);

    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Square added')),
      );
    }
  }

  Future<void> _searchText() async {
    final controller = TextEditingController();

    await showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Search Text'),
        content: TextField(
          controller: controller,
          decoration: const InputDecoration(
            hintText: 'Enter search query',
            border: OutlineInputBorder(),
          ),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('Cancel'),
          ),
          ElevatedButton(
            onPressed: () async {
              Navigator.pop(context);
              final results = await _pdfController.searchText(controller.text);

              if (mounted) {
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(
                    content: Text('Found ${results.length} results'),
                  ),
                );
              }
            },
            child: const Text('Search'),
          ),
        ],
      ),
    );
  }

  Future<void> _savePDF() async {
    try {
      final directory = await getApplicationDocumentsDirectory();
      final outputPath =
          '${directory.path}/edited_pdf_${DateTime.now().millisecondsSinceEpoch}.pdf';

      final success = await _pdfController.save(outputPath: outputPath);

      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text(
              success
                  ? 'PDF saved to: $outputPath'
                  : 'Failed to save PDF',
            ),
          ),
        );
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Error saving PDF: $e')),
        );
      }
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(
          _isInEditMode
              ? 'Edit Mode'
              : _pdfController.hasDocument
                  ? 'Page ${_pdfController.currentPage + 1} of ${_pdfController.pageCount}'
                  : 'PDF Viewer',
        ),
        actions: [
          if (!_isInEditMode) ...[
            IconButton(
              icon: const Icon(Icons.edit_document),
              onPressed: _pdfController.hasDocument ? _enterEditMode : null,
              tooltip: 'Edit (Convert to DOCX)',
            ),
            IconButton(
              icon: const Icon(Icons.auto_fix_high),
              onPressed: _pdfController.hasDocument ? _enterAdvancedEditMode : null,
              tooltip: 'Advanced Edit (Keep Format)',
            ),
            IconButton(
              icon: const Icon(Icons.undo),
              onPressed: _pdfController.hasDocument
                  ? () => _pdfController.undo()
                  : null,
              tooltip: 'Undo',
            ),
            IconButton(
              icon: const Icon(Icons.redo),
              onPressed: _pdfController.hasDocument
                  ? () => _pdfController.redo()
                  : null,
              tooltip: 'Redo',
            ),
            IconButton(
              icon: const Icon(Icons.search),
              onPressed: _pdfController.hasDocument ? _searchText : null,
              tooltip: 'Search',
            ),
            IconButton(
              icon: const Icon(Icons.save),
              onPressed: _pdfController.hasDocument ? _savePDF : null,
              tooltip: 'Save',
            ),
          ],
        ],
      ),
      body: _isLoading
          ? const Center(child: CircularProgressIndicator())
          : _error != null
              ? Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      const Icon(Icons.error, size: 64, color: Colors.red),
                      const SizedBox(height: 16),
                      Text('Error: $_error'),
                      const SizedBox(height: 16),
                      ElevatedButton(
                        onPressed: () => Navigator.pop(context),
                        child: const Text('Go Back'),
                      ),
                    ],
                  ),
                )
              : _isInEditMode && _currentDocxFile != null
                  ? DocxEditorWidget(
                      docxFile: _currentDocxFile!,
                      backendUrl: kBackendUrl,
                      onPdfGenerated: _onPdfGenerated,
                      onCancel: () {
                        setState(() {
                          _isInEditMode = false;
                          _currentDocxFile = null;
                        });
                      },
                    )
                  : Column(
                  children: [
                    // PDF Viewer
                    Expanded(
                      child: PdfViewerWidget(
                        controller: _pdfController,
                        onPageChanged: (page) {
                          setState(() {});
                        },
                        onDocumentLoaded: () {
                          debugPrint('Document loaded');
                        },
                        onError: (error) {
                          ScaffoldMessenger.of(context).showSnackBar(
                            SnackBar(content: Text('Error: $error')),
                          );
                        },
                      ),
                    ),

                    // Tool Bar
                    Container(
                      decoration: BoxDecoration(
                        color: Colors.white,
                        boxShadow: [
                          BoxShadow(
                            color: Colors.black.withValues(alpha: 0.1),
                            blurRadius: 4,
                            offset: const Offset(0, -2),
                          ),
                        ],
                      ),
                      child: Padding(
                        padding: const EdgeInsets.symmetric(
                          horizontal: 8.0,
                          vertical: 8.0,
                        ),
                        child: Row(
                          mainAxisAlignment: MainAxisAlignment.spaceAround,
                          children: [
                            _buildToolButton(
                              Icons.visibility,
                              'View',
                              'view',
                            ),
                            _buildToolButton(
                              Icons.highlight,
                              'Highlight',
                              'highlight',
                              onPressed: _addHighlight,
                            ),
                            _buildToolButton(
                              Icons.text_fields,
                              'Text',
                              'text',
                              onPressed: _addTextNote,
                            ),
                            _buildToolButton(
                              Icons.crop_square,
                              'Shape',
                              'shape',
                              onPressed: _addSquare,
                            ),
                          ],
                        ),
                      ),
                    ),

                    // Navigation Bar
                    Container(
                      color: Colors.grey[200],
                      padding: const EdgeInsets.all(8.0),
                      child: Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          IconButton(
                            icon: const Icon(Icons.chevron_left),
                            onPressed: _pdfController.currentPage > 0
                                ? () => _pdfController.previousPage()
                                : null,
                          ),
                          Text(
                            'Page ${_pdfController.currentPage + 1} / ${_pdfController.pageCount}',
                            style: const TextStyle(fontWeight: FontWeight.bold),
                          ),
                          IconButton(
                            icon: const Icon(Icons.chevron_right),
                            onPressed: _pdfController.currentPage <
                                    _pdfController.pageCount - 1
                                ? () => _pdfController.nextPage()
                                : null,
                          ),
                          const SizedBox(width: 16),
                          IconButton(
                            icon: const Icon(Icons.zoom_out),
                            onPressed: () {
                              final newZoom = _pdfController.currentZoom - 0.2;
                              _pdfController.setZoom(newZoom);
                            },
                          ),
                          Text('${(_pdfController.currentZoom * 100).toInt()}%'),
                          IconButton(
                            icon: const Icon(Icons.zoom_in),
                            onPressed: () {
                              final newZoom = _pdfController.currentZoom + 0.2;
                              _pdfController.setZoom(newZoom);
                            },
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
    );
  }

  Widget _buildToolButton(
    IconData icon,
    String label,
    String tool, {
    VoidCallback? onPressed,
  }) {
    final isSelected = _currentTool == tool;

    return InkWell(
      onTap: () {
        setState(() {
          _currentTool = tool;
        });
        onPressed?.call();
      },
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
        decoration: BoxDecoration(
          color: isSelected ? Colors.blue.withValues(alpha: 0.1) : Colors.transparent,
          borderRadius: BorderRadius.circular(8),
          border: Border.all(
            color: isSelected ? Colors.blue : Colors.transparent,
            width: 2,
          ),
        ),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(
              icon,
              color: isSelected ? Colors.blue : Colors.grey[700],
            ),
            const SizedBox(height: 4),
            Text(
              label,
              style: TextStyle(
                fontSize: 12,
                color: isSelected ? Colors.blue : Colors.grey[700],
              ),
            ),
          ],
        ),
      ),
    );
  }
}
1
likes
0
points
299
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter PDF viewer and on-canvas text editor for iOS (native PDFKit), plus PDF↔Word conversion. Requires a companion self-hosted backend (included in server/).

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

archive, flutter, http, path_provider, plugin_platform_interface

More

Packages that depend on custom_pdf_editor

Packages that implement custom_pdf_editor