@neofaceid/web-sdk 1.39.0 → 1.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +6 -3
- package/dist/neoface-id-sdk.es.js +244 -119
- package/dist/neoface-id-sdk.umd.js +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -483,6 +483,7 @@ export declare class DocumentCaptureModal {
|
|
|
483
483
|
private flashController;
|
|
484
484
|
private frontBlob;
|
|
485
485
|
private backBlob;
|
|
486
|
+
private _previewObjectUrl;
|
|
486
487
|
constructor(options?: DocumentCaptureOptions);
|
|
487
488
|
open(): Promise<DocumentCaptureResult>;
|
|
488
489
|
private render;
|
|
@@ -593,7 +594,9 @@ export declare enum ErrorType {
|
|
|
593
594
|
CAPTURE_ERROR = "CaptureError",
|
|
594
595
|
NOT_FOUND = "NotFoundError",
|
|
595
596
|
UNKNOWN = "UnknownError",
|
|
596
|
-
CONSENT_DENIED = "ConsentDeniedError"
|
|
597
|
+
CONSENT_DENIED = "ConsentDeniedError",
|
|
598
|
+
/** NEO-224 · piscada real (EAR) não detectada dentro da janela de liveness. */
|
|
599
|
+
LIVENESS_BLINK_MISSING = "LivenessBlinkMissingError"
|
|
597
600
|
}
|
|
598
601
|
|
|
599
602
|
export declare function FaceCaptureModal({ accessToken, onClose, onSuccess, onCannotGesture, autoLighting, }: FaceCaptureModalProps): JSX_2.Element;
|
|
@@ -1339,7 +1342,7 @@ export declare const registerPersonWithoutFace: (personData: {
|
|
|
1339
1342
|
/**
|
|
1340
1343
|
* Data de lançamento da versão atual
|
|
1341
1344
|
*/
|
|
1342
|
-
export declare const RELEASE_DATE = "2026-09-
|
|
1345
|
+
export declare const RELEASE_DATE = "2026-09-09";
|
|
1343
1346
|
|
|
1344
1347
|
declare interface RequestChallengeWithSessionParams {
|
|
1345
1348
|
applicationToken: string;
|
|
@@ -1706,7 +1709,7 @@ export declare const validateToken: (applicationToken: string) => Promise<boolea
|
|
|
1706
1709
|
* MINOR: Incrementado quando adicionamos funcionalidades mantendo compatibilidade
|
|
1707
1710
|
* PATCH: Incrementado quando corrigimos bugs mantendo compatibilidade
|
|
1708
1711
|
*/
|
|
1709
|
-
export declare const VERSION = "1.
|
|
1712
|
+
export declare const VERSION = "1.40.0";
|
|
1710
1713
|
|
|
1711
1714
|
/**
|
|
1712
1715
|
* Executa `fn(sessionId)`. Se o servidor devolver 410 (sessão consumida/expirada),
|
|
@@ -162,6 +162,7 @@ var ErrorType = /* @__PURE__ */ ((ErrorType2) => {
|
|
|
162
162
|
ErrorType2["NOT_FOUND"] = "NotFoundError";
|
|
163
163
|
ErrorType2["UNKNOWN"] = "UnknownError";
|
|
164
164
|
ErrorType2["CONSENT_DENIED"] = "ConsentDeniedError";
|
|
165
|
+
ErrorType2["LIVENESS_BLINK_MISSING"] = "LivenessBlinkMissingError";
|
|
165
166
|
return ErrorType2;
|
|
166
167
|
})(ErrorType || {});
|
|
167
168
|
class NeoFaceError extends Error {
|
|
@@ -199,6 +200,8 @@ class NeoFaceError extends Error {
|
|
|
199
200
|
return "Dados inválidos ou rosto não detectado corretamente.";
|
|
200
201
|
case "ConsentDeniedError":
|
|
201
202
|
return "É necessário aceitar o uso do seu rosto para continuar.";
|
|
203
|
+
case "LivenessBlinkMissingError":
|
|
204
|
+
return "Não detectamos uma piscada. Pisque os olhos naturalmente e tente novamente.";
|
|
202
205
|
case "ApiError":
|
|
203
206
|
if (this.message.includes("400")) return "Requisição inválida. Tente novamente.";
|
|
204
207
|
if (this.message.includes("500"))
|
|
@@ -4681,6 +4684,14 @@ function getLocale() {
|
|
|
4681
4684
|
function getConsent() {
|
|
4682
4685
|
return { ...globalConfig.consent };
|
|
4683
4686
|
}
|
|
4687
|
+
async function withObjectURL(blob, fn) {
|
|
4688
|
+
const url = URL.createObjectURL(blob);
|
|
4689
|
+
try {
|
|
4690
|
+
return await fn(url);
|
|
4691
|
+
} finally {
|
|
4692
|
+
URL.revokeObjectURL(url);
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
4684
4695
|
const getApiBaseUrl = () => getBaseUrl();
|
|
4685
4696
|
const REQUEST_TIMEOUT = 1e4;
|
|
4686
4697
|
const ensureSecureContext = () => {
|
|
@@ -4698,75 +4709,81 @@ const createTimeoutController = () => {
|
|
|
4698
4709
|
controller.signal.addEventListener("abort", () => clearTimeout(timeoutId));
|
|
4699
4710
|
return controller;
|
|
4700
4711
|
};
|
|
4701
|
-
const compressImage = async (imageBlob) =>
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
const
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
(
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
}
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
height
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
}
|
|
4712
|
+
const compressImage = async (imageBlob) => withObjectURL(
|
|
4713
|
+
imageBlob,
|
|
4714
|
+
(url) => new Promise((resolve, reject) => {
|
|
4715
|
+
const img = new Image();
|
|
4716
|
+
img.onload = () => {
|
|
4717
|
+
const canvas = document.createElement("canvas");
|
|
4718
|
+
canvas.width = img.width;
|
|
4719
|
+
canvas.height = img.height;
|
|
4720
|
+
const ctx = canvas.getContext("2d");
|
|
4721
|
+
if (!ctx) {
|
|
4722
|
+
reject(new Error("Failed to get canvas context"));
|
|
4723
|
+
return;
|
|
4724
|
+
}
|
|
4725
|
+
ctx.drawImage(img, 0, 0);
|
|
4726
|
+
canvas.toBlob(
|
|
4727
|
+
(blob) => {
|
|
4728
|
+
if (blob) {
|
|
4729
|
+
resolve(blob);
|
|
4730
|
+
} else {
|
|
4731
|
+
reject(new Error("Failed to compress image"));
|
|
4732
|
+
}
|
|
4733
|
+
},
|
|
4734
|
+
"image/jpeg",
|
|
4735
|
+
0.7
|
|
4736
|
+
);
|
|
4737
|
+
};
|
|
4738
|
+
img.onerror = () => {
|
|
4739
|
+
reject(new Error("Failed to load image"));
|
|
4740
|
+
};
|
|
4741
|
+
img.src = url;
|
|
4742
|
+
})
|
|
4743
|
+
);
|
|
4744
|
+
const compressDocumentImage = async (imageBlob) => withObjectURL(
|
|
4745
|
+
imageBlob,
|
|
4746
|
+
(url) => new Promise((resolve, reject) => {
|
|
4747
|
+
const img = new Image();
|
|
4748
|
+
img.onload = () => {
|
|
4749
|
+
const MAX_SIZE = 1280;
|
|
4750
|
+
let { width } = img;
|
|
4751
|
+
let { height } = img;
|
|
4752
|
+
if (width > height && width > MAX_SIZE) {
|
|
4753
|
+
height = height * MAX_SIZE / width;
|
|
4754
|
+
width = MAX_SIZE;
|
|
4755
|
+
} else if (height > MAX_SIZE) {
|
|
4756
|
+
width = width * MAX_SIZE / height;
|
|
4757
|
+
height = MAX_SIZE;
|
|
4758
|
+
}
|
|
4759
|
+
const canvas = document.createElement("canvas");
|
|
4760
|
+
canvas.width = width;
|
|
4761
|
+
canvas.height = height;
|
|
4762
|
+
const ctx = canvas.getContext("2d");
|
|
4763
|
+
if (!ctx) {
|
|
4764
|
+
reject(new Error("Failed to get canvas context"));
|
|
4765
|
+
return;
|
|
4766
|
+
}
|
|
4767
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
4768
|
+
canvas.toBlob(
|
|
4769
|
+
(blob) => {
|
|
4770
|
+
if (blob) {
|
|
4771
|
+
resolve(blob);
|
|
4772
|
+
} else {
|
|
4773
|
+
reject(new Error("Failed to compress document image"));
|
|
4774
|
+
}
|
|
4775
|
+
},
|
|
4776
|
+
"image/jpeg",
|
|
4777
|
+
0.75
|
|
4778
|
+
// 75% quality to keep file size manageable
|
|
4779
|
+
);
|
|
4780
|
+
};
|
|
4781
|
+
img.onerror = () => {
|
|
4782
|
+
reject(new Error("Failed to load document image"));
|
|
4783
|
+
};
|
|
4784
|
+
img.src = url;
|
|
4785
|
+
})
|
|
4786
|
+
);
|
|
4770
4787
|
const validateToken = async (applicationToken) => {
|
|
4771
4788
|
ensureSecureContext();
|
|
4772
4789
|
const controller = createTimeoutController();
|
|
@@ -6436,6 +6453,63 @@ function ScreenFlashOverlay({
|
|
|
6436
6453
|
] })
|
|
6437
6454
|
] });
|
|
6438
6455
|
}
|
|
6456
|
+
const POLL_INTERVAL_MS = 50;
|
|
6457
|
+
const MIN_HORIZONTAL_DISTANCE = 1e-6;
|
|
6458
|
+
function distance(a, b) {
|
|
6459
|
+
return Math.hypot(a.x - b.x, a.y - b.y);
|
|
6460
|
+
}
|
|
6461
|
+
function calculateEAR(eyePoints) {
|
|
6462
|
+
if (eyePoints.length !== 6) {
|
|
6463
|
+
throw new Error(`calculateEAR espera exatamente 6 pontos do olho, recebeu ${eyePoints.length}`);
|
|
6464
|
+
}
|
|
6465
|
+
const [p1, p2, p3, p4, p5, p6] = eyePoints;
|
|
6466
|
+
const verticalA = distance(p2, p6);
|
|
6467
|
+
const verticalB = distance(p3, p5);
|
|
6468
|
+
const horizontal = distance(p1, p4);
|
|
6469
|
+
if (horizontal < MIN_HORIZONTAL_DISTANCE) return 0;
|
|
6470
|
+
return (verticalA + verticalB) / (2 * horizontal);
|
|
6471
|
+
}
|
|
6472
|
+
function detectBlinkInWindow(faceapi2, videoEl, windowMs = 3e3, threshold = 0.2, signal) {
|
|
6473
|
+
return new Promise((resolve) => {
|
|
6474
|
+
const deadline = Date.now() + windowMs;
|
|
6475
|
+
let eyesWereClosed = false;
|
|
6476
|
+
let isResolved = false;
|
|
6477
|
+
const finish = (result) => {
|
|
6478
|
+
if (isResolved) return;
|
|
6479
|
+
isResolved = true;
|
|
6480
|
+
resolve(result);
|
|
6481
|
+
};
|
|
6482
|
+
const checkFrame = () => {
|
|
6483
|
+
if (isResolved) return;
|
|
6484
|
+
if (signal == null ? void 0 : signal.aborted) {
|
|
6485
|
+
finish(false);
|
|
6486
|
+
return;
|
|
6487
|
+
}
|
|
6488
|
+
if (Date.now() >= deadline) {
|
|
6489
|
+
finish(false);
|
|
6490
|
+
return;
|
|
6491
|
+
}
|
|
6492
|
+
const detectionTask = faceapi2.detectSingleFace(videoEl, new faceapi2.TinyFaceDetectorOptions()).withFaceLandmarks();
|
|
6493
|
+
detectionTask.then((detection) => {
|
|
6494
|
+
if (detection) {
|
|
6495
|
+
const leftEAR = calculateEAR(detection.landmarks.getLeftEye());
|
|
6496
|
+
const rightEAR = calculateEAR(detection.landmarks.getRightEye());
|
|
6497
|
+
const ear = (leftEAR + rightEAR) / 2;
|
|
6498
|
+
if (ear < threshold) {
|
|
6499
|
+
eyesWereClosed = true;
|
|
6500
|
+
} else if (eyesWereClosed) {
|
|
6501
|
+
finish(true);
|
|
6502
|
+
return;
|
|
6503
|
+
}
|
|
6504
|
+
}
|
|
6505
|
+
setTimeout(checkFrame, POLL_INTERVAL_MS);
|
|
6506
|
+
}).catch(() => {
|
|
6507
|
+
setTimeout(checkFrame, POLL_INTERVAL_MS);
|
|
6508
|
+
});
|
|
6509
|
+
};
|
|
6510
|
+
checkFrame();
|
|
6511
|
+
});
|
|
6512
|
+
}
|
|
6439
6513
|
const modalReducer$1 = (state, action) => {
|
|
6440
6514
|
switch (action.type) {
|
|
6441
6515
|
case "PERMISSION_GRANTED":
|
|
@@ -6582,6 +6656,8 @@ function FaceCaptureModal({
|
|
|
6582
6656
|
const canvasRef = useRef(null);
|
|
6583
6657
|
const streamRef = useRef(null);
|
|
6584
6658
|
const faceDetectionTimeoutRef = useRef(null);
|
|
6659
|
+
const isCapturingRef = useRef(false);
|
|
6660
|
+
const blinkAbortControllerRef = useRef(null);
|
|
6585
6661
|
const [state, dispatch] = useReducer(modalReducer$1, { status: "requestingPermission" });
|
|
6586
6662
|
const [faceDetected, setFaceDetected] = useState(false);
|
|
6587
6663
|
const { startCamera, stopCamera } = useCamera();
|
|
@@ -6617,6 +6693,13 @@ function FaceCaptureModal({
|
|
|
6617
6693
|
useEffect(() => {
|
|
6618
6694
|
injectThemeStyles();
|
|
6619
6695
|
}, []);
|
|
6696
|
+
useEffect(
|
|
6697
|
+
() => () => {
|
|
6698
|
+
var _a2;
|
|
6699
|
+
(_a2 = blinkAbortControllerRef.current) == null ? void 0 : _a2.abort();
|
|
6700
|
+
},
|
|
6701
|
+
[]
|
|
6702
|
+
);
|
|
6620
6703
|
const flashEnabled = autoLighting && (state.status === "ready" || state.status === "capturing");
|
|
6621
6704
|
const { flashOn } = useScreenFlash(videoRef, { enabled: flashEnabled });
|
|
6622
6705
|
useEffect(() => {
|
|
@@ -6660,19 +6743,40 @@ function FaceCaptureModal({
|
|
|
6660
6743
|
}, [startCamera, stopCamera]);
|
|
6661
6744
|
const handleCapture = async () => {
|
|
6662
6745
|
if (!canvasRef.current || !videoRef.current) return;
|
|
6746
|
+
if (isCapturingRef.current) return;
|
|
6747
|
+
isCapturingRef.current = true;
|
|
6748
|
+
const videoEl = videoRef.current;
|
|
6749
|
+
const canvasEl = canvasRef.current;
|
|
6750
|
+
const abortController = new AbortController();
|
|
6751
|
+
blinkAbortControllerRef.current = abortController;
|
|
6663
6752
|
try {
|
|
6664
6753
|
dispatch({ type: "START_CAPTURE" });
|
|
6665
|
-
const
|
|
6754
|
+
const blinkDetected = await detectBlinkInWindow(
|
|
6755
|
+
faceapi,
|
|
6756
|
+
videoEl,
|
|
6757
|
+
void 0,
|
|
6758
|
+
void 0,
|
|
6759
|
+
abortController.signal
|
|
6760
|
+
);
|
|
6761
|
+
if (abortController.signal.aborted) return;
|
|
6762
|
+
if (!blinkDetected) {
|
|
6763
|
+
const blinkError = new NeoFaceError(
|
|
6764
|
+
"Blink not detected within window",
|
|
6765
|
+
ErrorType.LIVENESS_BLINK_MISSING
|
|
6766
|
+
);
|
|
6767
|
+
dispatch({ type: "SET_ERROR", message: blinkError.getFriendlyMessage() });
|
|
6768
|
+
return;
|
|
6769
|
+
}
|
|
6770
|
+
const context = canvasEl.getContext("2d");
|
|
6666
6771
|
if (!context) {
|
|
6667
6772
|
dispatch({ type: "SET_ERROR", message: "Failed to get canvas context" });
|
|
6668
6773
|
return;
|
|
6669
6774
|
}
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
context.drawImage(
|
|
6775
|
+
canvasEl.width = videoEl.videoWidth;
|
|
6776
|
+
canvasEl.height = videoEl.videoHeight;
|
|
6777
|
+
context.drawImage(videoEl, 0, 0, canvasEl.width, canvasEl.height);
|
|
6673
6778
|
const blob = await new Promise((resolve, reject) => {
|
|
6674
|
-
|
|
6675
|
-
(_a2 = canvasRef.current) == null ? void 0 : _a2.toBlob(
|
|
6779
|
+
canvasEl.toBlob(
|
|
6676
6780
|
(b) => {
|
|
6677
6781
|
if (b) resolve(b);
|
|
6678
6782
|
else reject(new Error("Failed to convert canvas to blob"));
|
|
@@ -6697,6 +6801,8 @@ function FaceCaptureModal({
|
|
|
6697
6801
|
}
|
|
6698
6802
|
} catch (error) {
|
|
6699
6803
|
dispatch({ type: "SET_ERROR", message: "Failed to capture image" });
|
|
6804
|
+
} finally {
|
|
6805
|
+
isCapturingRef.current = false;
|
|
6700
6806
|
}
|
|
6701
6807
|
};
|
|
6702
6808
|
const handleRetry = () => {
|
|
@@ -7735,6 +7841,7 @@ function BiometricRegistrationModal({
|
|
|
7735
7841
|
if (onPhotosCaptured) {
|
|
7736
7842
|
dispatch({ type: "SUCCESS" });
|
|
7737
7843
|
onPhotosCaptured(photos);
|
|
7844
|
+
photos.length = 0;
|
|
7738
7845
|
return;
|
|
7739
7846
|
}
|
|
7740
7847
|
if (!personData || !applicationToken) {
|
|
@@ -7754,6 +7861,7 @@ function BiometricRegistrationModal({
|
|
|
7754
7861
|
dispatch({ type: "SUCCESS" });
|
|
7755
7862
|
if (onSuccess) {
|
|
7756
7863
|
onSuccess(adaptedResult);
|
|
7864
|
+
photos.length = 0;
|
|
7757
7865
|
}
|
|
7758
7866
|
} catch (error) {
|
|
7759
7867
|
const errorMessage = error instanceof Error ? error.message : "Erro desconhecido";
|
|
@@ -11163,8 +11271,8 @@ const biometricDetection = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.
|
|
|
11163
11271
|
initializeBiometricDetection,
|
|
11164
11272
|
isAdvancedDetectionAvailable
|
|
11165
11273
|
}, Symbol.toStringTag, { value: "Module" }));
|
|
11166
|
-
const VERSION = "1.
|
|
11167
|
-
const RELEASE_DATE = "2026-09-
|
|
11274
|
+
const VERSION = "1.40.0";
|
|
11275
|
+
const RELEASE_DATE = "2026-09-09";
|
|
11168
11276
|
let cachedSession = null;
|
|
11169
11277
|
function getCachedCaptureSession() {
|
|
11170
11278
|
return cachedSession;
|
|
@@ -12130,11 +12238,13 @@ const authorizeOperation = async (applicationToken, cpf, options) => {
|
|
|
12130
12238
|
sessionData
|
|
12131
12239
|
);
|
|
12132
12240
|
if (!recognitionResult.success) {
|
|
12241
|
+
capturedPhotos.length = 0;
|
|
12133
12242
|
throw new NeoFaceError(
|
|
12134
12243
|
"Authorization failed: Person not recognized",
|
|
12135
12244
|
ErrorType.RECOGNITION_FAILED
|
|
12136
12245
|
);
|
|
12137
12246
|
}
|
|
12247
|
+
capturedPhotos.length = 0;
|
|
12138
12248
|
return {
|
|
12139
12249
|
success: true,
|
|
12140
12250
|
authorizationHash: sessionData.sessionId,
|
|
@@ -13117,6 +13227,7 @@ class DocumentCaptureModal {
|
|
|
13117
13227
|
__publicField(this, "flashController", null);
|
|
13118
13228
|
__publicField(this, "frontBlob", null);
|
|
13119
13229
|
__publicField(this, "backBlob", null);
|
|
13230
|
+
__publicField(this, "_previewObjectUrl", null);
|
|
13120
13231
|
this.options = {
|
|
13121
13232
|
title: (options == null ? void 0 : options.title) ?? "Validação de Identidade",
|
|
13122
13233
|
subtitle: (options == null ? void 0 : options.subtitle) ?? "Selecione o tipo de documento",
|
|
@@ -13422,47 +13533,50 @@ class DocumentCaptureModal {
|
|
|
13422
13533
|
}
|
|
13423
13534
|
}
|
|
13424
13535
|
compressUploadedImage(file) {
|
|
13425
|
-
return
|
|
13426
|
-
|
|
13427
|
-
|
|
13428
|
-
const
|
|
13429
|
-
|
|
13430
|
-
|
|
13431
|
-
|
|
13432
|
-
|
|
13433
|
-
width
|
|
13434
|
-
|
|
13435
|
-
|
|
13436
|
-
height
|
|
13437
|
-
|
|
13438
|
-
|
|
13439
|
-
|
|
13440
|
-
|
|
13441
|
-
|
|
13442
|
-
|
|
13443
|
-
|
|
13444
|
-
|
|
13445
|
-
|
|
13446
|
-
|
|
13447
|
-
|
|
13448
|
-
(
|
|
13449
|
-
|
|
13450
|
-
|
|
13451
|
-
|
|
13452
|
-
|
|
13453
|
-
|
|
13454
|
-
|
|
13455
|
-
|
|
13456
|
-
|
|
13457
|
-
|
|
13458
|
-
|
|
13459
|
-
|
|
13460
|
-
|
|
13461
|
-
|
|
13462
|
-
|
|
13463
|
-
|
|
13464
|
-
|
|
13465
|
-
|
|
13536
|
+
return withObjectURL(
|
|
13537
|
+
file,
|
|
13538
|
+
(url) => new Promise((resolve, reject) => {
|
|
13539
|
+
const img = new Image();
|
|
13540
|
+
img.onload = () => {
|
|
13541
|
+
const MAX_SIZE = 1280;
|
|
13542
|
+
let { width } = img;
|
|
13543
|
+
let { height } = img;
|
|
13544
|
+
if (width > height && width > MAX_SIZE) {
|
|
13545
|
+
height = height * MAX_SIZE / width;
|
|
13546
|
+
width = MAX_SIZE;
|
|
13547
|
+
} else if (height > MAX_SIZE) {
|
|
13548
|
+
width = width * MAX_SIZE / height;
|
|
13549
|
+
height = MAX_SIZE;
|
|
13550
|
+
}
|
|
13551
|
+
const canvas = document.createElement("canvas");
|
|
13552
|
+
canvas.width = width;
|
|
13553
|
+
canvas.height = height;
|
|
13554
|
+
const ctx = canvas.getContext("2d");
|
|
13555
|
+
if (!ctx) {
|
|
13556
|
+
reject(new Error("Failed to get canvas context"));
|
|
13557
|
+
return;
|
|
13558
|
+
}
|
|
13559
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
13560
|
+
canvas.toBlob(
|
|
13561
|
+
(blob) => {
|
|
13562
|
+
if (blob) {
|
|
13563
|
+
console.log(
|
|
13564
|
+
`[DocumentCapture] Compressed image from ${(file.size / 1024).toFixed(0)}KB to ${(blob.size / 1024).toFixed(0)}KB`
|
|
13565
|
+
);
|
|
13566
|
+
resolve(blob);
|
|
13567
|
+
} else {
|
|
13568
|
+
reject(new Error("Failed to compress image"));
|
|
13569
|
+
}
|
|
13570
|
+
},
|
|
13571
|
+
"image/jpeg",
|
|
13572
|
+
0.75
|
|
13573
|
+
// 75% quality - keeps file size manageable for server limits
|
|
13574
|
+
);
|
|
13575
|
+
};
|
|
13576
|
+
img.onerror = () => reject(new Error("Failed to load image"));
|
|
13577
|
+
img.src = url;
|
|
13578
|
+
})
|
|
13579
|
+
);
|
|
13466
13580
|
}
|
|
13467
13581
|
bindPreviewEvents() {
|
|
13468
13582
|
var _a2, _b, _c;
|
|
@@ -13475,7 +13589,14 @@ class DocumentCaptureModal {
|
|
|
13475
13589
|
);
|
|
13476
13590
|
if (previewImg) {
|
|
13477
13591
|
const blob = this.currentStep === "preview-front" ? this.frontBlob : this.backBlob;
|
|
13478
|
-
if (blob)
|
|
13592
|
+
if (blob) {
|
|
13593
|
+
if (this._previewObjectUrl) {
|
|
13594
|
+
URL.revokeObjectURL(this._previewObjectUrl);
|
|
13595
|
+
this._previewObjectUrl = null;
|
|
13596
|
+
}
|
|
13597
|
+
this._previewObjectUrl = URL.createObjectURL(blob);
|
|
13598
|
+
previewImg.src = this._previewObjectUrl;
|
|
13599
|
+
}
|
|
13479
13600
|
}
|
|
13480
13601
|
this.bindCloseEvent();
|
|
13481
13602
|
}
|
|
@@ -13662,6 +13783,10 @@ class DocumentCaptureModal {
|
|
|
13662
13783
|
}
|
|
13663
13784
|
close() {
|
|
13664
13785
|
this.stopCamera();
|
|
13786
|
+
if (this._previewObjectUrl) {
|
|
13787
|
+
URL.revokeObjectURL(this._previewObjectUrl);
|
|
13788
|
+
this._previewObjectUrl = null;
|
|
13789
|
+
}
|
|
13665
13790
|
if (this.overlay) {
|
|
13666
13791
|
this.overlay.remove();
|
|
13667
13792
|
this.overlay = null;
|