document_scan 0.4.0
document_scan: ^0.4.0 copied to clipboard
Offline document edge detection for Flutter — returns corner coordinates from live camera frames and photos. A headless engine: you own the camera and the UI.
0.4.0 #
A crash fix and a realtime-path optimisation. No API change — every existing call still compiles — but the Android frame path does less work per frame, so this is a minor bump rather than a patch.
-
Realtime YUV frames stop paying for colour nothing reads. Detection is a grayscale pipeline, and a YUV420 frame's Y plane already is that grayscale, but the frame path was manufacturing colour anyway and discarding it one step later. Per 1080p frame, Android built a ~3 MB NV21 buffer, ran
cvtColor(YUV2BGR_NV21)into a ~6 MB three-channel Mat, and thendetectBestQuadimmediately converted it back to gray. On top of that, Dart marshalled the U and V planes — roughlywidth * height / 2bytes — across the platform channel every frame, to a native side that never read them (iOS streams BGRA and never touched them at all).The Y plane now goes straight into a single-channel Mat, honouring
yRowStride, and chroma never leaves Dart.ScanInput.yuvFramestill takes the three planes your camera hands you — they just stop at the channel boundary, so a future colour-aware detector can use them without breaking anyone. -
Detection no longer kills the process on an arm64 Android emulator. Pointing the realtime screen at anything — or running any detection at all — crashed the app with
SIGILL / ILL_ILLOPCinside OpenCV. The faulting instruction iscnth, an SVE instruction: an arm64 AVD running on an Apple Silicon host advertisessve2in/proc/cpuinfo, but the host CPU does not implement SVE, so the first SVE instruction is illegal. The SVE kernels come from KleidiCV, the ARM HAL that OpenCV bundles, which does its own CPU feature detection —OPENCV_CPU_DISABLEcannot reach it and no kill switch exists. Pinningorg.opencv:opencvto 4.11.0 (KleidiCV 0.3.0, before the SVE2 kernels landed in 0.5.0) is the only lever that works; 4.12, 4.13, 4.14 and even 5.0 all still crash.Physical devices were never affected — real hardware reports its CPU features honestly — so this changes nothing for shipped apps. It changes everything for anyone evaluating the package on a Mac.
0.3.1 #
Discovery and positioning only — no API or behaviour changes, no code changes.
- Retune
topicsfor how people actually search.edge-detection(nothing in this niche claims it) andcameraandpdf(both real capabilities here, and the topics competitors browse) replacescanner(redundant withdocument-scanner),computer-vision, andopencv— the last of which drew the wrong audience, since someone searching "opencv" wants a binding, and it described only the Android half anyway. - Lead the description with "document edge detection" rather than "headless". Ranking first for a phrase nobody types is not discovery.
- README: the "How it compares" table gained a third camp. Offline classical-CV
scanners you can style now exist, so "offline" and "no Play Services" no
longer separate this package. What does: it ships no widgets and has no
cameradependency. - Verified against what a fresh install resolves today —
image4.10.1 andpdf3.13.1 — including the EXIF-orientation contract the crop depends on. The existing constraints already allowed them; nothing had tested them.
0.3.0 #
Three behaviour fixes. No API breaks — every existing call still compiles — but scans, filters and auto-capture signals change, so this is a minor bump rather than a patch.
- The perspective correction is now an actual perspective correction.
DocumentProcessor.cropmapped the output to the source with a four-corner bilinear blend, which pins the corners exactly and gets everything between them wrong: it has no perspective division, so the foreshortening a tilted page introduces was never undone, only stretched out linearly. On a page photographed at ~50°, ten equal bands came back on a ramp — 32 px to 54 px against an ideal 42.5 — bending text spacing and hurting OCR. Replaced with the projective transform (homography) this always should have been; the same scene now recovers 42–43 px bands. Degenerate quads fall back to the old bilinear map, and non-finite coordinates are clamped, so a bad quad still yields a bad crop rather than a crash. ScanFilter.blackWhitenow actually binarizes. It applied a contrast + brightness boost and left a full range of greys behind, contradicting both its own dartdoc andenhance's ("without the hard binarization ofblackWhite"). It now thresholds every pixel to ink or paper with Otsu's method — one global cut, the fast, crisp choice on an evenly-lit page, withmagicColorstill the per-region option for shadows and gradients. If you were on 0.2.x and wanted whatblackWhiteactually did, switch toScanFilter.enhance: grayscale plus a contrast boost, which is the same family of result and keeps its greys.- Auto-capture fires once per hold again. While latched,
AutoCaptureAnalyzer.addEventechoedreadyback on everyDetectionSkipped, soshouldCapturere-fired on dropped frames — anddetectStream(minInterval:)makes dropped frames the common case, turning one steady hold into a burst of captures. A skip now reportsdetecting(orsearchingwhen nothing has been seen) while still holding the accumulated steadiness. DocumentScanner.detectCornerstakes asensitivityargument, likescan,detectanddetectStreamalready did. It defaults tobalanced— for corners you're about to show, a wrong quad the user must drag back is worse than no quad.- Example: the home-menu test asserted five demo rows and has been failing since the camera-capture flow made it six. It now checks the titles themselves, so a new screen that isn't wired into the menu fails loudly instead of silently.
- Docs: corrected the README filter table (
magicColorwas described as "brightened, saturated color"), the_orderByAngletie-break description, the claim thatDocumentProcessor"works anywhere Flutter runs" (background: trueneedsIsolate.run, which web lacks), and noted that a background crop copies aBytesScanInput's buffer while aFileScanInputcarries only a path.
0.2.3 #
- Declare the logo under
screenshots:so it renders as the pub.dev listing thumbnail (0.2.2 shipped the image but didn't register it).
0.2.2 #
Visual identity only — no API or behaviour changes.
- Add a package logo, shown as the pub.dev listing thumbnail.
- Add a landscape cover image for link/social previews.
0.2.1 #
Docs & discoverability only — no API or behaviour changes.
- Add runnable code examples to the dartdoc of
DocumentScanner.scan,DocumentDetector.detect, andDocumentDetector.detectStream, so the API reference stands on its own without jumping back to the README. - Sharpen the package description and README to lead with what sets it apart — a headless engine that hands you the document's corner coordinates, offline, rather than a fullscreen scanner UI.
- Retune
topicsfor discovery. - Fix a broken link in the API reference: a dartdoc reference pointed at
VNRectangleObservation, an iOS-only Swift type with no Dart symbol, so it rendered as a dead link on theconfidencedocs. Now plain code text.
0.2.0 #
DetectionSensitivity(strict/balanced/lenient) — a portable control over how eagerly a rectangle counts as a document, ondetect,detectStream, andscan. The level is the contract; each native engine maps it to its own thresholds (Vision confidence + min-size on iOS; contour-area, adaptive-Canny band, corner-angle, and score floor on Android). Context defaults:detectStream→strict,scan→lenient,detect→balanced.detectStream(minInterval:)— cap the detection rate so a 30–60 fps camera doesn't run native detection on every frame (which heats the device without a smoother overlay). Frames that arrive too soon are dropped asDetectionSkippedbefore reaching the engine.- Android detection reworked for recall on solid / low-contrast documents: a
multi-strategy candidate generator (adaptive Canny + close, adaptive-threshold
silhouette, Triangle auto-threshold) pooled through
convexHull+ anapproxPolyDPepsilon sweep, then scored by rectangularity + right-angle-ness instead of taking the first/largest quad.RETR_EXTERNAL, a max-area cap, and a per-level score floor reject the whole-frame false capture and background clutter that a single Canny pass either missed or over-triggered on. - Example: a camera-capture flow (live preview → shutter → scan → result), the realtime flow now grabs a full-resolution still on auto-capture and shows the scanned result (with an auto/manual toggle), a shared pinch-zoomable result screen, tap-to-inspect multi-page thumbnails, and a working PDF export via the system share sheet.
0.1.1 #
DocumentProcessor.pagesToPdf(List<ScannedDocument>)— combine several scanned pages into one multi-page PDF (one A4 page per scan, in order). The multi-page counterpart tooutput: ScanOutputFormat.pdf; pass the pages you collected in aScanSession. Each page's bytes are embedded as-is (no re-encode); returns null for an empty list.
0.1.0 #
Initial release.
DocumentScanner— one-call scanning:scan(input)detects and crops; passcorners:to skip detection and crop user-adjusted corners instead.DocumentDetector— document corner detection for still images (detect) and live camera frames (detectStream), backed by Apple Vision on iOS and OpenCV on Android, with per-frame downscaling for realtime performance.detectStreamemits a sealedDetectionEvent(DetectionSuccess/DetectionEmpty/DetectionSkipped/DetectionError) so a consumer can tell an empty scene from a backpressure drop from a native error, rather than a blindnull. Corners carry aconfidence(engine value on iOS, geometric heuristic on Android).CornerStabilizer— optional EMA smoothing for the live corner stream, so an overlay drawn fromdetectStream(stabilize: ...)stays steady instead of jittering; snaps rather than slides when the document jumps.DocumentProcessor— pure-Dart perspective correction (crop) withgrayscale,enhance,blackWhite,sharpen, and adaptivemagicColorfilters. Output as PNG, JPEG, or a single-page PDF viaScanOutputFormat. Undecodable input (and a degenerate/1px warp) returnsnullrather than throwing.AutoCaptureAnalyzer— pure-Dart stream analyzer that signals when a document has been held steady and confident long enough to auto-capture. Pipe the detector straight in withbindEvents(detectStream(...))(orbindCorners/addEventfor finer control) — the event distinctions are preserved.DocumentProcessor.cropcaps the warp output's long side atmaxDimension(default 2000px, aspect-preserving; passnullto warp at full resolution) so a near-full-frame high-megapixel photo doesn't produce a needlessly huge scan.- Off-UI-thread crop:
crop/applyFiltertake abackgroundflag (defaultfalse) that runs the pure-Dart warp+filter on a background isolate; theDocumentScanner.scanfaçade defaults it totrue, so the simple path never janks the UI without the caller reaching forIsolate.run. ScanSession— an immutable multi-page container (add / reorder / remove).- Plugin-free value types with value equality:
ScanInput(with format-specificbgraFrame/yuvFrameframe factories),DocumentCorners(geometry-ordered corners + confidence),ScannedDocument,ScanFilter,ScanOutputFormat(png/jpeg/pdfconstants, plusjpegAt(quality)). - Example app with four flows: gallery scan, realtime overlay (detectStream +
CornerStabilizer + AutoCaptureAnalyzer over a live camera), manual corner
edit (drag-adjust → crop with
corners:), and reprocess (detect once, swap filters live). PlainStatefulWidget, no state-management dependency.
