Skip to content

Commit b9ee6ef

Browse files
committed
Add support for inverted color QR codes (mebjas#94)
1 parent 1a857a7 commit b9ee6ef

File tree

1 file changed

+91
-72
lines changed

1 file changed

+91
-72
lines changed

src/html5-qrcode.ts

Lines changed: 91 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ export class Html5Qrcode {
268268
private readonly qrcode: RobustQrcodeDecoderAsync;
269269

270270
private shouldScan: boolean;
271+
private flippedScan: boolean;
271272

272273
// Nullable elements
273274
// TODO(mebjas): Reduce the state-fulness of this mammoth class, by splitting
@@ -280,7 +281,7 @@ export class Html5Qrcode {
280281
private borderShaders: Array<HTMLElement> | null = null;
281282
private qrMatch: boolean | null = null;
282283
private renderedCamera: RenderedCamera | null = null;
283-
284+
284285
private foreverScanTimeout: any;
285286
private qrRegion: QrcodeRegionBounds | null = null;
286287
private context: CanvasRenderingContext2D | null = null;
@@ -308,15 +309,15 @@ export class Html5Qrcode {
308309
*
309310
* TODO(mebjas): Deprecate the verbosity boolean flag completely.
310311
*/
311-
public constructor(elementId: string,
312+
public constructor(elementId: string,
312313
configOrVerbosityFlag?: boolean | Html5QrcodeFullConfig | undefined) {
313314
if (!document.getElementById(elementId)) {
314315
throw `HTML Element with id=${elementId} not found`;
315316
}
316317

317318
this.elementId = elementId;
318319
this.verbose = false;
319-
320+
320321
let experimentalFeatureConfig : ExperimentalFeaturesConfig | undefined;
321322
let configObject: Html5QrcodeFullConfig | undefined;
322323
if (typeof configOrVerbosityFlag == "boolean") {
@@ -326,7 +327,7 @@ export class Html5Qrcode {
326327
this.verbose = configObject.verbose === true;
327328
experimentalFeatureConfig = configObject.experimentalFeatures;
328329
}
329-
330+
330331
this.logger = new BaseLoggger(this.verbose);
331332
this.qrcode = new Html5QrcodeShim(
332333
this.getSupportedFormats(configOrVerbosityFlag),
@@ -336,6 +337,7 @@ export class Html5Qrcode {
336337

337338
this.foreverScanTimeout;
338339
this.shouldScan = true;
340+
this.flippedScan = false;
339341
this.stateManagerProxy = StateManagerFactory.create();
340342
}
341343

@@ -390,7 +392,7 @@ export class Html5Qrcode {
390392
if (!internalConfig.isMediaStreamConstraintsValid()) {
391393
this.logger.logError(
392394
"'videoConstraints' is not valid 'MediaStreamConstraints, "
393-
+ "it will be ignored.'",
395+
+ "it will be ignored.'",
394396
/* experimental= */ true);
395397
} else {
396398
videoConstraintsAvailableAndValid = true;
@@ -413,8 +415,8 @@ export class Html5Qrcode {
413415
Html5QrcodeScannerState.SCANNING);
414416
return new Promise((resolve, reject) => {
415417
const videoConstraints = areVideoConstraintsEnabled
416-
? internalConfig.videoConstraints
417-
: $this.createVideoConstraints(cameraIdOrConfig);
418+
? internalConfig.videoConstraints
419+
: $this.createVideoConstraints(cameraIdOrConfig);
418420
if (!videoConstraints) {
419421
toScanningStateChangeTransaction.cancel();
420422
reject("videoConstraints should be defined");
@@ -566,7 +568,7 @@ export class Html5Qrcode {
566568
if (childElement) {
567569
this.element.removeChild(childElement);
568570
}
569-
};
571+
};
570572

571573
let $this = this;
572574
return this.renderedCamera!.close().then(() => {
@@ -637,7 +639,7 @@ export class Html5Qrcode {
637639
: Promise<Html5QrcodeResult> {
638640
if (!imageFile || !(imageFile instanceof File)) {
639641
throw "imageFile argument is mandatory and should be instance "
640-
+ "of File. Use 'event.target.files[0]'.";
642+
+ "of File. Use 'event.target.files[0]'.";
641643
}
642644

643645
if (isNullOrUndefined(showImage)) {
@@ -841,7 +843,7 @@ export class Html5Qrcode {
841843
private getRenderedCameraOrFail() {
842844
if (this.renderedCamera == null) {
843845
throw "Scanning is not in running state, call this API only when"
844-
+ " QR code scanning using camera is in running state.";
846+
+ " QR code scanning using camera is in running state.";
845847
}
846848
return this.renderedCamera!;
847849
}
@@ -882,7 +884,7 @@ export class Html5Qrcode {
882884
Html5QrcodeSupportedFormats.UPC_EAN_EXTENSION,
883885
];
884886

885-
if (!configOrVerbosityFlag
887+
if (!configOrVerbosityFlag
886888
|| typeof configOrVerbosityFlag == "boolean") {
887889
return allFormats;
888890
}
@@ -893,7 +895,7 @@ export class Html5Qrcode {
893895

894896
if (!Array.isArray(configOrVerbosityFlag.formatsToSupport)) {
895897
throw "configOrVerbosityFlag.formatsToSupport should be undefined "
896-
+ "or an array.";
898+
+ "or an array.";
897899
}
898900

899901
if (configOrVerbosityFlag.formatsToSupport.length === 0) {
@@ -962,7 +964,7 @@ export class Html5Qrcode {
962964
const validateMinSize = (size: number) => {
963965
if (size < Constants.MIN_QR_BOX_SIZE) {
964966
throw "minimum size of 'config.qrbox' dimension value is"
965-
+ ` ${Constants.MIN_QR_BOX_SIZE}px.`;
967+
+ ` ${Constants.MIN_QR_BOX_SIZE}px.`;
966968
}
967969
};
968970

@@ -1013,7 +1015,7 @@ export class Html5Qrcode {
10131015
// Alternatively, the config is expected to be of type QrDimensions.
10141016
if (qrboxSize.width === undefined || qrboxSize.height === undefined) {
10151017
throw "Invalid instance of QrDimensions passed for "
1016-
+ "'config.qrbox'. Both 'width' and 'height' should be set.";
1018+
+ "'config.qrbox'. Both 'width' and 'height' should be set.";
10171019
}
10181020
}
10191021

@@ -1058,7 +1060,7 @@ export class Html5Qrcode {
10581060

10591061
// If `qrbox` size is not set, it will default to the dimensions of the
10601062
// viewfinder.
1061-
const qrboxSize = isNullOrUndefined(internalConfig.qrbox) ?
1063+
const qrboxSize = isNullOrUndefined(internalConfig.qrbox) ?
10621064
{width: viewfinderWidth, height: viewfinderHeight}: internalConfig.qrbox!;
10631065

10641066
this.validateQrboxConfig(qrboxSize);
@@ -1068,10 +1070,10 @@ export class Html5Qrcode {
10681070
+ "greater than the height of the video stream. Shading will be"
10691071
+ " ignored");
10701072
}
1071-
1073+
10721074
const shouldShadingBeApplied
10731075
= internalConfig.isShadedBoxEnabled()
1074-
&& qrDimensions.height <= viewfinderHeight;
1076+
&& qrDimensions.height <= viewfinderHeight;
10751077
const defaultQrRegion: QrcodeRegionBounds = {
10761078
x: 0,
10771079
y: 0,
@@ -1082,7 +1084,7 @@ export class Html5Qrcode {
10821084
const qrRegion = shouldShadingBeApplied
10831085
? this.getShadedRegionBounds(viewfinderWidth, viewfinderHeight, qrDimensions)
10841086
: defaultQrRegion;
1085-
1087+
10861088
const canvasElement = this.createCanvasElement(
10871089
qrRegion.width, qrRegion.height);
10881090
// Tell user agent that this canvas will be read frequently.
@@ -1104,7 +1106,7 @@ export class Html5Qrcode {
11041106
}
11051107

11061108
this.createScannerPausedUiElement(this.element!);
1107-
1109+
11081110
// Update local states
11091111
this.qrRegion = qrRegion;
11101112
this.context = context;
@@ -1126,38 +1128,38 @@ export class Html5Qrcode {
11261128
rootElement.appendChild(scannerPausedUiElement);
11271129
this.scannerPausedUiElement = scannerPausedUiElement;
11281130
}
1129-
1130-
/**
1131-
* Scans current context using the qrcode library.
1132-
*
1133-
* <p>This method call would result in callback being triggered by the
1134-
* qrcode library. This method also handles the border coloring.
1135-
*
1136-
* @returns true if scan match is found, false otherwise.
1137-
*/
1131+
1132+
/**
1133+
* Scans current context using the qrcode library.
1134+
*
1135+
* <p>This method call would result in callback being triggered by the
1136+
* qrcode library. This method also handles the border coloring.
1137+
*
1138+
* @returns true if scan match is found, false otherwise.
1139+
*/
11381140
private scanContext(
1139-
qrCodeSuccessCallback: QrcodeSuccessCallback,
1140-
qrCodeErrorCallback: QrcodeErrorCallback
1141-
): Promise<boolean> {
1141+
qrCodeSuccessCallback: QrcodeSuccessCallback,
1142+
qrCodeErrorCallback: QrcodeErrorCallback
1143+
): Promise<boolean> {
11421144
if (this.stateManagerProxy.isPaused()) {
11431145
return Promise.resolve(false);
11441146
}
11451147

11461148
return this.qrcode.decodeAsync(this.canvasElement!)
1147-
.then((result) => {
1148-
qrCodeSuccessCallback(
1149-
result.text,
1150-
Html5QrcodeResultFactory.createFromQrcodeResult(
1151-
result));
1152-
this.possiblyUpdateShaders(/* qrMatch= */ true);
1153-
return true;
1154-
}).catch((error) => {
1155-
this.possiblyUpdateShaders(/* qrMatch= */ false);
1156-
let errorMessage = Html5QrcodeStrings.codeParseError(error);
1157-
qrCodeErrorCallback(
1158-
errorMessage, Html5QrcodeErrorFactory.createFrom(errorMessage));
1159-
return false;
1160-
});
1149+
.then((result) => {
1150+
qrCodeSuccessCallback(
1151+
result.text,
1152+
Html5QrcodeResultFactory.createFromQrcodeResult(
1153+
result));
1154+
this.possiblyUpdateShaders(/* qrMatch= */ true);
1155+
return true;
1156+
}).catch((error) => {
1157+
this.possiblyUpdateShaders(/* qrMatch= */ false);
1158+
let errorMessage = Html5QrcodeStrings.codeParseError(error);
1159+
qrCodeErrorCallback(
1160+
errorMessage, Html5QrcodeErrorFactory.createFrom(errorMessage));
1161+
return false;
1162+
});
11611163
}
11621164

11631165
/**
@@ -1217,14 +1219,31 @@ export class Html5Qrcode {
12171219
// TODO(mebjas): Move this logic to decoding library.
12181220
this.scanContext(qrCodeSuccessCallback, qrCodeErrorCallback)
12191221
.then((isSuccessfull) => {
1220-
// Previous scan failed and disableFlip is off.
1221-
if (!isSuccessfull && internalConfig.disableFlip !== true) {
1222-
this.context!.translate(this.context!.canvas.width, 0);
1223-
this.context!.scale(-1, 1);
1224-
this.scanContext(qrCodeSuccessCallback, qrCodeErrorCallback)
1225-
.finally(() => {
1226-
triggerNextScan();
1227-
});
1222+
// Previous scan failed
1223+
if (!isSuccessfull) {
1224+
// disableFlip is off and is not flipped
1225+
if (internalConfig.disableFlip !== true && this.flippedScan === false) {
1226+
this.context!.translate(this.context!.canvas.width, 0);
1227+
this.context!.scale(-1, 1);
1228+
this.flippedScan = true;
1229+
} else {
1230+
// disableFlip is off
1231+
if (internalConfig.disableFlip !== true) {
1232+
this.context!.translate(this.context!.canvas.width, 0);
1233+
this.context!.scale(-1, 1);
1234+
}
1235+
this.flippedScan = false;
1236+
}
1237+
// previous scan failed, flipped scan failed, try to invert colors
1238+
if (this.context!.filter === 'none' && this.flippedScan === false) {
1239+
this.context!.filter = 'invert(1)';
1240+
} else {
1241+
// restore initial filter value
1242+
if (this.context!.filter === 'invert(1)' && this.flippedScan === false) {
1243+
this.context!.filter = 'none';
1244+
}
1245+
}
1246+
triggerNextScan();
12281247
} else {
12291248
triggerNextScan();
12301249
}
@@ -1237,7 +1256,7 @@ export class Html5Qrcode {
12371256

12381257
private createVideoConstraints(
12391258
cameraIdOrConfig: string | MediaTrackConstraints)
1240-
: MediaTrackConstraints | undefined {
1259+
: MediaTrackConstraints | undefined {
12411260
if (typeof cameraIdOrConfig == "string") {
12421261
// If it's a string it should be camera device Id.
12431262
return { deviceId: { exact: cameraIdOrConfig } };
@@ -1254,20 +1273,20 @@ export class Html5Qrcode {
12541273
} else {
12551274
// Invalid config
12561275
throw "config has invalid 'facingMode' value = "
1257-
+ `'${value}'`;
1276+
+ `'${value}'`;
12581277
}
12591278
};
12601279

12611280
const keys = Object.keys(cameraIdOrConfig);
12621281
if (keys.length !== 1) {
12631282
throw "'cameraIdOrConfig' object should have exactly 1 key,"
1264-
+ ` if passed as an object, found ${keys.length} keys`;
1283+
+ ` if passed as an object, found ${keys.length} keys`;
12651284
}
12661285

12671286
const key:string = Object.keys(cameraIdOrConfig)[0];
12681287
if (key !== facingModeKey && key !== deviceIdKey) {
12691288
throw `Only '${facingModeKey}' and '${deviceIdKey}' `
1270-
+ " are supported for 'cameraIdOrConfig'";
1289+
+ " are supported for 'cameraIdOrConfig'";
12711290
}
12721291

12731292
if (key === facingModeKey) {
@@ -1286,15 +1305,15 @@ export class Html5Qrcode {
12861305
} else if (typeof facingMode == "object") {
12871306
if (exactKey in facingMode) {
12881307
if (isValidFacingModeValue(facingMode[`${exactKey}`])) {
1289-
return {
1290-
facingMode: {
1291-
exact: facingMode[`${exactKey}`]
1292-
}
1293-
};
1308+
return {
1309+
facingMode: {
1310+
exact: facingMode[`${exactKey}`]
1311+
}
1312+
};
12941313
}
12951314
} else {
12961315
throw "'facingMode' should be string or object with"
1297-
+ ` ${exactKey} as key.`;
1316+
+ ` ${exactKey} as key.`;
12981317
}
12991318
} else {
13001319
const type = (typeof facingMode);
@@ -1316,7 +1335,7 @@ export class Html5Qrcode {
13161335
};
13171336
} else {
13181337
throw "'deviceId' should be string or object with"
1319-
+ ` ${exactKey} as key.`;
1338+
+ ` ${exactKey} as key.`;
13201339
}
13211340
} else {
13221341
const type = (typeof deviceId);
@@ -1443,7 +1462,7 @@ export class Html5Qrcode {
14431462
height: number,
14441463
qrboxSize: QrDimensions) {
14451464
if ((width - qrboxSize.width) < 1 || (height - qrboxSize.height) < 1) {
1446-
return;
1465+
return;
14471466
}
14481467
const shadingElement = document.createElement("div");
14491468
shadingElement.style.position = "absolute";
@@ -1465,18 +1484,18 @@ export class Html5Qrcode {
14651484
shadingElement.style.left = "0px";
14661485
shadingElement.style.right = "0px";
14671486
shadingElement.id = `${Constants.SHADED_REGION_ELEMENT_ID}`;
1468-
1487+
14691488
// Check if div is too small for shadows. As there are two 5px width
14701489
// borders the needs to have a size above 10px.
1471-
if ((width - qrboxSize.width) < 11
1490+
if ((width - qrboxSize.width) < 11
14721491
|| (height - qrboxSize.height) < 11) {
1473-
this.hasBorderShaders = false;
1492+
this.hasBorderShaders = false;
14741493
} else {
14751494
const smallSize = 5;
14761495
const largeSize = 40;
14771496
this.insertShaderBorders(
14781497
shadingElement,
1479-
/* width= */ largeSize,
1498+
/* width= */ largeSize,
14801499
/* height= */ smallSize,
14811500
/* top= */ -smallSize,
14821501
/* bottom= */ null,
@@ -1563,12 +1582,12 @@ export class Html5Qrcode {
15631582
elem.style.bottom = `${bottom}px`;
15641583
}
15651584
if (isLeft) {
1566-
elem.style.left = `${side}px`;
1585+
elem.style.left = `${side}px`;
15671586
} else {
1568-
elem.style.right = `${side}px`;
1587+
elem.style.right = `${side}px`;
15691588
}
15701589
if (!this.borderShaders) {
1571-
this.borderShaders = [];
1590+
this.borderShaders = [];
15721591
}
15731592
this.borderShaders.push(elem);
15741593
shaderElem.appendChild(elem);

0 commit comments

Comments
 (0)