annotation_for_all

A standalone Flutter toolkit for annotating PDFs: freehand drawing, highlight, underline, grading marks (tick / cross / circle / question mark / number stamps), and image insertion.

You hand it a PDF, the user annotates it, you get the annotated PDF back as bytes. That's it β€” no backend, no storage assumptions, no accounts. What you do with the bytes (save locally, upload, email, whatever) is entirely up to your app.

A quiz PDF annotated with tick/cross marks and a freehand circle

Features

  • ✏️ Freehand drawing with a color picker
  • πŸ–οΈ Highlight and underline selected text
  • βœ… Grading marks: tick, cross, circle, question mark, number stamps (handy for marking student submissions)
  • πŸ–ΌοΈ Image insertion (from gallery, auto-compressed if large)
  • πŸ–ΌοΈβž‘οΈπŸ“„ Photos/scans work too β€” FileAnnotator/ImageToPdf wrap an image as a one-page PDF automatically, so it goes through the same annotator
  • πŸ“Ž FileAnnotator.open handles "whatever the user tapped" generically β€” PDF or image, it annotates; anything else, it offers View/Download instead
  • πŸ“„ Add/remove blank pages (mobile/desktop only β€” see Platform notes)
  • ↩️ Undo/redo and a one-tap reset
  • Works on Android, iOS, and Web

Install

dependencies:
  annotation_for_all: ^0.1.0

Quick start

The simplest way β€” push the editor and await the result:

import 'package:annotation_for_all/annotation_for_all.dart';

final bytes = await PdfAnnotator.open(context, pdfFile: myPdfFile);
if (bytes != null) {
  // user saved β€” bytes is the fully annotated PDF
  await File('checked.pdf').writeAsBytes(bytes);
} else {
  // user cancelled without saving
}

On web, pass pdfBytes instead of pdfFile (there's no writable file system in the browser):

final bytes = await PdfAnnotator.open(context, pdfBytes: myPdfBytes);

Grading workflow

enableMarksDialog is off by default β€” apps that aren't grading submissions (a banking app annotating a statement, say) never see this dialog unless they opt in. If you are grading, turn it on and optionally set totalMarks for the "out of N" display; Save will then ask for a mark before handing you both:

await Navigator.push(context, MaterialPageRoute(
  builder: (context) => PdfAnnotatorScreen(
    pdfFile: submissionFile,
    enableMarksDialog: true,
    totalMarks: 100, // optional β€” omit for an open-ended mark with no total shown
    onSaved: (bytes, {enteredMarks}) {
      // enteredMarks is e.g. "87" β€” upload bytes + save the mark however you like
      Navigator.pop(context);
    },
  ),
));

Even with the dialog on, entering a mark is never mandatory β€” it always offers "Skip & Save" alongside "Save & Submit", so the person actually using the app decides whether they want to bother with it.

Turning off tools you don't need

Every capability is an independent flag:

PdfAnnotatorScreen(
  pdfFile: file,
  enableDraw: true,
  enableHighlight: true,
  enableUnderline: true,
  enableImage: false,     // no image insertion
  enablePageManagement: false,
  onSaved: (bytes, {enteredMarks}) { /* ... */ },
)

Choosing which grading marks to show

enabledMarks controls the marks toolbar individually β€” pass just the ones you want (e.g. only Tick and Cross, no number/circle/question mark):

PdfAnnotatorScreen(
  pdfFile: file,
  enabledMarks: {MarkType.rightTick, MarkType.incorrectCross},
  onSaved: (bytes, {enteredMarks}) { /* ... */ },
)

Leave it unset for all five (tick, cross, number, circle, question mark), or pass an empty set ({}) to hide the marks toolbar entirely.

One entry point for "whatever the user tapped"

If you're handling arbitrary attachments and don't know ahead of time whether it's a PDF, an image, or something else entirely, use FileAnnotator.open instead of PdfAnnotator.open:

final result = await FileAnnotator.open(
  context,
  bytes: fileBytes, // or `file: File(...)` off web
  fileName: 'homework.docx',
);

switch (result.handledAs) {
  case FileHandledAs.annotated:
    // it was a PDF, or an image that got wrapped into one β€” result.annotatedBytes
    break;
  case FileHandledAs.viewed:
  case FileHandledAs.downloaded:
    // wasn't a PDF/image β€” the user viewed or downloaded it instead
    break;
  case FileHandledAs.cancelled:
    break;
}

What it does, based on the file's extension:

  • .pdf β†’ opens directly in the annotator.
  • Images (jpg/jpeg/png/gif/bmp/webp) β†’ wrapped as a one-page PDF first (via ImageToPdf.convert, also usable on its own), then opens the same annotator β€” so a photographed/scanned submission gets the exact same drawing/marks/highlight tools as a real PDF.
  • Anything else (docx, xlsx, ...) β†’ there's no in-app renderer for arbitrary file types, so instead of failing silently, the user is asked to View (opens with whatever the OS/browser has registered for that file type) or Download (saves it) β€” nothing is annotated in this case.

Platform notes

  • Web requires a one-time index.html setup step. The PDF viewer renders pages on web using the PDF.js library, which isn't bundled automatically β€” you must add this to your app's web/index.html, inside <body>, before the Flutter bootstrap script:

    <script type="module" async>
      import * as pdfjsLib from 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.9.155/pdf.min.mjs';
      pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.9.155/pdf.worker.min.mjs";
    </script>
    

    Without this, every PDF fails to open on web with a generic "There was an error opening this document" message β€” the bytes are fine, the viewer just has no renderer to hand them to. See syncfusion_flutter_pdfviewer's web docs if you need a different PDF.js version.

  • Page add/remove is mobile/desktop only. There's no writable file system on web, and page management works against a File β€” the flag is silently ignored on web rather than crashing.

  • iOS requires a photo library usage description. The Image tool uses image_picker, and iOS crashes on first use if your app's Info.plist doesn't declare why it needs photo library access. Add this to your own ios/Runner/Info.plist (not something this package can add for you):

    <key>NSPhotoLibraryUsageDescription</key>
    <string>Used to insert a photo into the document you're annotating.</string>
    
  • Syncfusion license: this package renders and edits PDFs using syncfusion_flutter_pdf / syncfusion_flutter_pdfviewer, which are free to use under Syncfusion's Community License for qualifying individuals and small businesses (revenue/team-size thresholds apply) β€” check syncfusion.com/license if your usage might fall outside that. This package can't work without them, so that license's terms carry through to anything that depends on it.

Attribution

The annotation controllers (drawing, highlight, underline, image, save) are adapted from the nextgen_pdf_editor package (BSD-3-Clause, Β© Asint.net). See NOTICE for details. The grading marks feature and the top-level API are original to this package.

License

BSD-3-Clause β€” see LICENSE.

Libraries

annotation_for_all
A standalone Flutter PDF annotation toolkit: freehand drawing, highlight, underline, text boxes, grading marks (tick / cross / circle / question mark / number stamps), and image insertion β€” with no backend or storage assumptions. You get the annotated PDF back as bytes; what you do with them is up to you.