offline_document_viewer 0.1.0
offline_document_viewer: ^0.1.0 copied to clipboard
View PDF, Word, Excel and PowerPoint documents fully offline. Renders on the device with bundled open-source engines — no network, no server, no cloud.
offline_document_viewer #
View PDF, Word, Excel and PowerPoint documents in Flutter — entirely on the device. No server, no conversion API, no network permission. The rendering engines ship inside the package.
DocumentView(source: DocumentSource.file('/path/to/report.xlsx'))
That is the whole minimum. The widget draws the document and nothing else — no app bar, no toolbar, no page counter — so it drops into your design instead of fighting it.
The example app. Everything except the document itself belongs to the app — the package contributes the rendering surface.
Why this exists #
Android has no system component that renders Office documents. The usual workaround is a WebView pointed at Google Docs Viewer or Office Online, which requires an internet connection and sends the document to a third party. Commercial SDKs do work offline, but they are enterprise-priced and closed.
This package takes the third road: open-source engines, bundled, running on the device.
Supported formats #
| Format | Fidelity | Engine |
|---|---|---|
.pdf |
Full | PDFium, native — never touches the WebView |
.docx .docm |
High | docx-preview (patched) |
.xlsx .xlsm |
High | SheetJS + this package's own style layer |
.pptx .pptm |
High | PPTXjs (patched) |
.xls |
Values, no styling | SheetJS |
.csv .tsv |
Full | SheetJS |
.rtf |
Text and formatting | Converted to HTML in Dart |
.doc .ppt |
Text only, labelled | OLE compound file reader, in Dart |
Every one of these is verified rendering on a device, not just in unit tests.
.rtf, .doc and .ppt are handled entirely in Dart — no JavaScript engine
is involved. RTF keeps paragraphs, emphasis, colours, alignment and lists. The
two legacy binary formats have no open-source on-device layout engine, so they
are shown as text and say so: a clear text view beats a half-working fidelity
attempt, and DocumentFormat.fidelity reports textOnly so your UI can too.
What Excel gets right that the underlying engine does not #
SheetJS Community Edition reads cell fills but not borders, fonts or
alignment; those are a Pro feature. This package parses xl/styles.xml,
xl/theme/theme1.xml and the per-sheet style indices itself, so you get
border styles and weights, theme colours with tint and shade, the indexed
palette, bold/italic/underline/strike, super- and subscript, horizontal and
vertical alignment, wrapping, indent, rotated text, merged cells, column
widths, row heights, hidden rows and columns, and frozen panes.
Installation #
dependencies:
offline_document_viewer: ^0.1.0
No asset declarations, no platform channels to register — the engines and fonts travel with the package.
Android #
minSdkVersion 21 or higher.
If your project uses Android Gradle Plugin 9, add this to your app's
pubspec.yaml:dependencies: flutter_inappwebview: ^6.2.0-beta.3The 6.1.x line references a ProGuard file that AGP 9 rejects, which fails the build before your code is compiled. This package's constraint is deliberately left open so you are not forced onto a pre-release when you do not need one.
iOS #
iOS 12 or higher — the floor set by the bundled PDFium and WebView plugins. No
Info.plist entries are required: the package requests no permissions and
opens no network connections.
The bundled example/ app targets iOS 14, because the file picker it uses to
demonstrate the package needs it. That is the example's requirement, not the
package's.
Usage #
With your own chrome #
final controller = DocumentViewController();
@override
void dispose() {
controller.dispose();
super.dispose();
}
DocumentView(
source: DocumentSource.bytes(bytes, name: 'q3-report.pptx'),
controller: controller,
placeholderBuilder: (context) => const MySkeleton(),
errorBuilder: (context, failure) => MyError(failure: failure),
onReady: (info) => debugPrint('${info.unitCount} slides in ${info.renderDuration}'),
);
// elsewhere
await controller.search('revenue'); // '' clears the highlight
await controller.goTo(3);
controller.info?.outline; // headings, sheet names, slide titles
DocumentViewController is a ChangeNotifier. Listen to it for status,
position, searchHits, info and failure.
Sources #
DocumentSource.file(path) // from disk
DocumentSource.bytes(bytes, name: 'a.docx') // already in memory
DocumentSource.asset('assets/manual.pdf') // from your bundle
Bytes are read once and copied into a private workspace, so a stale iOS security-scoped bookmark or a revoked Android URI permission cannot break an already-opened document.
Failures are typed, not strings #
errorBuilder: (context, failure) => Text(switch (failure) {
EncryptedDocumentFailure() => 'This document is password protected',
TooLargeFailure(:final sizeBytes) => 'Too large: $sizeBytes bytes',
UnsupportedFormatFailure(:final extension) => '.$extension is not supported',
_ => 'Could not open this document',
}),
DocumentFailure is sealed, so a switch over it is exhaustive. Each failure
also carries a recovery hint — retry, reselect the file, ask for a password,
open elsewhere — and no user-facing text, because wording and translation
belong to your app.
Validate before you show any UI #
final report = DocumentPreflight.inspect(bytes: bytes, fileName: name);
if (report is DocumentFailure) {
// reject with your own message
}
API #
| Type | Purpose |
|---|---|
DocumentView |
The widget. placeholderBuilder, errorBuilder, onReady, onFailure, renderTimeout |
DocumentViewController |
search(), goTo(), plus status, position, searchHits, info, failure |
DocumentSource |
.file() · .bytes() · .asset() |
DocumentPreflight |
Standalone validation, with its limits exposed as constants |
DocumentFormat |
Detected format, its fidelity and family |
DocumentFailure |
Sealed failure hierarchy with RecoveryHint |
Security #
Documents are untrusted input, and the package treats them that way.
- Preflight runs before any engine sees the bytes: empty files, size caps, entry-count caps, path traversal, and zip bombs. The zip central directory is read by hand so declared sizes are checked without inflating anything.
- No network. The WebView that hosts the Office engines carries a strict
Content Security Policy with
connect-src 'none', and every navigation outside the document's private workspace is refused. Remote references embedded in a document cannot phone home. - No loosened file access.
allowFileAccessFromFileURLsandallowUniversalAccessFromFileURLsstay off. On iOS the document bytes are streamed over the JavaScript bridge rather than fetched, precisely so those flags can remain disabled. - Renderer crashes are contained. On Android an unhandled WebView
renderer-process death takes the whole application down; it is intercepted
and reported as a recoverable
RendererFailure.
How the bridge works #
Android and iOS deliberately diverge:
- Android —
WebViewAssetLoaderserving the workspace from the virtualhttps://appassets.androidplatform.netorigin. Google's own recommendation: a real origin keeps the same-origin policy intact. - iOS —
WKWebView.loadFileURLwith read access scoped to the workspace. A custom scheme handler is not used: on iOS 26, WebKit terminates pages loaded through a custom scheme that contain scripts.
Fitting the screen #
Word pages, slides and spreadsheets are authored at a fixed width that is far wider than a phone. Each is scaled down to the viewport on render, which keeps every relationship in the document intact and leaves pinch-zoom available. Without it a document reads as broken rather than merely zoomed out — columns look misaligned and content appears to be missing.
Very wide spreadsheets stop scaling at 40% and scroll from there, so text never becomes unreadably small.
Size #
Roughly 6 MB compressed. Most of that is fonts, and they are not optional: Office documents size their text boxes using Calibri, Cambria and Arial metrics, which exist on neither Android nor iOS. Without metric-compatible substitutes text overflows its box and fixed-position layouts drift — the difference is immediately visible, not cosmetic.
Known limits #
- Spreadsheets are cut off past 3000 rows rather than exhausting memory;
DocumentInfo.truncatedtells you when that happened. - Charts, conditional formatting, data validation and pivot tables are not rendered.
- PowerPoint does not apply
normAutofit, and long lines can still break mid-word. .xlsgives you values without styling — the legacy format's style data is not readable by the bundled engine.- Password-protected documents are detected and reported, not opened.
Example #
example/ is a small document library: format badges, search, an outline
sheet and error copy, all built on DocumentView. It is the reference for how
the package expects to be used.
cd example && flutter run
Contributing #
Issues and pull requests are welcome. Before opening a PR:
flutter analyze # must be clean, including public_member_api_docs
flutter test # unit tests plus fixture and asset guards
./tool/check_js.sh # parses the bundled JavaScript; needs Node
The bundled engines carry patches; each is marked /* PATCH: */ in the source
and explained in THIRD_PARTY_NOTICES.md and doc/.
If you change one, record it there too — the licences require it.
Licence #
MIT, plus the bundled engines and fonts under their own licences. See THIRD_PARTY_NOTICES.md — if you ship an app with this package, you need to carry those notices. The licence texts are bundled as assets so you can show them from an about screen:
await rootBundle.loadString(
'packages/offline_document_viewer/assets/licenses/LICENSE-liberation',
);