Custom PDF Editor

A Flutter PDF viewer and on-canvas text editor for iOS (native PDFKit), plus PDF ↔ Word conversion. Tap any line to edit it directly on the page with the original font, size and colour preserved; shrink-to-fit keeps edits from overlapping neighbouring text.

pub package Platform

iOS only. Android is not yet implemented.

⚠️ Backend required

Text extraction, text modification and PDF↔Word conversion run on a small self-hosted Python backend (Flask + PyMuPDF), included in this repo under server/. You must deploy your own (e.g. free on Railway) and pass its URL as backendUrl — this package does not ship a hosted server. See server/README for deploy steps.

const backendUrl = 'https://your-app.up.railway.app';

✨ Features

Core Viewing

  • 📄 High-performance PDF rendering
  • 🔍 Zoom and pan gestures
  • 📑 Thumbnail navigation panel
  • 🔎 Full-text search with highlighting
  • 📄 Page navigation controls
  • 🎨 Dark mode support
  • 📱 Responsive UI

Advanced Editing

  • ✏️ Text Editing: Add, edit, and style text directly on PDF pages
    • Custom fonts, sizes, and colors
    • Text alignment (left, center, right, justified)
    • Bold and italic styles
  • 🎨 Annotations: Full annotation support
    • Highlight text
    • Ink/drawing annotations
    • Shapes (squares, circles)
    • Free text notes
    • Stamps and signatures
    • Customizable colors and opacity
  • 🖼️ Image Handling: Insert images
    • Add images to any page at a given position/size

Document Management

  • 💾 Save to file or get as bytes
  • 📄 Page operations:
    • Delete pages
    • Rotate pages
  • 🔒 Password protection support
  • 📤 Export to file or bytes

Not yet implemented (present in the Dart API but the native call silently no-ops, or absent entirely — don't rely on these): undo/redo, page reordering, inserting new pages, PDF forms, replacing an existing image in place, and native-side redaction (the paragraph editor's move/resize does use real PyMuPDF redaction on the backend — that one works).

📦 Installation

Add this to your package's pubspec.yaml file:

dependencies:
  custom_pdf_editor: ^0.1.0

Then run:

flutter pub get

iOS Setup

Add the following to your Info.plist:

<key>NSPhotoLibraryUsageDescription</key>
<string>Need access to photo library to insert images into PDFs</string>
<key>NSCameraUsageDescription</key>
<string>Need access to camera to capture images for PDFs</string>

Minimum iOS version: 12.0 (set in ios/Podfile):

platform :ios, '12.0'

Android Setup

Minimum SDK version: 21 (set in android/app/build.gradle):

minSdkVersion 21

Add permissions to AndroidManifest.xml:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

🚀 Quick Start

PDF editing (main feature)

Full-screen editor with on-canvas text editing. Requires your backendUrl.

import 'package:custom_pdf_editor/custom_pdf_editor.dart';

AdvancedPdfEditorWidget(
  pdfBytes: myPdfBytes,                       // Uint8List
  backendUrl: 'https://your-app.up.railway.app',
  onBack: () => Navigator.pop(context),
);

Paragraph editor (Acrobat-style, beta)

Separate full-screen mode where each paragraph is a movable, resizable box. Select a block to drag it, resize with the corner handles, double-tap to edit its text directly on the canvas (no dialog), or restyle it (size, colour, bold, italic, alignment). Select a word or range while editing to restyle just that range — mixed formatting (e.g. a bold coloured phrase inside a plain paragraph) is preserved and independently editable, matching how the source PDF is actually styled. Images under a moved/edited block are kept visible in the live preview and are untouched on export (redaction-based, not a white box), so nothing gets covered or clipped. Edits preview live and are baked with reflow on save (backend insert_htmlbox), then open a clean, box-free Preview screen with a Download button.

ParagraphEditorWidget(
  pdfBytes: myPdfBytes,
  backendUrl: 'https://your-app.up.railway.app',
  onBack: () => Navigator.pop(context),
  onSaved: (newBytes) { /* updated PDF */ },
);

Paragraph detection uses PyMuPDF text blocks — great for typical documents, less reliable for complex multi-column layouts or text wrapped around images. Moving/resizing a block only ever removes the original text (via redaction); images and vector art are never covered or deleted. Fonts are approximated (mapped to Base14 families), not the PDF's embedded fonts — very close but not byte-identical to the original. No undo/redo yet.

Conversions, repair & OCR — PdfBackendService

One unified, UI-free client for every backend operation. All methods take/return bytes so your app owns the pickers, previews and file handling.

final svc = PdfBackendService(baseUrl: 'https://your-app.up.railway.app');

// Office → PDF (LibreOffice, high fidelity)
await svc.wordToPdf(bytes, filename: 'doc.docx');
await svc.pptToPdf(bytes, filename: 'deck.pptx');
await svc.excelToPdf(bytes, filename: 'sheet.xlsx');

// PDF → Office (best effort)
await svc.pdfToWord(pdfBytes);
await svc.pdfToPptx(pdfBytes);
await svc.pdfToExcel(pdfBytes);   // extracts tables

// Utilities
await svc.compressPdf(pdfBytes, level: CompressionLevel.medium);
await svc.repairPdf(pdfBytes);
await svc.ocrPdf(pdfBytes, language: 'eng');

Backend must be the Docker image in server/ (LibreOffice + Tesseract + qpdf). The old Python buildpack can't run these — see server/Dockerfile. PDF → PPT/Excel/Word are best-effort (layout/tables), not pixel-perfect.

Compress PDF

final service = PdfCompressionService(baseUrl: 'https://your-app.up.railway.app');
final Uint8List? smaller = await service.compressPdf(
  myPdfBytes,
  level: CompressionLevel.medium, // low | medium | high
);

Most of the size reduction comes from downsampling/re-encoding images, so image-heavy PDFs shrink the most; text-only PDFs change little.

Basic viewing

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

class PDFViewerPage extends StatefulWidget {
  @override
  _PDFViewerPageState createState() => _PDFViewerPageState();
}

class _PDFViewerPageState extends State<PDFViewerPage> {
  late PdfController _pdfController;

  @override
  void initState() {
    super.initState();
    
    // Initialize controller with configuration
    _pdfController = PdfController(
      config: PdfEditorConfig(
        enableTextEditing: true,
        enableAnnotations: true,
        enableImageEditing: true,
        showToolbar: true,
        enableThumbnails: true,
      ),
    );
    
    // Load PDF
    _loadPDF();
  }

  Future<void> _loadPDF() async {
    // Load from file path
    await _pdfController.loadFromPath('/path/to/document.pdf');
    
    // Or load from bytes
    // final bytes = await rootBundle.load('assets/sample.pdf');
    // await _pdfController.loadFromBytes(bytes.buffer.asUint8List());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('PDF Viewer'),
        actions: [
          IconButton(
            icon: Icon(Icons.save),
            onPressed: () => _pdfController.save(),
          ),
        ],
      ),
      body: PdfViewerWidget(
        controller: _pdfController,
        onPageChanged: (page) {
          print('Page changed to: $page');
        },
        onDocumentLoaded: () {
          print('Document loaded');
        },
      ),
    );
  }

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

Adding Annotations

// Add highlight annotation
final highlight = PdfAnnotation(
  id: DateTime.now().millisecondsSinceEpoch.toString(),
  type: PdfAnnotationType.highlight,
  pageIndex: 0,
  bounds: PdfRect(x: 100, y: 100, width: 200, height: 50),
  color: PdfColor.yellow,
  opacity: 0.5,
  createdAt: DateTime.now(),
);

await _pdfController.addAnnotation(highlight);

// Add square shape
final square = PdfAnnotation(
  id: DateTime.now().millisecondsSinceEpoch.toString(),
  type: PdfAnnotationType.square,
  pageIndex: 0,
  bounds: PdfRect(x: 150, y: 200, width: 150, height: 150),
  color: PdfColor.red,
  borderWidth: 2.0,
  createdAt: DateTime.now(),
);

await _pdfController.addAnnotation(square);

Adding Text

final textEdit = PdfTextEdit(
  id: DateTime.now().millisecondsSinceEpoch.toString(),
  pageIndex: 0,
  bounds: PdfRect(x: 100, y: 300, width: 300, height: 50),
  text: 'Hello, PDF!',
  fontSize: 16.0,
  textColor: PdfColor.black,
  alignment: PdfTextAlignment.center,
  isBold: true,
);

await _pdfController.addText(textEdit);

Adding Images

// Load image bytes
final ByteData imageData = await rootBundle.load('assets/logo.png');
final Uint8List imageBytes = imageData.buffer.asUint8List();

// Add to PDF
await _pdfController.addImage(
  0, // page index
  PdfRect(x: 50, y: 50, width: 200, height: 200),
  imageBytes,
);

Search Text

final results = await _pdfController.searchText('flutter');
print('Found ${results.length} results');

for (final result in results) {
  print('Page ${result.pageIndex}: ${result.text}');
}

Page Management

// Go to specific page
await _pdfController.goToPage(5);

// Navigate
await _pdfController.nextPage();
await _pdfController.previousPage();

// Delete page
await _pdfController.deletePage(2);

// Rotate page
await _pdfController.rotatePage(0, 90); // Rotate by 90 degrees

// Reorder pages
await _pdfController.reorderPages(0, 2); // Move page 0 to position 2

Save Document

// Save to original location
await _pdfController.save();

// Save to specific path
await _pdfController.save(outputPath: '/path/to/output.pdf');

// Get bytes
final bytes = await _pdfController.getBytes();

🎨 Configuration

The PdfEditorConfig class allows you to customize the editor behavior:

PdfEditorConfig(
  // Enable/disable features
  enableTextEditing: true,
  enableAnnotations: true,
  enableImageEditing: true,
  enablePageManagement: true,
  enableForms: true,
  enableSearch: true,
  
  // UI options
  enableThumbnails: true,
  showToolbar: true,
  showPageNumbers: true,
  darkMode: false,
  
  // Zoom settings
  enableZoom: true,
  minZoom: 0.5,
  maxZoom: 5.0,
  
  // Other options
  initialPage: 0,
  autoSaveInterval: 0, // 0 to disable auto-save
  enablePasswordProtection: false,
)

📱 Platform-Specific Features

iOS (PDFKit)

  • ✅ Full feature support
  • ✅ Native annotations
  • ✅ Text editing with custom drawing
  • ✅ PencilKit integration for ink
  • ✅ Forms support
  • ✅ High performance

Android

  • ⚠️ Basic implementation
  • ✅ PDF viewing (PdfRenderer)
  • ⚠️ Limited annotation support
  • 📝 Full feature parity planned

Note: The current Android implementation is basic. For production use on Android, consider integrating third-party libraries like:

📚 API Reference

PdfController

Main controller for PDF operations.

Properties:

  • document: Current PDF document
  • currentPage: Current page index (0-based)
  • pageCount: Total number of pages
  • currentZoom: Current zoom level
  • annotations: List of all annotations
  • textEdits: List of all text edits
  • isLoading: Whether document is loading
  • hasDocument: Whether a document is loaded

Methods:

  • loadFromPath(String path, {String? password}): Load PDF from file
  • loadFromBytes(Uint8List bytes, {String? password}): Load PDF from bytes
  • goToPage(int index): Navigate to page
  • nextPage(): Go to next page
  • previousPage(): Go to previous page
  • setZoom(double zoom): Set zoom level
  • addAnnotation(PdfAnnotation): Add annotation
  • removeAnnotation(String id): Remove annotation
  • addText(PdfTextEdit): Add text
  • removeText(String id): Remove text
  • addImage(int page, PdfRect bounds, Uint8List bytes): Add image
  • searchText(String query): Search text
  • save({String? outputPath}): Save document
  • getBytes(): Get document as bytes
  • deletePage(int index): Delete page
  • rotatePage(int index, int degrees): Rotate page
  • close(): Close document

undo(), redo(), and reorderPages() exist on PdfController but have no native implementation yet — they return false / no-op rather than doing anything. Don't call them expecting an effect.

PdfAnnotation

Represents a PDF annotation.

Types:

  • ink: Freehand drawing
  • highlight: Text highlighting
  • underline: Text underline
  • strikeout: Text strikeout
  • square: Square shape
  • circle: Circle shape
  • freeText: Text note
  • stamp: Stamp
  • signature: Signature
  • image: Image annotation

PdfTextEdit

Represents text content in PDF.

Properties:

  • text: The text content
  • fontName: Font family
  • fontSize: Font size
  • textColor: Text color
  • alignment: Text alignment
  • isBold: Bold style
  • isItalic: Italic style

🔧 Troubleshooting

iOS Issues

Issue: "Symbol not found" errors

  • Solution: Make sure iOS deployment target is 12.0 or higher

Issue: Annotations not saving

  • Solution: Ensure you call save() before closing the document

Android Issues

Issue: "Platform view not found"

  • Solution: Ensure minimum SDK is 21 or higher

Issue: Limited features on Android

  • Solution: Android implementation is basic. Full features available on iOS.

🎯 Roadmap

  • Enhanced Android implementation with full feature parity
  • Web support using PDF.js
  • Desktop support (macOS, Windows, Linux)
  • OCR text recognition
  • Redaction tools
  • Digital signatures
  • Form creation tools
  • Batch operations
  • Cloud storage integration
  • Collaborative editing

🤝 Contributing

Contributions are welcome! Please read our Contributing Guide for details.

Development Setup

  1. Clone the repository
  2. Install dependencies: flutter pub get
  3. Run example: cd example && flutter run

Running Tests

flutter test

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

💖 Acknowledgments

  • Built with PDFKit (iOS)
  • Inspired by ComPDFKit
  • Community feedback and contributions

📞 Support

🌟 Show Your Support

If you find this plugin helpful, please give it a ⭐ on GitHub!


Made with ❤️ by the Flutter community