Handwriting for stylus tablets. Free, offline-first, open source.
🌐 English · Español
Handwriting app for stylus tablets, in the spirit of GoodNotes or Samsung Notes, built with Flutter. Real pressure sensitivity, palm rejection, notebooks with templates, lasso, handwriting recognition, PDF import and optional Google Drive backup.
Free, ad-free, no accounts and no analytics. Everything lives on your device.
Main platform: Android (tablets) · Secondary: Linux desktop.
Inklus is distributed as an APK on GitHub Releases (it is not on Google Play).
- From the latest release, download the APK for your tablet. Most current tablets use
app-arm64-v8a-release.apk; if it won't install, tryapp-armeabi-v7a-release.apk. - Open it. Android will ask you to allow installing apps from unknown sources for your browser or file manager: grant it for this installation only.
- Updates install the same way, on top of the previous version, without losing your notes.
Before switching devices, export a backup: Settings → Export full backup (or enable Google Drive backup).
- Real pressure modulating stroke width, smoothed with
perfect_freehand. - Full palm rejection: detects the pen touching or hovering nearby, with a grace window and contact-size check. Automatic pen-only mode.
- Automatic eraser with the pen's back end or the S-Pen side button.
- Finger zoom and pan: writing and navigating never interfere.
- Favorite pens (3 + highlighter) with their own color and width, plus recent colors.
| Tool | What it does |
|---|---|
| Ballpoint / Pencil | Uniform stroke or pressure-dependent width |
| Highlighter | Wide, translucent stroke |
| Eraser | Partial (splits the stroke like a real eraser), whole stroke, or highlighter only |
| Lasso | Move, scale, rotate, copy, paste, duplicate, recolor and convert to text (strokes, images and text) |
| Shapes | Hold at the end of a stroke to straighten it: lines, rectangles, triangles, ellipses |
| Ruler and protractor | cm/mm measurements on the page, live angle, strokes snap to the edge |
| Rich text | Bold, italic, underline, alignment, font family and size |
| Laser pointer and magnifier | For presenting or writing in detail |
| Images | Insert from the device, move and resize |
| Template | Behavior |
|---|---|
| Infinite canvas | Blank, grows as you write |
| Sheet | A4-like fixed size, with continuous scrolling between sheets |
| Ruled / Grid | Notebook style |
| Custom template | Upload an image: fixed sheet or infinite fill |
- Library with notebooks, covers, favorites, recents and grid/list view.
- Tags and smart folders.
- Search across all notes, including recognized handwriting (ML Kit, on-device).
- Page bookmarks, go to page and keyboard shortcuts.
- Trash with restore and automatic purge after 30 days.
- Reminders linked to notebooks and writing stats (totals and streaks).
- PDF import to annotate it (one note page per PDF page).
- Export a page or a whole notebook to PNG, PDF, SVG or PPTX (off-screen rendering, independent of zoom).
- Native
.inklusformat: a single file per notebook with the document and its images: shareable and re-importable. - Instant save of every change and local version history.
- Full backup as
.zip, with optional AES-256-GCM encryption (key derived with PBKDF2). - Marketplace of free packs (templates, palettes, stickers) with sha256 verification.
The app is offline-first: it works 100% without an account or network. If you sign in with Google:
- Each notebook is mirrored to an Inklus folder in your Drive as
.inklus, always in the same file. - It uses the
drive.filescope: the app only sees files it created itself. - Restore from Drive brings back the latest version of each note, and the version history also shows revisions saved on Drive.
There are no Inklus servers, analytics or ads. Errors are logged on the device only and you decide whether to share them (Settings → Error log). Details in docs/privacy.md.
- Never in the repo: keystore (
*.jks),android/key.properties,client_secret*.json,google-services.json,.env. All are in.gitignoreand CI (secret-scan, gitleaks) rejects anything that slips through. - Release signing: CI reads
ANDROID_KEYSTORE_BASE64,ANDROID_KEYSTORE_PASSWORD,ANDROID_KEY_ALIASandANDROID_KEY_PASSWORDfrom GitHub → Settings → Secrets. Keep the keystore outside the repository with an encrypted backup: losing it prevents updating already-installed APKs. - Public (not secret): the OAuth client ID and the signing SHA-1 fingerprints; they identify the app, they don't grant access. A
client_secretis secret: the app doesn't need it, don't download it. - If something leaks: rotate it in Google Cloud (Credentials), regenerate the GitHub secret, and rewrite history only if the value is still valid.
A raw Listener receives all pointer events before the gesture arena. While the pen is touching or near the screen, simultaneous touches are discarded: they don't draw, pan or select. Since Listener doesn't compete in the gesture arena, there is no conflict with the zoom/pan GestureDetector.
- Committed layer (template + strokes + images) inside a
RepaintBoundary: the GPU reuses it while only the in-progress stroke changes. - Active layer (in-progress stroke, eraser cursor, selection) on top; repainted every frame without invalidating the committed one. Adding a point is O(1).
- During zoom/pan the canvas is drawn from a snapshot and only the strips uncovered by the gesture are painted live; a crisp repaint happens on release.
- Culling, per-stroke caches, level of detail in templates and encoding of large notes in a separate isolate.
Strokes are stored in zoom-independent coordinates. The transform (scale + translation) only affects the view, so exporting gives the same result at any zoom.
lib/
├── main.dart · app.dart # Entry point, theme and locales
├── models/ # Notebook, Note, Page, Stroke, ImageItem, TextItem, Template
├── logic/
│ ├── canvas_controller.dart # Central state (+ canvas_view / canvas_selection / canvas_layers)
│ ├── stroke_engine.dart # Per-tool smoothing
│ ├── palm_rejection.dart # Palm rejection
│ ├── eraser.dart · lasso.dart # Fine erasing and selection
│ ├── shape_detector.dart # Shape recognition
│ ├── ruler.dart · snap_guides.dart
│ └── undo_stack.dart # Per-page undo/redo
├── services/
│ ├── storage_service.dart # Local persistence (atomic writes)
│ ├── inklus_format.dart # .inklus format
│ ├── export_service.dart # PNG / PDF / SVG (+ pptx_builder.dart)
│ ├── import_service.dart # .inklus and backups
│ ├── pdf_import_service.dart # PDF → annotatable pages
│ ├── drive_sync_service.dart # Google Drive backup
│ ├── backup_crypto.dart # AES-256-GCM + PBKDF2
│ ├── ocr_service.dart # Handwriting recognition (ML Kit)
│ ├── search_service.dart · version_history_service.dart
│ ├── reminder_service.dart · writing_stats_service.dart
│ └── marketplace/ # Pack catalog
├── ui/
│ ├── canvas/ # Canvas, overlays and painter (shared with export)
│ ├── editor/ # Toolbar, popovers, pages panel, history
│ ├── theme/ # Design system (theme, tokens, colors)
│ ├── widgets/ # Shared components
│ └── *_screen.dart # Library, settings, trash, reminders…
└── l10n/ # Translations (es, en, pt-BR, fr, de)
Requires Flutter 3.47 (stable).
flutter pub get
flutter run -d linux # desktop
flutter run -d <tablet> # Android (developer mode / USB)flutter analyze
flutter test # models, storage, canvas, history, translations…
flutter test tool/screenshots/screenshots_test.dart # UI screenshots (build/screenshots)
flutter test integration_test -d <device> # critical flows on tablet/emulatorGitHub Actions CI runs analysis, tests and integration tests on every PR, and publishes signed APKs when a v* tag is created (release notes come from the CHANGELOG).
The icon source is assets/icon/*.svg (→ dart run flutter_launcher_icons). The animated icon and the How it works SVGs are generated, dependency-free, with:
python3 tool/docs_assets/generate.py # → docs/assets/ (icon.svg, icon-animated.svg, icon.png, anim/*.svg)If you build a fork you need your own Google Cloud project (no Firebase or google-services.json required):
- In console.cloud.google.com, create a project and enable the Google Drive API.
- Configure the OAuth consent screen with the scope
https://www.googleapis.com/auth/drive.file. - Under Credentials, create an Android OAuth client (package + your keystore's SHA-1 fingerprint) and a Web client; put the web client ID in
android/app/src/main/res/values/strings.xml(default_web_client_id).Without the SHA-1 fingerprint registered, Google Sign-In returns errorkeytool -list -v -alias androiddebugkey -keystore ~/.android/debug.keystore -storepass android -keypass android10.
Flutter · Dart · Material 3 · perfect_freehand · pdf / printing · archive · google_sign_in + googleapis · cryptography · Google ML Kit (text and digital ink) · share_plus · file_picker · shared_preferences · intl
Contributions are welcome: code, translations, templates for the marketplace, bug reports.
CONTRIBUTING.md— setup, workflow, DCO sign-off ·CODE_OF_CONDUCT.md·SECURITY.md·GOVERNANCE.mddocs/ARCHITECTURE.md·docs/DESIGN_SYSTEM.md·docs/TRANSLATING.md·docs/RELEASING.md·ROADMAP.md
Inklus is free software: you can redistribute it and/or modify it under the terms of the
GNU General Public License v3.0 or later (GPL-3.0-or-later), published by the Free
Software Foundation. See the LICENSE file.
Copyright © 2026 Pedro (@Pedroj-64) and Inklus contributors.
Every source file carries the header // SPDX-License-Identifier: GPL-3.0-or-later.
Any modified version that is distributed must also be released under GPL-3.0 with its source code.