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.
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 — seeserver/Dockerfile.PDF → PPT/Excel/Wordare 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:
- AndroidPdfViewer
- PdfBox-Android
- Commercial: PSPDFKit, PDFTron
📚 API Reference
PdfController
Main controller for PDF operations.
Properties:
document: Current PDF documentcurrentPage: Current page index (0-based)pageCount: Total number of pagescurrentZoom: Current zoom levelannotations: List of all annotationstextEdits: List of all text editsisLoading: Whether document is loadinghasDocument: Whether a document is loaded
Methods:
loadFromPath(String path, {String? password}): Load PDF from fileloadFromBytes(Uint8List bytes, {String? password}): Load PDF from bytesgoToPage(int index): Navigate to pagenextPage(): Go to next pagepreviousPage(): Go to previous pagesetZoom(double zoom): Set zoom leveladdAnnotation(PdfAnnotation): Add annotationremoveAnnotation(String id): Remove annotationaddText(PdfTextEdit): Add textremoveText(String id): Remove textaddImage(int page, PdfRect bounds, Uint8List bytes): Add imagesearchText(String query): Search textsave({String? outputPath}): Save documentgetBytes(): Get document as bytesdeletePage(int index): Delete pagerotatePage(int index, int degrees): Rotate pageclose(): Close document
undo(),redo(), andreorderPages()exist onPdfControllerbut have no native implementation yet — they returnfalse/ no-op rather than doing anything. Don't call them expecting an effect.
PdfAnnotation
Represents a PDF annotation.
Types:
ink: Freehand drawinghighlight: Text highlightingunderline: Text underlinestrikeout: Text strikeoutsquare: Square shapecircle: Circle shapefreeText: Text notestamp: Stampsignature: Signatureimage: Image annotation
PdfTextEdit
Represents text content in PDF.
Properties:
text: The text contentfontName: Font familyfontSize: Font sizetextColor: Text coloralignment: Text alignmentisBold: Bold styleisItalic: 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 parityWeb support using PDF.jsDesktop support (macOS, Windows, Linux)OCR text recognitionRedaction toolsDigital signaturesForm creation toolsBatch operationsCloud storage integrationCollaborative editing
🤝 Contributing
Contributions are welcome! Please read our Contributing Guide for details.
Development Setup
- Clone the repository
- Install dependencies:
flutter pub get - 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
📞 Support
- 📧 Email: support@example.com
- 🐛 Issues: GitHub Issues
- 💬 Discussions: GitHub Discussions
- 📖 Documentation: Full Documentation
🌟 Show Your Support
If you find this plugin helpful, please give it a ⭐ on GitHub!
Made with ❤️ by the Flutter community