django-camera-kit 0.1.0__py3-none-any.whl

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.
Files changed (32) hide show
  1. django_camera_kit/__init__.py +1 -0
  2. django_camera_kit/apps.py +7 -0
  3. django_camera_kit/kyc/__init__.py +0 -0
  4. django_camera_kit/kyc/apps.py +8 -0
  5. django_camera_kit/kyc/conf.py +24 -0
  6. django_camera_kit/kyc/embeddings.py +39 -0
  7. django_camera_kit/kyc/migrations/0001_initial.py +85 -0
  8. django_camera_kit/kyc/migrations/__init__.py +0 -0
  9. django_camera_kit/kyc/models.py +67 -0
  10. django_camera_kit/kyc/serializers.py +23 -0
  11. django_camera_kit/kyc/static/django_camera_kit/kyc/css/kyc_capture.css +59 -0
  12. django_camera_kit/kyc/static/django_camera_kit/kyc/js/face_kit.js +116 -0
  13. django_camera_kit/kyc/static/django_camera_kit/kyc/js/kyc_capture.js +347 -0
  14. django_camera_kit/kyc/static/django_camera_kit/kyc/vendor/face_detection_yunet_2023mar.onnx +0 -0
  15. django_camera_kit/kyc/templates/django_camera_kit/kyc/widgets/kyc_verification.html +7 -0
  16. django_camera_kit/kyc/throttling.py +20 -0
  17. django_camera_kit/kyc/urls.py +9 -0
  18. django_camera_kit/kyc/views.py +65 -0
  19. django_camera_kit/kyc/widgets.py +39 -0
  20. django_camera_kit/static/django_camera_kit/css/camera_kit.css +97 -0
  21. django_camera_kit/static/django_camera_kit/js/camera_kit.js +213 -0
  22. django_camera_kit/static/django_camera_kit/js/doc_scan.js +367 -0
  23. django_camera_kit/static/django_camera_kit/vendor/README.md +6 -0
  24. django_camera_kit/static/django_camera_kit/vendor/jspdf.umd.min.js +373 -0
  25. django_camera_kit/static/django_camera_kit/vendor/opencv.js +0 -0
  26. django_camera_kit/templates/django_camera_kit/widgets/document_scanner.html +6 -0
  27. django_camera_kit/widgets.py +25 -0
  28. django_camera_kit-0.1.0.dist-info/METADATA +129 -0
  29. django_camera_kit-0.1.0.dist-info/RECORD +32 -0
  30. django_camera_kit-0.1.0.dist-info/WHEEL +5 -0
  31. django_camera_kit-0.1.0.dist-info/licenses/LICENSE +21 -0
  32. django_camera_kit-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,213 @@
1
+ (function (window) {
2
+ "use strict";
3
+
4
+ function openStream(constraints) {
5
+ constraints = constraints || {
6
+ video: { facingMode: { ideal: "environment" } },
7
+ audio: false,
8
+ };
9
+ return navigator.mediaDevices.getUserMedia(constraints);
10
+ }
11
+
12
+ function stopStream(stream) {
13
+ if (!stream) return;
14
+ stream.getTracks().forEach(function (track) {
15
+ track.stop();
16
+ });
17
+ }
18
+
19
+ function captureFrame(video) {
20
+ var canvas = document.createElement("canvas");
21
+ canvas.width = video.videoWidth;
22
+ canvas.height = video.videoHeight;
23
+ canvas.getContext("2d").drawImage(video, 0, 0, canvas.width, canvas.height);
24
+ return canvas;
25
+ }
26
+
27
+ // opencv.js is an Emscripten module: window.cv is *not* guaranteed to be
28
+ // the ready module. Depending on load timing it's a Promise that resolves
29
+ // to the module, a not-yet-initialized module (no .Mat until the WASM
30
+ // runtime is up), or the ready module itself. ready() resolves once and
31
+ // caches the real module so detectDocumentCorners()/warpToDocument() never
32
+ // have to guess — see https://docs.opencv.org/5.x/utilities.html.
33
+ var resolvedCv = null;
34
+ var cvReadyPromise = null;
35
+
36
+ function ready() {
37
+ if (cvReadyPromise) return cvReadyPromise;
38
+
39
+ cvReadyPromise = new Promise(function (resolve) {
40
+ if (!window.cv) {
41
+ resolve(false); // vendor/opencv.js not loaded, see vendor/README.md
42
+ return;
43
+ }
44
+ if (typeof window.cv.then === "function") {
45
+ window.cv.then(function (mod) {
46
+ resolvedCv = mod;
47
+ resolve(true);
48
+ });
49
+ } else if (window.cv.Mat) {
50
+ resolvedCv = window.cv;
51
+ resolve(true);
52
+ } else {
53
+ window.cv["onRuntimeInitialized"] = function () {
54
+ resolvedCv = window.cv;
55
+ resolve(true);
56
+ };
57
+ }
58
+ });
59
+
60
+ return cvReadyPromise;
61
+ }
62
+
63
+ // Largest 4-point contour in the given canvas, in canvas pixel coordinates.
64
+ // Returns null if OpenCV isn't ready yet or nothing convincing was found,
65
+ // so callers always have a manual-adjustment fallback.
66
+ function detectDocumentCorners(canvas) {
67
+ var cv = resolvedCv;
68
+ if (!cv || !cv.Mat) return null;
69
+
70
+ var src = cv.imread(canvas);
71
+ var gray = new cv.Mat();
72
+ var blurred = new cv.Mat();
73
+ var edged = new cv.Mat();
74
+ var contours = new cv.MatVector();
75
+ var hierarchy = new cv.Mat();
76
+ var corners = null;
77
+
78
+ try {
79
+ cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY);
80
+ cv.GaussianBlur(gray, blurred, new cv.Size(5, 5), 0);
81
+ cv.Canny(blurred, edged, 75, 200);
82
+ cv.findContours(edged, contours, hierarchy, cv.RETR_LIST, cv.CHAIN_APPROX_SIMPLE);
83
+
84
+ var bestArea = 0;
85
+ var bestApprox = null;
86
+ var minArea = src.rows * src.cols * 0.2;
87
+
88
+ for (var i = 0; i < contours.size(); i++) {
89
+ var contour = contours.get(i);
90
+ var peri = cv.arcLength(contour, true);
91
+ var approx = new cv.Mat();
92
+ cv.approxPolyDP(contour, approx, 0.02 * peri, true);
93
+
94
+ if (approx.rows === 4) {
95
+ var area = cv.contourArea(approx);
96
+ if (area > bestArea && area > minArea) {
97
+ bestArea = area;
98
+ if (bestApprox) bestApprox.delete();
99
+ bestApprox = approx;
100
+ } else {
101
+ approx.delete();
102
+ }
103
+ } else {
104
+ approx.delete();
105
+ }
106
+ contour.delete();
107
+ }
108
+
109
+ if (bestApprox) {
110
+ corners = [];
111
+ for (var p = 0; p < 4; p++) {
112
+ corners.push({ x: bestApprox.intAt(p, 0), y: bestApprox.intAt(p, 1) });
113
+ }
114
+ bestApprox.delete();
115
+ }
116
+ } finally {
117
+ src.delete();
118
+ gray.delete();
119
+ blurred.delete();
120
+ edged.delete();
121
+ contours.delete();
122
+ hierarchy.delete();
123
+ }
124
+
125
+ return corners ? sortCornersClockwise(corners) : null;
126
+ }
127
+
128
+ function sortCornersClockwise(corners) {
129
+ var center = corners.reduce(
130
+ function (acc, c) {
131
+ return { x: acc.x + c.x / 4, y: acc.y + c.y / 4 };
132
+ },
133
+ { x: 0, y: 0 }
134
+ );
135
+ return corners.slice().sort(function (a, b) {
136
+ return (
137
+ Math.atan2(a.y - center.y, a.x - center.x) -
138
+ Math.atan2(b.y - center.y, b.x - center.x)
139
+ );
140
+ });
141
+ }
142
+
143
+ function distance(a, b) {
144
+ return Math.hypot(a.x - b.x, a.y - b.y);
145
+ }
146
+
147
+ // Crops and un-warps the quadrilateral defined by `corners` (clockwise,
148
+ // starting top-left) out of `canvas` into a flat rectangular document image.
149
+ function warpToDocument(canvas, corners) {
150
+ var cv = resolvedCv;
151
+ if (!cv || !cv.Mat) {
152
+ throw new Error("django-camera-kit: opencv.js is not ready, see vendor/README.md");
153
+ }
154
+ var src = cv.imread(canvas);
155
+
156
+ var widthTop = distance(corners[0], corners[1]);
157
+ var widthBottom = distance(corners[3], corners[2]);
158
+ var heightLeft = distance(corners[0], corners[3]);
159
+ var heightRight = distance(corners[1], corners[2]);
160
+
161
+ var outWidth = Math.round(Math.max(widthTop, widthBottom));
162
+ var outHeight = Math.round(Math.max(heightLeft, heightRight));
163
+
164
+ var srcTri = cv.matFromArray(4, 1, cv.CV_32FC2, [
165
+ corners[0].x, corners[0].y,
166
+ corners[1].x, corners[1].y,
167
+ corners[2].x, corners[2].y,
168
+ corners[3].x, corners[3].y,
169
+ ]);
170
+ var dstTri = cv.matFromArray(4, 1, cv.CV_32FC2, [
171
+ 0, 0,
172
+ outWidth, 0,
173
+ outWidth, outHeight,
174
+ 0, outHeight,
175
+ ]);
176
+
177
+ var transform = cv.getPerspectiveTransform(srcTri, dstTri);
178
+ var dst = new cv.Mat();
179
+ cv.warpPerspective(src, dst, transform, new cv.Size(outWidth, outHeight));
180
+
181
+ var outCanvas = document.createElement("canvas");
182
+ outCanvas.width = outWidth;
183
+ outCanvas.height = outHeight;
184
+ cv.imshow(outCanvas, dst);
185
+
186
+ src.delete();
187
+ dst.delete();
188
+ transform.delete();
189
+ srcTri.delete();
190
+ dstTri.delete();
191
+
192
+ return outCanvas;
193
+ }
194
+
195
+ function canvasToBlob(canvas, type, quality) {
196
+ return new Promise(function (resolve) {
197
+ canvas.toBlob(resolve, type || "image/jpeg", quality || 0.92);
198
+ });
199
+ }
200
+
201
+ window.CameraKit = {
202
+ openStream: openStream,
203
+ stopStream: stopStream,
204
+ captureFrame: captureFrame,
205
+ ready: ready,
206
+ getCv: function () {
207
+ return resolvedCv;
208
+ },
209
+ detectDocumentCorners: detectDocumentCorners,
210
+ warpToDocument: warpToDocument,
211
+ canvasToBlob: canvasToBlob,
212
+ };
213
+ })(window);
@@ -0,0 +1,367 @@
1
+ (function (window, document) {
2
+ "use strict";
3
+
4
+ function init() {
5
+ document.querySelectorAll(".camera-kit-scanner").forEach(function (container) {
6
+ if (container.dataset.camerakitBound) return;
7
+ container.dataset.camerakitBound = "true";
8
+ bindScanner(container);
9
+ });
10
+ }
11
+
12
+ function bindScanner(container) {
13
+ var input = container.querySelector("input[type=file]");
14
+ var trigger = container.querySelector(".camera-kit-trigger");
15
+ if (!input || !trigger) return;
16
+
17
+ // Only hide the native file picker once JS has actually taken over —
18
+ // if this script fails to load, the raw input stays as the fallback.
19
+ container.classList.add("camera-kit-js-ready");
20
+
21
+ trigger.addEventListener("click", function () {
22
+ openScanner(container, input);
23
+ });
24
+ }
25
+
26
+ function openScanner(container, input) {
27
+ var session = {
28
+ pages: [], // warped page canvases, in order
29
+ stream: null,
30
+ frame: null,
31
+ corners: null,
32
+ lastCorners: null,
33
+ mode: "live", // "live" | "adjust" | "pages" | "closed"
34
+ };
35
+
36
+ var overlay = buildOverlay();
37
+ document.body.appendChild(overlay.root);
38
+
39
+ CameraKit.ready().then(function (isReady) {
40
+ if (!isReady) {
41
+ overlay.status.textContent =
42
+ "opencv.js n'est pas chargé (voir vendor/README.md) : le scan ne fonctionnera pas.";
43
+ }
44
+ });
45
+
46
+ CameraKit.openStream()
47
+ .then(function (stream) {
48
+ session.stream = stream;
49
+ overlay.video.srcObject = stream;
50
+ return overlay.video.play();
51
+ })
52
+ .then(function () {
53
+ trackLiveContour(session, overlay);
54
+ })
55
+ .catch(function (err) {
56
+ overlay.status.textContent = "Impossible d'accéder à la caméra : " + err.message;
57
+ });
58
+
59
+ overlay.captureBtn.addEventListener("click", function () {
60
+ capturePage(session, overlay);
61
+ });
62
+
63
+ overlay.retakeBtn.addEventListener("click", function () {
64
+ enterLiveMode(session, overlay);
65
+ });
66
+
67
+ overlay.confirmPageBtn.addEventListener("click", function () {
68
+ confirmPage(session, overlay);
69
+ });
70
+
71
+ overlay.addPageBtn.addEventListener("click", function () {
72
+ enterLiveMode(session, overlay);
73
+ });
74
+
75
+ overlay.finishBtn.addEventListener("click", function () {
76
+ finishScan(session, input, container);
77
+ closeOverlay(session, overlay);
78
+ });
79
+
80
+ overlay.cancelBtn.addEventListener("click", function () {
81
+ closeOverlay(session, overlay);
82
+ });
83
+ }
84
+
85
+ function trackLiveContour(session, overlay) {
86
+ if (session.mode !== "live" || !session.stream) return;
87
+
88
+ var video = overlay.video;
89
+ if (video.readyState === video.HAVE_ENOUGH_DATA) {
90
+ var frame = CameraKit.captureFrame(video);
91
+ session.lastCorners = CameraKit.detectDocumentCorners(frame);
92
+ drawOverlayQuad(overlay, video, session.lastCorners);
93
+ }
94
+
95
+ window.requestAnimationFrame(function () {
96
+ trackLiveContour(session, overlay);
97
+ });
98
+ }
99
+
100
+ function drawOverlayQuad(overlay, video, corners) {
101
+ var canvas = overlay.liveCanvas;
102
+ canvas.width = video.clientWidth;
103
+ canvas.height = video.clientHeight;
104
+ var ctx = canvas.getContext("2d");
105
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
106
+ if (!corners || !video.videoWidth) return;
107
+
108
+ var scaleX = video.clientWidth / video.videoWidth;
109
+ var scaleY = video.clientHeight / video.videoHeight;
110
+
111
+ ctx.strokeStyle = "#00723d";
112
+ ctx.lineWidth = 3;
113
+ ctx.beginPath();
114
+ corners.forEach(function (c, i) {
115
+ var x = c.x * scaleX;
116
+ var y = c.y * scaleY;
117
+ if (i === 0) ctx.moveTo(x, y);
118
+ else ctx.lineTo(x, y);
119
+ });
120
+ ctx.closePath();
121
+ ctx.stroke();
122
+ }
123
+
124
+ function capturePage(session, overlay) {
125
+ session.frame = CameraKit.captureFrame(overlay.video);
126
+ session.corners = session.lastCorners || defaultCorners(session.frame.width, session.frame.height);
127
+ enterAdjustMode(session, overlay);
128
+ }
129
+
130
+ function defaultCorners(width, height) {
131
+ var marginX = width * 0.08;
132
+ var marginY = height * 0.08;
133
+ return [
134
+ { x: marginX, y: marginY },
135
+ { x: width - marginX, y: marginY },
136
+ { x: width - marginX, y: height - marginY },
137
+ { x: marginX, y: height - marginY },
138
+ ];
139
+ }
140
+
141
+ function enterAdjustMode(session, overlay) {
142
+ session.mode = "adjust";
143
+ showStep(overlay, "adjust");
144
+
145
+ var canvas = overlay.adjustCanvas;
146
+ canvas.width = session.frame.width;
147
+ canvas.height = session.frame.height;
148
+ canvas.getContext("2d").drawImage(session.frame, 0, 0);
149
+
150
+ mountCornerHandles(session, overlay);
151
+ }
152
+
153
+ function mountCornerHandles(session, overlay) {
154
+ var wrapper = overlay.adjustWrapper;
155
+ wrapper.querySelectorAll(".camera-kit-handle").forEach(function (h) {
156
+ h.remove();
157
+ });
158
+
159
+ session.corners.forEach(function (corner, index) {
160
+ var handle = document.createElement("div");
161
+ handle.className = "camera-kit-handle";
162
+ wrapper.appendChild(handle);
163
+ positionHandle(handle, corner, overlay.adjustCanvas);
164
+ makeDraggable(handle, overlay.adjustCanvas, function (point) {
165
+ session.corners[index] = point;
166
+ redrawAdjustQuad(session, overlay);
167
+ });
168
+ });
169
+
170
+ redrawAdjustQuad(session, overlay);
171
+ }
172
+
173
+ function positionHandle(handle, corner, canvas) {
174
+ var rect = canvas.getBoundingClientRect();
175
+ var scaleX = rect.width / canvas.width;
176
+ var scaleY = rect.height / canvas.height;
177
+ handle.style.left = corner.x * scaleX - 12 + "px";
178
+ handle.style.top = corner.y * scaleY - 12 + "px";
179
+ }
180
+
181
+ function makeDraggable(handle, canvas, onMove) {
182
+ function toCanvasPoint(clientX, clientY) {
183
+ var rect = canvas.getBoundingClientRect();
184
+ return {
185
+ x: ((clientX - rect.left) / rect.width) * canvas.width,
186
+ y: ((clientY - rect.top) / rect.height) * canvas.height,
187
+ };
188
+ }
189
+
190
+ function onPointerMove(evt) {
191
+ var rect = canvas.getBoundingClientRect();
192
+ handle.style.left = evt.clientX - rect.left - 12 + "px";
193
+ handle.style.top = evt.clientY - rect.top - 12 + "px";
194
+ onMove(toCanvasPoint(evt.clientX, evt.clientY));
195
+ }
196
+
197
+ function onPointerUp() {
198
+ document.removeEventListener("pointermove", onPointerMove);
199
+ document.removeEventListener("pointerup", onPointerUp);
200
+ }
201
+
202
+ handle.addEventListener("pointerdown", function (evt) {
203
+ evt.preventDefault();
204
+ document.addEventListener("pointermove", onPointerMove);
205
+ document.addEventListener("pointerup", onPointerUp);
206
+ });
207
+ }
208
+
209
+ function redrawAdjustQuad(session, overlay) {
210
+ var canvas = overlay.adjustCanvas;
211
+ var ctx = canvas.getContext("2d");
212
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
213
+ ctx.drawImage(session.frame, 0, 0);
214
+
215
+ ctx.strokeStyle = "#00723d";
216
+ ctx.lineWidth = 3;
217
+ ctx.beginPath();
218
+ session.corners.forEach(function (c, i) {
219
+ if (i === 0) ctx.moveTo(c.x, c.y);
220
+ else ctx.lineTo(c.x, c.y);
221
+ });
222
+ ctx.closePath();
223
+ ctx.stroke();
224
+ }
225
+
226
+ function confirmPage(session, overlay) {
227
+ var warped;
228
+ try {
229
+ warped = CameraKit.warpToDocument(session.frame, session.corners);
230
+ } catch (err) {
231
+ overlay.status.textContent = err.message;
232
+ return;
233
+ }
234
+ session.pages.push(warped);
235
+ renderPageStrip(session, overlay);
236
+ session.mode = "pages";
237
+ showStep(overlay, "pages");
238
+ }
239
+
240
+ function renderPageStrip(session, overlay) {
241
+ overlay.pageStrip.innerHTML = "";
242
+ session.pages.forEach(function (pageCanvas, index) {
243
+ var thumb = document.createElement("img");
244
+ thumb.src = pageCanvas.toDataURL("image/jpeg", 0.7);
245
+ thumb.className = "camera-kit-thumb";
246
+ thumb.alt = "Page " + (index + 1);
247
+ overlay.pageStrip.appendChild(thumb);
248
+ });
249
+ }
250
+
251
+ function enterLiveMode(session, overlay) {
252
+ session.mode = "live";
253
+ session.frame = null;
254
+ session.corners = null;
255
+ showStep(overlay, "live");
256
+ trackLiveContour(session, overlay);
257
+ }
258
+
259
+ function finishScan(session, input, container) {
260
+ if (session.pages.length === 0) return;
261
+
262
+ var jsPDF = window.jspdf && window.jspdf.jsPDF;
263
+ if (!jsPDF) {
264
+ throw new Error("django-camera-kit: jsPDF vendor file is missing, see vendor/README.md");
265
+ }
266
+
267
+ var pdf = new jsPDF({ unit: "pt" });
268
+
269
+ session.pages.forEach(function (pageCanvas, index) {
270
+ var imgData = pageCanvas.toDataURL("image/jpeg", 0.92);
271
+ var pageWidth = pdf.internal.pageSize.getWidth();
272
+ var pageHeight = (pageCanvas.height / pageCanvas.width) * pageWidth;
273
+
274
+ if (index > 0) pdf.addPage();
275
+ pdf.addImage(imgData, "JPEG", 0, 0, pageWidth, pageHeight);
276
+ });
277
+
278
+ var blob = pdf.output("blob");
279
+ var fileName = (input.name || "document") + ".pdf";
280
+ var file = new File([blob], fileName, { type: "application/pdf" });
281
+
282
+ var dataTransfer = new DataTransfer();
283
+ dataTransfer.items.add(file);
284
+ input.files = dataTransfer.files;
285
+ input.dispatchEvent(new Event("change", { bubbles: true }));
286
+
287
+ container.dispatchEvent(
288
+ new CustomEvent("camerakit:scan-complete", {
289
+ bubbles: true,
290
+ detail: { pageCount: session.pages.length, file: file },
291
+ })
292
+ );
293
+ }
294
+
295
+ function closeOverlay(session, overlay) {
296
+ CameraKit.stopStream(session.stream);
297
+ session.mode = "closed";
298
+ overlay.root.remove();
299
+ }
300
+
301
+ function showStep(overlay, step) {
302
+ overlay.root.querySelectorAll("[data-step]").forEach(function (el) {
303
+ el.hidden = el.dataset.step !== step;
304
+ });
305
+ }
306
+
307
+ function buildOverlay() {
308
+ var root = document.createElement("div");
309
+ root.className = "camera-kit-overlay";
310
+ root.innerHTML = [
311
+ '<div class="camera-kit-modal">',
312
+ ' <div class="camera-kit-status"></div>',
313
+
314
+ ' <div data-step="live" class="camera-kit-step">',
315
+ ' <div class="camera-kit-video-wrapper">',
316
+ " <video autoplay playsinline muted></video>",
317
+ " <canvas></canvas>",
318
+ " </div>",
319
+ ' <div class="camera-kit-actions">',
320
+ ' <button type="button" class="btn btn-secondary camera-kit-cancel">Annuler</button>',
321
+ ' <button type="button" class="btn btn-primary camera-kit-capture">Capturer</button>',
322
+ " </div>",
323
+ " </div>",
324
+
325
+ ' <div data-step="adjust" class="camera-kit-step" hidden>',
326
+ ' <div class="camera-kit-adjust-wrapper">',
327
+ " <canvas></canvas>",
328
+ " </div>",
329
+ ' <div class="camera-kit-actions">',
330
+ ' <button type="button" class="btn btn-secondary camera-kit-retake">Reprendre</button>',
331
+ ' <button type="button" class="btn btn-primary camera-kit-confirm-page">Valider la page</button>',
332
+ " </div>",
333
+ " </div>",
334
+
335
+ ' <div data-step="pages" class="camera-kit-step" hidden>',
336
+ ' <div class="camera-kit-page-strip"></div>',
337
+ ' <div class="camera-kit-actions">',
338
+ ' <button type="button" class="btn btn-secondary camera-kit-add-page">Page suivante</button>',
339
+ ' <button type="button" class="btn btn-success camera-kit-finish">Terminer</button>',
340
+ " </div>",
341
+ " </div>",
342
+ "</div>",
343
+ ].join("\n");
344
+
345
+ return {
346
+ root: root,
347
+ status: root.querySelector(".camera-kit-status"),
348
+ video: root.querySelector("video"),
349
+ liveCanvas: root.querySelector('[data-step="live"] canvas'),
350
+ captureBtn: root.querySelector(".camera-kit-capture"),
351
+ cancelBtn: root.querySelector(".camera-kit-cancel"),
352
+ retakeBtn: root.querySelector(".camera-kit-retake"),
353
+ confirmPageBtn: root.querySelector(".camera-kit-confirm-page"),
354
+ adjustWrapper: root.querySelector(".camera-kit-adjust-wrapper"),
355
+ adjustCanvas: root.querySelector('[data-step="adjust"] canvas'),
356
+ addPageBtn: root.querySelector(".camera-kit-add-page"),
357
+ finishBtn: root.querySelector(".camera-kit-finish"),
358
+ pageStrip: root.querySelector(".camera-kit-page-strip"),
359
+ };
360
+ }
361
+
362
+ if (document.readyState === "loading") {
363
+ document.addEventListener("DOMContentLoaded", init);
364
+ } else {
365
+ init();
366
+ }
367
+ })(window, document);
@@ -0,0 +1,6 @@
1
+ These two files are required by `DocumentScannerWidget` and are intentionally not vendored into the repo (binary/generated build output). Download pinned versions before the widget will work:
2
+
3
+ - `opencv.js` — OpenCV.js WASM build. https://docs.opencv.org/4.x/opencv.js (build with `--build_wasm` for a browser target, or take a release build from the opencv/opencv-python-headless-js mirror). Pin a specific 4.x version.
4
+ - `jspdf.umd.min.js` — https://github.com/parallax/jsPDF releases, UMD build. Pin a specific version.
5
+
6
+ Place both files in this directory (`django_camera_kit/static/django_camera_kit/vendor/`). `widgets.py` references them by these exact filenames.