first commit

This commit is contained in:
Dhananjaya99
2026-09-28 11:48:14 +05:30
commit a1b870fa8a
242 changed files with 39483 additions and 0 deletions
+137
View File
@@ -0,0 +1,137 @@
/**
* Client-side normalisation for face photos headed to a Hikvision face library.
*
* The terminal is strict in ways a raw camera roll photo never satisfies, so the browser
* does the work before upload:
* - JPEG only. Phone photos are usually HEIC, which the terminal rejects outright.
* - Small. The ~3MB figure in the Hikvision docs is the ISAPI transport ceiling; the
* face *library* caps far lower, so an oversized photo uploads happily and then fails
* enrolment -- the worst failure mode, because it looks like it worked.
* - Portrait. Terminals expect a 3:4 face frame; a square crop tends to cut the chin.
*
* Canvas does all of this, so there is no image dependency in this project.
*/
/** Target frame. 3:4 portrait is what the face library expects. */
export const FACE_WIDTH = 480
export const FACE_HEIGHT = 640
/** Face-library budget, well under the ISAPI transport limit. */
export const FACE_TARGET_BYTES = 150 * 1024
export const FACE_MAX_BYTES = 200 * 1024
/** Guard on the *input* file, applied before decoding so a huge file can't exhaust memory. */
export const INPUT_MAX_BYTES = 3 * 1024 * 1024
/** Quality ladder: start high, step down only while over budget. */
const QUALITY_STEPS = [0.85, 0.75, 0.65]
export interface NormalizedFacePhoto {
/** Bare base64 JPEG, no data: prefix — what the API expects. */
base64: string
/** data: URL for previewing in an <img>. */
previewUrl: string
bytes: number
width: number
height: number
}
export class FacePhotoError extends Error {}
/**
* Decodes any browser-readable image, cover-crops it to 3:4, resizes to 480x640 and
* encodes JPEG under the face-library budget.
*/
export async function normalizeFacePhoto(
source: Blob | HTMLVideoElement
): Promise<NormalizedFacePhoto> {
if (source instanceof Blob) {
if (source.size > INPUT_MAX_BYTES) {
throw new FacePhotoError(
`That image is ${Math.round(source.size / 1024 / 1024)} MB. Choose one under 3 MB.`
)
}
if (source.size === 0) {
throw new FacePhotoError("That file is empty.")
}
}
const bitmap = await toBitmap(source)
try {
const canvas = document.createElement("canvas")
canvas.width = FACE_WIDTH
canvas.height = FACE_HEIGHT
const ctx = canvas.getContext("2d")
if (!ctx) throw new FacePhotoError("This browser cannot process images.")
// Cover-crop: fill the 3:4 frame from the centre rather than squashing the face.
const scale = Math.max(FACE_WIDTH / bitmap.width, FACE_HEIGHT / bitmap.height)
const drawWidth = bitmap.width * scale
const drawHeight = bitmap.height * scale
ctx.imageSmoothingQuality = "high"
// A face library needs an opaque background; a transparent PNG would otherwise
// flatten to black and wreck the exposure.
ctx.fillStyle = "#ffffff"
ctx.fillRect(0, 0, FACE_WIDTH, FACE_HEIGHT)
ctx.drawImage(
bitmap,
(FACE_WIDTH - drawWidth) / 2,
(FACE_HEIGHT - drawHeight) / 2,
drawWidth,
drawHeight
)
let blob: Blob | null = null
for (const quality of QUALITY_STEPS) {
blob = await canvasToBlob(canvas, quality)
if (blob && blob.size <= FACE_TARGET_BYTES) break
}
if (!blob) throw new FacePhotoError("Could not process that image. Try another photo.")
if (blob.size > FACE_MAX_BYTES) {
throw new FacePhotoError(
"Could not compress that photo enough for the access terminal. Try a simpler " +
"background or better lighting."
)
}
const dataUrl = await blobToDataUrl(blob)
return {
base64: dataUrl.slice(dataUrl.indexOf(",") + 1),
previewUrl: dataUrl,
bytes: blob.size,
width: FACE_WIDTH,
height: FACE_HEIGHT,
}
} finally {
if ("close" in bitmap && typeof bitmap.close === "function") bitmap.close()
}
}
async function toBitmap(source: Blob | HTMLVideoElement): Promise<ImageBitmap> {
try {
// createImageBitmap decodes HEIC on Safari and every other format the browser knows.
return await createImageBitmap(source as ImageBitmapSource)
} catch {
throw new FacePhotoError(
"That file isn't a readable image. Use a JPEG or PNG photo, or take one with the camera."
)
}
}
function canvasToBlob(canvas: HTMLCanvasElement, quality: number): Promise<Blob | null> {
return new Promise((resolve) => canvas.toBlob(resolve, "image/jpeg", quality))
}
function blobToDataUrl(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(String(reader.result))
reader.onerror = () => reject(new FacePhotoError("Could not read the processed image."))
reader.readAsDataURL(blob)
})
}