@@ -2325,21 +2325,19 @@ export type DecodeOpts = {
23252325 */
23262326 grid ?: DecodeGrid ;
23272327 /**
2328- * Retry failed decodes with inverted colors (light-on-dark codes, screens)
2329- * and 2x..32x box-downscaled rasters (codes overflowing the frame, moire).
2328+ * Adaptive multi-pass detection.
23302329 *
2331- * Enabled by default: both retries only run after the primary pass fails,
2332- * so images that already decode pay nothing. Set to `false` to disable.
2330+ * - `true` (default): for large frames, detect on a downscaled raster
2331+ * first; retry failed decodes with inverted colors (light-on-dark codes,
2332+ * screens) and 2x..32x box-downscaled rasters (codes overflowing the
2333+ * frame, moire). Extra passes only run after the primary pass fails, so
2334+ * images that already decode pay nothing.
2335+ * - `'max'`: additionally try rotated, cropped, and upscaled fallback
2336+ * candidates — decodes more hard images, but can be several times
2337+ * slower on frames that still fail.
2338+ * - `false`: strict single-pass decode.
23332339 */
2334- invertDownscale ?: boolean ;
2335- /**
2336- * Try rotated, cropped, and upscaled fallback candidates after the primary
2337- * decode attempt fails.
2338- *
2339- * Disabled by default because these retries can be much slower on hard
2340- * failures.
2341- */
2342- moreEffort ?: boolean ;
2340+ multiPass ?: boolean | 'max' ;
23432341 /**
23442342 * Custom byte-to-text decoder used for byte segments.
23452343 *
@@ -2467,7 +2465,7 @@ function cropImageWithOffset(
24672465
24682466// Fallback candidates copy pixels with plain byte moves: a per-pixel
24692467// `data.subarray()` + `set()` allocated millions of temporary views per frame
2470- // and dominated moreEffort profiles (~57% self time on 9MP failures).
2468+ // and dominated multiPass:'max' profiles (~57% self time on 9MP failures).
24712469function copyPixel (
24722470 dst : Uint8Array ,
24732471 dstPos : number ,
@@ -2642,7 +2640,11 @@ function visitFallbackImageCandidates(img: TArg<Image>, visit: CandidateVisitor)
26422640 // from nearest-neighbor upscaling, while large multi-QR scenes need window
26432641 // crops before full-frame rotations. This also avoids allocating three full
26442642 // rotated frames before trying the much smaller windows on >4MP images.
2645- if ( area <= 150_000 ) {
2643+ // Upscaling adds no pixels of information, but it shrinks the binarizer's
2644+ // fixed 8x8 blocks relative to the modules — a cheap stand-in for a
2645+ // finer-grained binarizer. 1MP was measured as the sweet spot on BoofCV
2646+ // (multiPass:'max' 344->353); 2.5MP decodes nothing more and costs 18% extra.
2647+ if ( area <= 1_000_000 ) {
26462648 const found = visitRotatedImageCandidates ( base , ( candidate ) => {
26472649 if ( visit ( scaleNearestCandidate ( candidate , 2 ) ) ) return true ;
26482650 return visit ( scaleNearestCandidate ( candidate , 3 ) ) ;
@@ -2770,8 +2772,8 @@ function decodePreparedImage(
27702772 // color-inverted bitmap (ISO/IEC 18004:2024 §12 b)5 reflectance reversal):
27712773 // light-on-dark codes can fail at any stage after the finder, so the legacy
27722774 // finder-level negate retry inside detectTriples() is not enough.
2773- // `opts.invertDownscale : false` keeps only the legacy finder-level retry.
2774- const fullInvert = options . invertDownscale !== false ;
2775+ // `opts.multiPass : false` keeps only the legacy finder-level retry.
2776+ const fullInvert = options . multiPass !== false ;
27752777 let lastErr : unknown ;
27762778 for ( let polarity = 0 ; polarity < 2 ; polarity ++ ) {
27772779 let triples ;
@@ -2964,12 +2966,12 @@ function validateDecodeOptions(opts: TArg<DecodeOpts>): DecodeOpts {
29642966 const options = validateObject ( opts , 'opts' ) as DecodeOpts ;
29652967 if ( options . cropToSquare !== undefined && typeof options . cropToSquare !== 'boolean' )
29662968 throw new Error ( `invalid opts.cropToSquare=${ options . cropToSquare } ` ) ;
2967- if ( options . moreEffort !== undefined && typeof options . moreEffort !== 'boolean' )
2968- throw new Error ( `invalid opts.moreEffort= ${ options . moreEffort } ( ${ typeof options . moreEffort } )` ) ;
2969- if ( options . invertDownscale !== undefined && typeof options . invertDownscale !== 'boolean' )
2970- throw new Error (
2971- `invalid opts.invertDownscale= ${ options . invertDownscale } ( ${ typeof options . invertDownscale } )`
2972- ) ;
2969+ if (
2970+ options . multiPass !== undefined &&
2971+ typeof options . multiPass !== 'boolean' &&
2972+ options . multiPass !== 'max'
2973+ )
2974+ throw new Error ( `invalid opts.multiPass= ${ options . multiPass } ( ${ typeof options . multiPass } )` ) ;
29732975 if ( options . grid !== undefined ) {
29742976 const grid = validateObject ( options . grid , 'opts.grid' ) as DecodeGrid ;
29752977 asafenumber ( grid . moduleSize , 'opts.grid.moduleSize' ) ;
@@ -3040,7 +3042,7 @@ export function decodeQR(img: TArg<Image | Bitmap>, opts: TArg<DecodeOpts> = {})
30403042 // full-resolution cost; when it fails, the full pipeline below still runs,
30413043 // adding at most ~25% to the failure path.
30423044 let preScale = 0 ;
3043- if ( options . invertDownscale !== false && options . grid === undefined ) {
3045+ if ( options . multiPass !== false && options . grid === undefined ) {
30443046 let img = image ;
30453047 let scale = 1 ;
30463048 while (
@@ -3063,7 +3065,7 @@ export function decodeQR(img: TArg<Image | Bitmap>, opts: TArg<DecodeOpts> = {})
30633065 return decodePreparedImage ( image , options , mapPoint ) ;
30643066 } catch ( e ) {
30653067 if ( isUserDecodeError ( e ) ) throw e . error ;
3066- if ( options . invertDownscale !== false && options . grid === undefined ) {
3068+ if ( options . multiPass !== false && options . grid === undefined ) {
30673069 try {
30683070 const retry = decodeWithDownscale ( image , options , mapPoint , preScale ) ;
30693071 if ( retry !== undefined ) return retry ;
@@ -3072,7 +3074,7 @@ export function decodeQR(img: TArg<Image | Bitmap>, opts: TArg<DecodeOpts> = {})
30723074 throw downscaleError ;
30733075 }
30743076 }
3075- if ( options . moreEffort && options . grid === undefined ) {
3077+ if ( options . multiPass === 'max' && options . grid === undefined ) {
30763078 try {
30773079 const retry = decodeWithFallback ( image , options , mapPoint ) ;
30783080 if ( retry !== undefined ) return retry ;
0 commit comments