Scanner > License Plate Recognition > POST */ $this->post('/modules/scanner/lpr', function () { global $response; $route_started_at = microtime(true); $image_upload = self::getLPRImageUpload(); $raw_image_upload = $image_upload === null ? self::getLPRRawImageUpload() : null; $client_timings = self::getLPRClientTimings(); $base64_image = null; //self::requirePermission('modules_scanner_lpr'); if ($image_upload === null && $raw_image_upload === null) { $base64_image = self::getParameter('base64_image'); if (!is_string($base64_image) || trim($base64_image) === '') { $response->error('Image is required.'); } } $recognizer = new licenseplaterecognizer(false); try { if ($image_upload !== null) { $lpr_result = $recognizer->licenseplaterecognizerUploadFile($image_upload['path'], $image_upload['mime_type']); } elseif ($raw_image_upload !== null) { $lpr_result = $recognizer->licenseplaterecognizerUploadUncached($raw_image_upload['data'], $raw_image_upload['mime_type']); } else { $lpr_result = $recognizer->licenseplaterecognizer((string)$base64_image); } } finally { self::sendLPRServerTiming(array_merge($client_timings, $recognizer->getLastTimings()), $route_started_at); } if ($lpr_result['success']) { // Set the LICENSE_PLATE_NUMBER to uppercase and remove spaces. $lpr_result['license_plate_number'] = strtoupper(str_replace(' ', '', $lpr_result['license_plate_number'])); // Make sure the scanned plate is more than 3 characters long. $scannedPlateIsTooShort = strlen($lpr_result['license_plate_number']) <= 3; // If the confidence is below 90%, treat it as a recoverable scanner miss. if (isset($lpr_result['confidence']) && $lpr_result['confidence'] < 0.9 && !$scannedPlateIsTooShort) { $response->response(false, [ 'message' => 'License plate recognition confidence too low.', 'reason' => 'low_confidence_license_plate', 'confidence' => $lpr_result['confidence'], 'license_plate_number' => $lpr_result['license_plate_number'], ], 200); } // Success $response->success(['success' => true, 'license_plate_number' => $lpr_result['license_plate_number']]); } else { $response->response(false, [ 'message' => $lpr_result['message'] ?? 'No license plate detected.', 'reason' => 'no_license_plate_detected', ], 200); } }, [ 'modules_scanner_lpr' => 'License Plate Recognition', ] ); } private static function getLPRImageUpload(): ?array { global $response; if (!isset($_FILES[self::LPR_IMAGE_UPLOAD_FIELD]) || !is_array($_FILES[self::LPR_IMAGE_UPLOAD_FIELD])) { return null; } $file = $_FILES[self::LPR_IMAGE_UPLOAD_FIELD]; if (is_array($file['error'] ?? null)) { $response->error('Only one image can be uploaded.', 400); } $upload_error = (int)($file['error'] ?? UPLOAD_ERR_NO_FILE); if ($upload_error === UPLOAD_ERR_NO_FILE) { return null; } if ($upload_error !== UPLOAD_ERR_OK) { $response->error('Image upload failed.', 400); } $size = (int)($file['size'] ?? 0); if ($size <= 0) { $response->error('Image upload is empty.', 400); } if ($size > self::LPR_IMAGE_UPLOAD_MAX_BYTES) { $response->error('Image upload is too large.', 413); } $tmp_name = (string)($file['tmp_name'] ?? ''); if ($tmp_name === '' || !is_uploaded_file($tmp_name)) { $response->error('Image upload is invalid.', 400); } if (!is_readable($tmp_name)) { $response->error('Image upload could not be read.', 400); } $mime_type = self::detectLPRImageMimeType($file, $tmp_name); if (!str_starts_with($mime_type, 'image/')) { $response->error('Image upload must be an image.', 400); } return [ 'path' => $tmp_name, 'mime_type' => $mime_type, ]; } private static function getLPRRawImageUpload(): ?array { global $response; $mime_type = self::getRequestContentType(); if (!str_starts_with($mime_type, 'image/')) { return null; } $content_length = isset($_SERVER['CONTENT_LENGTH']) && is_numeric($_SERVER['CONTENT_LENGTH']) ? (int)$_SERVER['CONTENT_LENGTH'] : null; if ($content_length !== null && $content_length > self::LPR_IMAGE_UPLOAD_MAX_BYTES) { $response->error('Image upload is too large.', 413); } $image_data = file_get_contents('php://input'); if (!is_string($image_data) || $image_data === '') { $response->error('Image upload is empty.', 400); } if (strlen($image_data) > self::LPR_IMAGE_UPLOAD_MAX_BYTES) { $response->error('Image upload is too large.', 413); } return [ 'data' => $image_data, 'mime_type' => $mime_type, ]; } private static function getRequestContentType(): string { $content_type = (string)($_SERVER['CONTENT_TYPE'] ?? $_SERVER['HTTP_CONTENT_TYPE'] ?? ''); $content_type = strtolower(trim(explode(';', $content_type, 2)[0] ?? '')); return $content_type; } private static function detectLPRImageMimeType(array $file, string $tmp_name): string { $mime_type = trim((string)($file['type'] ?? '')); if ($mime_type !== '') { return $mime_type; } if (class_exists(\finfo::class)) { $finfo = new \finfo(FILEINFO_MIME_TYPE); $detected = $finfo->file($tmp_name); if (is_string($detected) && $detected !== '') { return $detected; } } return 'image/jpeg'; } private static function getLPRClientTimings(): array { $timings = []; $client_capture_ms = self::getNumericClientField( self::LPR_CLIENT_CAPTURE_MS_FIELD, self::LPR_CLIENT_CAPTURE_MS_HEADER, 0, self::LPR_CLIENT_CAPTURE_MAX_MS ); if ($client_capture_ms !== null) { $timings['client_capture'] = $client_capture_ms; } $client_preflight_ms = self::getNumericClientField( self::LPR_CLIENT_PREFLIGHT_MS_FIELD, self::LPR_CLIENT_PREFLIGHT_MS_HEADER, 0, self::LPR_CLIENT_CAPTURE_MAX_MS ); if ($client_preflight_ms !== null) { $timings['client_preflight'] = $client_preflight_ms; } $client_draw_ms = self::getNumericClientField( self::LPR_CLIENT_DRAW_MS_FIELD, self::LPR_CLIENT_DRAW_MS_HEADER, 0, self::LPR_CLIENT_CAPTURE_MAX_MS ); if ($client_draw_ms !== null) { $timings['client_draw'] = $client_draw_ms; } $client_encode_ms = self::getNumericClientField( self::LPR_CLIENT_ENCODE_MS_FIELD, self::LPR_CLIENT_ENCODE_MS_HEADER, 0, self::LPR_CLIENT_CAPTURE_MAX_MS ); if ($client_encode_ms !== null) { $timings['client_encode'] = $client_encode_ms; } $client_visual_fingerprint_ms = self::getNumericClientField( self::LPR_CLIENT_VISUAL_FINGERPRINT_MS_FIELD, self::LPR_CLIENT_VISUAL_FINGERPRINT_MS_HEADER, 0, self::LPR_CLIENT_CAPTURE_MAX_MS ); if ($client_visual_fingerprint_ms !== null) { $timings['client_visual_fingerprint'] = $client_visual_fingerprint_ms; } $client_frame_width = self::getNumericClientField( self::LPR_CLIENT_FRAME_WIDTH_FIELD, self::LPR_CLIENT_FRAME_WIDTH_HEADER, 1, self::LPR_CLIENT_FRAME_MAX_DIMENSION ); if ($client_frame_width !== null) { $timings['client_frame_width'] = $client_frame_width; } $client_frame_height = self::getNumericClientField( self::LPR_CLIENT_FRAME_HEIGHT_FIELD, self::LPR_CLIENT_FRAME_HEIGHT_HEADER, 1, self::LPR_CLIENT_FRAME_MAX_DIMENSION ); if ($client_frame_height !== null) { $timings['client_frame_height'] = $client_frame_height; } $client_frame_bytes = self::getNumericClientField( self::LPR_CLIENT_FRAME_BYTES_FIELD, self::LPR_CLIENT_FRAME_BYTES_HEADER, 1, self::LPR_IMAGE_UPLOAD_MAX_BYTES ); if ($client_frame_bytes !== null) { $timings['client_frame_bytes'] = $client_frame_bytes; } return $timings; } private static function getNumericClientField(string $field, string $server_header, float $min, float $max): ?float { $value = $_GET[$field] ?? null; if ($value === null) { $value = $_POST[$field] ?? null; } if ($value === null) { $value = $_SERVER[$server_header] ?? null; } if (is_array($value) || !is_numeric($value)) { return null; } $value = (float)$value; if ($value < $min || $value > $max) { return null; } return $value; } private static function sendLPRServerTiming(array $timings, float $route_started_at): void { if (headers_sent()) { return; } $parts = []; $timings['route_total'] = max(0, (microtime(true) - $route_started_at) * 1000); $timings['local'] = self::getLPRLocalDuration($timings); if (isset($_SERVER['REQUEST_TIME_FLOAT']) && is_numeric($_SERVER['REQUEST_TIME_FLOAT'])) { $timings['request_total'] = max(0, (microtime(true) - (float)$_SERVER['REQUEST_TIME_FLOAT']) * 1000); } foreach ([ 'client_capture', 'client_preflight', 'client_visual_fingerprint', 'client_draw', 'client_encode', 'client_frame_width', 'client_frame_height', 'client_frame_bytes', 'config', 'cache', 'cache_hit', 'cache_miss', 'local', 'payload', 'upstream_dns', 'upstream_connect', 'upstream_tls', 'upstream_pretransfer', 'upstream_ttfb', 'upstream_total', 'upstream_processing', 'upstream', 'parse', 'total', ] as $name) { if (!isset($timings[$name]) || !is_numeric($timings[$name])) { continue; } $parts[] = sprintf('lpr_%s;dur=%.3F', $name, (float)$timings[$name]); } foreach (['route_total', 'request_total'] as $name) { if (!isset($timings[$name]) || !is_numeric($timings[$name])) { continue; } $parts[] = sprintf('lpr_%s;dur=%.3F', $name, (float)$timings[$name]); } if ($parts !== []) { header('Server-Timing: ' . implode(', ', $parts)); } } private static function getLPRLocalDuration(array $timings): float { $upstream = null; foreach (['upstream', 'upstream_total'] as $name) { if (isset($timings[$name]) && is_numeric($timings[$name])) { $upstream = max(0, (float)$timings[$name]); break; } } return max(0, (float)$timings['route_total'] - ($upstream ?? 0)); } }