doclens 0.0.12
doclens: ^0.0.12 copied to clipboard
Document scanner for Flutter with native edge detection and a 100% Flutter UI you fully control, plus a one-line escape hatch to the OS-native scanner.
0.0.12 #
- Declared
linuxas a supported platform viadartPluginClass(no native code needed — sameMethodChannelDoclensfallback path web already uses). - Fixed WASM compilation: three widgets and
CanvasLargeDocMergerimporteddart:iounconditionally forFile/Image.file, which brokeflutter build web --wasm. Routed them through the existing conditionalfallback/file_io.dart(io/stub split) instead.
0.0.11 #
- Declared
web,macos, andwindowsas supported platforms (flutter.plugin.platformsinpubspec.yaml, registering the existingMethodChannelDoclensfallback path on web and the nativeDoclensPluginon macOS/Windows). No behavior change on web — the pure-Dart warp/rotate/detect fallback and theScannerUnavailableExceptionmessaging already worked there; this just stopsflutter build web/ pub.dev from warning that the plugin doesn't support these platforms.
0.0.10 #
- New
DoclensController.autoCapturegetter/setter — toggle auto-capture live instead of rebuilding the controller (was locked toScannerConfig.enableAutoCaptureat construction, forcing a dispose + reinit and ~400ms black preview per toggle). @bohdan-ilkiv-plant-in - Fixed: detection stream could go silent if
dispose()landed mid-initialize()— now subscribes to the stream before starting the native session. iOS also dropped an unconditionaleventStream.reset()indisposethat could kill a sink an incoming controller was already using. @bohdan-ilkiv-plant-in
0.0.9 #
Web & desktop support — the package no longer hard-fails off mobile
- The pure-compute half of the pipeline now has a pure-Dart fallback built
on
dart:ui, so the import → edit-corners → warp flow works on desktop (and as a safety net anywhere the native plugin is missing) instead of throwing an opaqueMissingPluginException.warpImagedoes a true perspective (homography) dewarp,rotateImagerotates,detectInImagereports the image size with anullquad (seeding a manual crop), andrecognizeTextreturns an empty result. - Graceful degradation — camera/native-only methods (
initialize,capture, flash/focus/zoom,scanWithNativeUI) now surface a clearScannerUnavailableExceptionoff Android/iOS rather than a raw plugin error. - Capability flags —
DoclensPlatform.supportsLiveScanandDoclensPlatform.supportsImportFlowlet you branch your UI without try/catch. - New building blocks exported for web (where the path-based API can't run):
PerspectiveWarp.warpBytes(bytes, quad)andImageEnhance.apply(image, enhancement)operate purely on bytes /ui.Image. - Fallback output is PNG (lossless —
jpegQualityignored) andautoOrientationis a no-op off-device (needs on-device OCR). See the new Platform support section in the README for the full capability matrix.
Dark documents now detect on Android — detectionPolarity
- The Android detector segmented every frame by keeping the brighter side
of the frame's mean luma, so it only ever found documents lighter than
their surroundings. A dark ID card, a saturated trading card, or a glossy
photo on a light desk lost the component search to the background and left
DetectionStatusatsearching/noPaper. iOS was unaffected — Apple Vision is contrast-agnostic. - New
ScannerConfig.detectionPolarity(DetectionPolarity.brighter,.darker,.auto).brighteris the default and is exactly the previous behavior;darkermirrors the threshold for documents darker than their background;autoruns both polarities per frame and keeps whichever candidate looks more document-like — solid, clear of the frame edges, and large — for roughly twice the per-frame detection cost. - New
ScannerConfig.detectionThresholdOffset(default20, range0–128) exposes the luma bias that was hardcoded in the detector. Lower it when a document barely separates from its background, raise it to keep shadows and highlights out of the document's component. - Both settings travel with
DoclensController.detectInImage(path)too, so gallery imports segment the way the live preview does. - Additive and default-preserving: existing apps behave exactly as before. Both are Android-only and are ignored on iOS.
- Note for anyone implementing
DoclensPlatformthemselves:detectInImagegained an optionalconfigparameter, so an override with the old signature needs the new parameter added. Callers are unaffected. - Fixed:
DoclensScreendroppedenableSharpnessGate,sharpnessFloor, andautoCaptureFocusTimeoutwhen merging its top-level overrides into a suppliedScannerConfig, so those three fell back to their defaults on that entry point.
0.0.8 #
- Swift Package Manager support. CocoaPods still works — no migration needed.
0.0.7 #
Large-document scan — capture a document too big for one frame
- New
DoclensLargeDocScreenlets the user build one document out of several overlapping captures: shoot a fragment, tap a+on any edge of the growing composite, and line the next shot up against a translucent overlap ghost of the previous piece. A miniview shows the whole document taking shape; Done stitches the pieces into a single image. Supports long pages (a row/column), L-shapes, and 2×2 blocks. - True to the package's "you own the UI" stance, every piece of chrome is
overridable —
plusButtonBuilder,miniviewBuilder,hintBuilder,captureButtonBuilder, plusaccentColor,overlapFraction, andghostOpacity. The capture pipeline, state machine, grid model, alignment, and stitching are all injectable. - Underlying pieces are exported for custom UIs:
LargeDocCanvas(2-D grid placement),LargeDocSession(the capture→review→merge state machine),LargeDocAligner(overlap-band correction; defaultManualPlacementAlignertrusts the hand alignment), andLargeDocMerger(CanvasLargeDocMergerpastes pieces at their grid slots — seam feathering is a future step).
Gallery import — detect → edit → warp on an existing photo
- New
DoclensController.detectInImage(path)(and the underlyingDoclensPlatform.detectInImage) runs the native edge detector — the same one that powers the live preview — on a still image already on disk, such as one the user imported from the gallery. It returns anImageDetectionwith the detected quad in normalized[0,1]coords (ornullwhen nothing document-like is found) plus the image's EXIF-upright pixel size. - This makes the package's whole pipeline available without the camera: feed
ImageDetection.imageSizeandImageDetection.quadIntoEditCornersScreen, thenwarpImagewith the user-adjusted corners.ImageDetection.quadInfalls back to a 10%-inset rectangle so there are always draggable corners to start from. It's a pure file operation — noinitialize()/ camera session required. - The example app gains a "Gallery import" entry demonstrating the full detect → edit → warp flow on a picked photo.
0.0.6 #
Sharpness-gated auto-capture
- Auto-capture now waits for the frame to be in focus before firing, not
just geometrically aligned, so it no longer shoots a well-framed but
blurry page. When a document aligns, the controller locks focus on the
quad's centroid and holds the shutter until sharpness clears threshold.
The new
DetectionStatus.focusingreports this state so the UI can show a "focusing, hold steady" hint, and the drop-in scanner already does. - Focus is judged from a per-frame variance-of-Laplacian sharpness value
via the new
SharpnessTracker. A fixed threshold doesn't work here, since the numbers swing with scene and distance, so a frame must clear an absolute floor and then plateau near the top of a short rolling window. The native side (iOS and AndroidSharpnessEstimator) measures the in-quad sharpness per frame and reports it on the newDetectionEvent.sharpnessfield;nullmeans "no signal" and never blocks capture. - Configurable via
ScannerConfig:enableSharpnessGate(defaulttrue; setfalsefor the old geometry-only behavior),autoCaptureFocusTimeout(default 2500 ms, after which capture fires anyway so nobody gets stuck), andsharpnessFloor(default8.0). Losing alignment resets the focus episode so the next alignment re-focuses.
Edit-corners magnifier loupe
-
EditCornersScreennow shows a magnifying loupe while a corner is dragged, so the finger no longer hides the point being placed. Configurable viashowMagnifier(defaulttrue),magnifierSize, andmagnifierScale, with an optionalmagnifierBuilderto supply a custom loupe widget (returnnullto fall back to the bundled one). -
New
ScanResult.copyWithfor persisting an updatedcroppedImagePath(e.g. after a manual rotate or re-warp) back into a result you're holding.
On-device OCR — new recognizeText API
- New
recognizeTextruns full on-device text recognition over any image on disk (typically a capture'scroppedImagePath) and returns the transcript plus structured geometry. Available onDoclensController.recognizeText(path)and directly onDoclensPlatform.instance.recognizeText(imagePath: …)— it is a pure file operation, so it needs no camera session orinitialize()call. - New result types:
OcrResult(text,blocks, flattenedlines,imageSize),OcrBlock(text, pixel-spaceboundingBox,lines, and on Android arecognizedLanguage), andOcrLine(text,boundingBox,confidence). All bounding boxes are in the recognised image's pixel coordinates (origin top-left). A blank/graphical page (or an unavailable recogniser) yields an emptyOcrResultrather than an error. - Reuses the OS text APIs already present on each platform — Apple Vision's
VNRecognizeTextRequest(run at the.accuratelevel) on iOS and Play-services ML Kit text recognition on Android — so no model is bundled and no new dependency is added; the Android model is delivered on demand by Google Play services. OCR was previously listed as a non-goal; it is now supported.
0.0.5 #
Multi-page / batch scanning — new DoclensMultiScreen
- New drop-in
DoclensMultiScreen, the batch sibling ofDoclensScreen, with the same two usage styles:DoclensMultiScreen.scan(context)pushes a route and returnsFuture<List<ScanResult>?>(ornullif cancelled);- mount the widget directly and receive the pages via
onComplete(the batch analogue ofDoclensScreen.onCapture).
- The user captures any number of pages without leaving the camera and taps "Done" to finish. Each page still flows through the same review screen (retake / edit corners / accept).
- The live preview grows a thumbnail rail of captured pages, a page-count chip, and a "Done" button; the review screen's accept button reads "Add".
- Tapping the rail opens a full-screen page manager to reorder (drag) and delete pages. Closing a session with uncommitted pages — via the close button or system back — prompts a discard confirmation.
- Optional
maxPagescap, plus configurable labels (addPageLabel,doneLabel), discard-dialog strings, and anonPagesChangedcallback. - Every behaviour/UI knob from
DoclensScreen(enhancement, auto-orientation, flash, overlay style, …) carries over. (Multi-page mode is also available onDoclensScreenitself via themultiPageflag, whichDoclensMultiScreenwraps.) - The post-capture review now returns the edited
ScanResultwhen the user adjusts corners before accepting (previously the pre-edit result was returned).DoclensReviewScreenpops aScanResult?instead of abool. - Scratch images are now cleaned up instead of accumulating in the temp directory: a retaken/cancelled capture, a crop superseded by edit-corners, a page deleted from a batch, and a discarded multi-page session all delete their backing files. Files for pages you keep (returned from the scanner) are never touched — the caller owns them.
Auto-orientation (upright) for the cropped output, plus a manual rotate API
- New
ScannerConfig.autoOrientation(and matchingDoclensScreenparameter):none(default, unchanged behaviour) orauto, which detects the captured page's dominant text direction on-device and rotates the crop in 90° steps so it reads upright. A blank or purely graphical page (no confident text) is left untouched.- iOS: Apple Vision's
VNRecognizeTextRequest(no bundled model). - Android: Play-services ML Kit Latin text recognition, delivered on demand —
exactly like
scanWithNativeUI's document scanner; no model bundled in the host APK.
- iOS: Apple Vision's
- Applies to both the capture's cropped output and re-warps via
EditCornersScreen(it travels onScannerConfigand thewarpImagechannel call). The raw image is never rotated. - New
DoclensController.rotateImage(path, quarterTurns)(androtateImagechannel method) for a manual rotate control —quarterTurnsis clockwise and normalized modulo 4. Writes a new file; the source is left untouched.
Image enhancement & shadow removal on the cropped output
- New
ScannerConfig.imageEnhancement(and matchingDoclensScreenparameter) applies a post-warp filter to the cropped document:none(default, unchanged behaviour),grayscale(plain desaturate),enhanced(shadow-corrected colour "magic colour"), orblackAndWhite(shadow-corrected near-bitonal — best for OCR on faint text). enhancedandblackAndWhitegenuinely remove uneven lighting and soft shadows via on-device illumination-division ("flatten"), not just global contrast. No model is bundled and no extra dependency is added.- iOS: Apple's
CIDocumentEnhancer(iOS 16+) with aCIHighlightShadowAdjustfallback on older OSes;blackAndWhitedesaturates then binarises withCIColorThresholdOtsu. - Android: background estimated from a heavily downscaled copy and divided
out per pixel;
blackAndWhiteuses adaptive-mean thresholding.
- iOS: Apple's
- Applies to both the capture's cropped output and re-warps performed via
EditCornersScreen(it travels on the controller's config and thewarpImagechannel call). The raw image is never modified.
Android: crop lands in the wrong position after editing corners
- On Android, large captures are decoded downscaled (
decodeDownscaled, max 3000 px), sorawImageSizeand the reported quad are in that downscaled pixel space. When no EXIF rotation was needed, capture returned the original full-resolution file asrawImagePathwhile those coordinates described the downscaled image. A later re-warp viaEditCornersScreendecoded that file at full resolution and applied the half-scale quad, cropping the wrong region (typically the top-left quadrant). Capture now always persists the upright bitmap it measured, sorawImagePath's pixel dimensions matchrawImageSizeand the quad.
0.0.4 #
Resume grace window prevents immediate re-capture
DoclensController.resume()now resets stability tracking and suppresses auto-capture for 1 200 ms (resumeAutoCaptureGrace) after a resume. Without this, a document still aligned in frame from before the pause would re-trip auto-capture within a frame or two of resuming, giving the user no chance to reposition after a retake.- Any in-progress confirmation phase is also cancelled on resume so the two-stage capture timer starts fresh.
0.0.3 #
Android preview no longer stretches
- The live preview now reports its size in the rotated (displayed)
orientation instead of the sensor-natural landscape buffer size, so a
portrait preview fills a portrait screen without
BoxFit.coverstretching it. Driven off CameraX'ssetTransformationInfoListener, with identical sizes deduped and 0x0 events dropped so a stale or repeat emission can't corrupt the layout.
Overlay shows from the first frame
DoclensViewnow paints the quad overlay during the brief window between the first camera frame and the firstpreviewSizeevent. Previously the overlay was missing for that window; the corners are normalized[0,1]so they align to the texture rect immediately.
Primary button
- The default primary button in
DoclensScreenis now icon-only (forward arrow), dropping the inline label + icon row for a cleaner control.
0.0.2 #
EditCornersScreen
- AppBar styled with white foreground, no elevation, and weighted title text.
- Bottom buttons are now full-width (
Expanded) with 12 px gap between them. onSavereturn value (warped image path) is passed back viaNavigator.pop.- New parameters:
resetLabel,saveLabel,savingLabel— customise button text without supplying a fullbuttonBuilder. - New parameters:
buttonStyle(ButtonStyle?) andbuttonTextStyle(TextStyle?) — style the default buttons without a custom builder.
0.0.1 #
Initial release.
Drop-in scanner
DoclensScreen.scan(context)— one-line, full-screen scanner route with live preview, auto-capture, and a built-in review screen (retake / edit corners / accept). ReturnsFuture<ScanResult?>.- Every visible string and behaviour knob (auto-capture timing, JPEG quality, flash mode, lens, accent colours, labels, edit-corners toggle) exposed as a top-level parameter with rich dartdoc.
Custom UI
DoclensViewFlutter widget rendering aTexture-backed live camera preview plus your overlay / shutter / flash button builders. Every slot acceptsnullto render nothing, a static default for quick start, or a custom widget.DoclensController extends ChangeNotifierowning the session. Streams:quadStream,statusStream,autoCaptureStream,lowLightStream,previewSizeStream. Methods:initialize,capture,warpImage,focusAt,setFlashMode,cycleFlashMode,switchCamera,pause,resume,dispose.QuadOverlayfamily of pre-built overlay widgets with named constructors:outline,filled,corners,cornersFilled,dots,dotsLine,glow. Status-driven colour followsaccent/warning. The drop-inDoclensScreenexposes anoverlayStyle: QuadOverlayStyleparameter so consumers can switch the look without writing a builder.
Native detection pipeline
- iOS —
VNDetectDocumentSegmentationRequest(the Core ML detector used by VisionKit) on iOS 15+, with a docs-tunedVNDetectRectanglesRequestfallback on iOS 13/14. - Android — pure-Kotlin Sobel + connected-components + convex-hull approximation on CameraX (no OpenCV, no on-device ML model bundling on the live-preview path).
- Streams normalised
Quadto Dart at a configurable throttle rate (default 15 Hz). - Two-stage auto-capture with
DetectionStatus.confirmingphase, configurable thresholds and durations.
Focus
- Continuous autofocus enabled by default on both platforms (iOS
.continuousAutoFocus+ near-distance hint; Android CameraXCONTROL_AF_MODE_CONTINUOUS_PICTURE). - Tap-to-focus on the preview triggers a one-shot focus + auto-exposure
at the tap point, with a focus-reticle animation. Reverts to
continuous AF after ~3 seconds. Programmatic access via
controller.focusAt(Offset).
Capture + warp
- Full-resolution still capture with native perspective warp
(
CIPerspectiveCorrectionon iOS,Matrix.setPolyToPolyon Android), EXIF orientation baked into pixel layout. - Graceful fallback when warp fails —
ScanResult.warpErroris surfaced, raw image and quad still returned. EditCornersScreenwith draggable handles, customisable builders, and re-warp callback.
Quality of life
- Median-of-N corner smoothing (
QuadSmoother) to kill single-frame jitter. - Flash / torch toggle, camera switching, pause/resume on app lifecycle.
- Low-light detection emitted on the status stream.
- Preview-size event so the overlay coordinate space always matches the rendered preview pixels.
ScannerConfigwith feature flags for auto-capture timing, smoothing, detection throttle, JPEG quality, flash mode, lens, lifecycle, telemetry, tap-to-focus, pinch-to-zoom — all with sensible defaults.- Typed exceptions:
ScannerPermissionException,ScannerUnavailableException,ScannerInitializationException,ScannerCaptureException.
OS-native scanner (one-line opt-in)
scanWithNativeUI()launches the OS-native scanner UI:VNDocumentCameraViewControlleron iOS,GmsDocumentScanner(Google Play services) on Android.- Returns the cropped page image paths or
nullon user cancel, consistently on both platforms. - Pre-launch Google Play services check on Android with
ScannerUnavailableExceptionon devices without GMS.
Tests + docs
- Pure-Dart unit tests for
Quad,StabilityTracker,StatusClassifier, andQuadSmoother. doc/architecture.mdwith the Dart ↔ native pipeline diagram.doc/decisions.md— every non-obvious design choice cited against Apple / Google docs.