output(new QrCode($spd), $size, [255, 255, 255], [0, 0, 0]); } public static function spd( string $ucetNeboIban, float $castka, string $vs = '', ?\DateTimeInterface $splatnost = null, string $zprava = '', ): ?string { $iban = self::toIban($ucetNeboIban); if ($iban === null) { return null; } $parts = [ 'SPD', '1.0', 'ACC:' . $iban, ]; if ($castka > 0) { $parts[] = 'AM:' . number_format($castka, 2, '.', ''); } $parts[] = 'CC:CZK'; if ($splatnost !== null) { $parts[] = 'DT:' . $splatnost->format('Ymd'); } $msg = self::spdText($zprava, 60); if ($msg !== '') { $parts[] = 'MSG:' . $msg; } $vsDigits = self::variableSymbol($vs); if ($vsDigits !== '') { $parts[] = 'X-VS:' . $vsDigits; } return implode('*', $parts) . '*'; } /** * IBAN z IBAN řetězce nebo českého formátu „účet/kód banky“. */ public static function toIban(string $value): ?string { $value = strtoupper(preg_replace('/\s+/', '', trim($value)) ?? ''); if ($value === '') { return null; } if (preg_match('/^CZ\d{22}$/', $value)) { return self::isValidIban($value) ? $value : null; } if (!preg_match('/^(?:(\d{1,6})-)?(\d{2,10})\/(\d{4})$/', $value, $m)) { return null; } $prefix = str_pad($m[1] !== '' ? $m[1] : '0', 6, '0', STR_PAD_LEFT); $number = str_pad($m[2], 10, '0', STR_PAD_LEFT); $bank = $m[3]; $bban = $bank . $prefix . $number; // CZ = 12 35 → kontrolní číslo z BBAN + "123500" $checkBase = $bban . '123500'; $mod = self::mod97($checkBase); if ($mod === null) { return null; } $check = 98 - $mod; return 'CZ' . sprintf('%02d', $check) . $bban; } private static function variableSymbol(string $value): string { $digits = preg_replace('/\D+/', '', $value) ?? ''; if ($digits === '') { return ''; } if (strlen($digits) > 10) { $digits = substr($digits, -10); } return ltrim($digits, '0') !== '' ? $digits : ''; } private static function spdText(string $value, int $maxLen): string { $value = self::bezDiakritiky(trim($value)); $value = preg_replace('/[^A-Za-z0-9 \/?:().,+\-]/', '', $value) ?? ''; $value = trim(preg_replace('/\s+/', ' ', $value) ?? ''); return mb_substr($value, 0, $maxLen); } private static function bezDiakritiky(string $value): string { $ascii = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value); if ($ascii === false) { return $value; } return $ascii; } private static function isValidIban(string $iban): bool { $rearranged = substr($iban, 4) . substr($iban, 0, 4); $numeric = ''; $length = strlen($rearranged); for ($i = 0; $i < $length; $i++) { $ch = $rearranged[$i]; if ($ch >= 'A' && $ch <= 'Z') { $numeric .= (string) (ord($ch) - 55); } elseif ($ch >= '0' && $ch <= '9') { $numeric .= $ch; } else { return false; } } $mod = self::mod97($numeric); return $mod === 1; } private static function mod97(string $number): ?int { if ($number === '' || !ctype_digit($number)) { return null; } $remainder = 0; $length = strlen($number); for ($i = 0; $i < $length; $i++) { $remainder = ($remainder * 10 + (int) $number[$i]) % 97; } return $remainder; } }