first commit

This commit is contained in:
Claudecio Martins
2026-06-16 10:04:10 -03:00
commit a951944997
4463 changed files with 419677 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
# Ignora Variáveis de ambiente
.env
@@ -0,0 +1,111 @@
<?php
namespace WorkbloomERP\Module\v0\Auth\Controllers;
use KrothiumAPI\Utils\HttpUtil;
use WorkbloomERP\Utils\SanitizeUtil;
use WorkbloomERP\Exceptions\AppException;
use WorkbloomERP\Module\v0\Auth\Factories\ServiceFactory;
class AuthController {
/**
* Endpoint de API para autenticação de usuário (**Login**).
*
* Este método gerencia a interface HTTP (POST) para validar as credenciais do usuário.
* Ele atua como uma camada fina (*Thin Controller*), responsável por capturar o
* payload da requisição, sanitizar os dados de entrada para prevenir injeções
* e delegar a verificação de identidade e gestão de sessão/tokens para o `AuthService`.
*
* ---
* ## Fluxo de Execução
* 1. **Captura e Validação:** Extrai o corpo da requisição. Se o payload estiver vazio, interrompe com erro 400 (Bad Request).
* 2. **Sanitização:** Aplica `SanitizeUtil::string()` nos campos de login e senha para garantir a limpeza dos dados antes do processamento de negócio.
* 3. **Processamento de Negócio:** Invoca `AuthService::login()`, que realiza a consulta de credenciais.
* 4. **Normalização de Resposta:**
* - **Sucesso (200 OK):** Retorna os dados de sessão (ex: token de acesso).
* - **Tratamento de Erros:** Captura `AppException` (credenciais inválidas, conta bloqueada, etc.), formatando a resposta para que o frontend receba um objeto de erro estruturado.
*
* ---
* ## Observações Técnicas
* - A responsabilidade de verificar a hash da senha e gerar o token de acesso reside exclusivamente no `AuthService`, isolando a lógica de segurança do controlador.
* - Utiliza `HttpUtil` para garantir consistência no formato da resposta JSON, independentemente do sucesso ou falha da autenticação.
*
* @return void O método encerra o processamento enviando uma resposta HTTP JSON ao cliente.
* @see AuthService::login() Para os detalhes sobre a regra de autenticação e geração de tokens.
*/
public function login(): void {
try {
$form = HttpUtil::getRequestBody(form_type: 'POST');
if (empty($form)) {
throw new AppException(message: 'Requisição inválida.', code: 400);
}
$authService = ServiceFactory::makeAuthService();
$response = $authService->login(login: SanitizeUtil::string(value: $form['login']), senha: SanitizeUtil::string(value: $form['senha']));
HttpUtil::jsonResponse(
response_code: $response['response_code'] ?? 200,
message: $response['message'] ?? 'Login realizado com sucesso.',
output: $response['output'] ?? null
);
} catch(AppException $e) {
HttpUtil::jsonResponse(
response_code: $e->getCode() ?? 500,
message: $e->getMessage(),
output: $e->getDetails() ? ['errors' => $e->getDetails()] : null
);
}
}
/**
* Endpoint de API para a seleção de contexto de empresa ativa.
*
* Este método gerencia a interface HTTP (POST) para permitir que um usuário
* autenticado alterne sua empresa ativa no sistema multi-empresa. O controlador
* realiza a captura do payload, sanitiza o UUID da empresa e delega a
* complexa lógica de troca de sessão e regeneração de tokens ao `AuthService`.
*
* ---
* ## Fluxo de Execução
* 1. **Captura e Validação:** Extrai o corpo da requisição. Se o payload estiver vazio ou malformado, retorna erro 400 (Bad Request).
* 2. **Sanitização:** Aplica `SanitizeUtil::string()` no `empresa_uuid` para garantir a integridade dos dados recebidos.
* 3. **Processamento de Negócio:** Invoca `AuthService::selectCompany()`, que valida o acesso à empresa, gera um novo JWT e registra a sessão.
* 4. **Normalização de Resposta:**
* - **Sucesso (200 OK):** Retorna o novo token de acesso contendo o contexto da empresa selecionada.
* - **Tratamento de Erros:** Captura `AppException` (ex: empresa inexistente, falta de permissão), retornando uma resposta JSON estruturada com os detalhes do erro para o cliente.
*
* ---
* ## Arquitetura de Seleção de Contexto
*
*
* ---
* ## Observações Técnicas
* - Segue o padrão "Thin Controller", onde a orquestração de infraestrutura (cache, banco de dados, JWT) é encapsulada na camada de serviço.
* - Este endpoint é fundamental para aplicações multi-tenant ou multi-empresa, permitindo que o usuário altere seu "escopo" de atuação sem precisar realizar um re-login completo.
*
* @return void O método encerra o processamento enviando uma resposta HTTP JSON ao cliente.
* @see AuthService::selectCompany() Para os detalhes sobre a regra de negócio aplicada na troca de contexto.
*/
public function selectCompany(): void {
try {
$form = HttpUtil::getRequestBody(form_type: 'POST');
if (empty($form)) {
throw new AppException(message: 'Requisição inválida.', code: 400);
}
$authService = ServiceFactory::makeAuthService();
$response = $authService->selectCompany(empresa_uuid: SanitizeUtil::string(value: $form['empresa_uuid']));
HttpUtil::jsonResponse(
response_code: $response['response_code'] ?? 200,
message: $response['message'] ?? 'Empresa selecionada com sucesso.',
output: $response['output'] ?? null
);
} catch(AppException $e) {
HttpUtil::jsonResponse(
response_code: $e->getCode() ?? 500,
message: $e->getMessage(),
output: $e->getDetails() ? ['errors' => $e->getDetails()] : null
);
}
}
}
@@ -0,0 +1,29 @@
<?php
namespace WorkbloomERP\Module\v0\Auth\Factories;
use WorkbloomERP\Services\DBService;
use WorkbloomERP\Module\v0\Auth\Services\AuthService;
use WorkbloomERP\Module\v0\Empresa\Repos\EmpresaRepo;
use WorkbloomERP\Module\v0\Usuario\Repos\UsuarioRepo;
use WorkbloomERP\Module\v0\Usuario\Repos\UsuarioSessionRepo;
use WorkbloomERP\Module\v0\Usuario\Repos\UsuarioEmpresaRepo;
class ServiceFactory {
public static function makeAuthService() {
$db = new DBService();
return new AuthService(
db: $db,
usuarioRepo: new UsuarioRepo(db: $db),
empresaRepo: new EmpresaRepo(db: $db),
usuarioSessionRepo: new UsuarioSessionRepo(db: $db),
usuarioEmpresaRepo: new UsuarioEmpresaRepo(db: $db),
);
}
public static function makeUsuarioSessionRepo() {
$db = new DBService();
return new UsuarioSessionRepo(
db: $db
);
}
}
@@ -0,0 +1,217 @@
<?php
namespace WorkbloomERP\Module\v0\Auth\Middlewares;
use WorkbloomERP\Utils\JwtUtil;
use KrothiumAPI\Utils\HttpUtil;
use WorkbloomERP\Utils\CacheUtil;
use WorkbloomERP\Utils\CypherUtil;
use WorkbloomERP\Exceptions\AppException;
use WorkbloomERP\Module\v0\Auth\Factories\ServiceFactory;
use WorkbloomERP\Module\v0\Usuario\Models\UsuarioSessionModel;
class AuthMiddleware {
/**
* Valida se o token JWT da requisição atual corresponde a um estado de "Login Inicial".
*
* Este método atua como um guardião de segurança (Guard) para rotas que exigem
* ações obrigatórias do usuário logo após o primeiro acesso ao sistema, como a
* troca obrigatória de senha ou a configuração de MFA (Autenticação de Dois Fatores).
*
* ---
* ## Fluxo de Execução
* 1. **Extração:** Obtém o token JWT do cabeçalho `Authorization: Bearer`.
* 2. **Validação:** Verifica a integridade e assinatura do token através do `JwtUtil`.
* 3. **Verificação de Claim:** Decodifica o token e confirma se a claim `initial_login` está presente e definida como `true`.
* 4. **Retorno:**
* - Retorna `true` se o token for válido e corresponder ao estado de login inicial.
* - Retorna um `array` estruturado contendo detalhes do erro caso a validação falhe.
*
* ---
* ## Observações Técnicas
* - O método utiliza variáveis de ambiente (`$_ENV`) para configurar o `JwtUtil`, garantindo que as configurações de `secretKey`, `algorithm` e `ttl` estejam sincronizadas com a política de segurança da aplicação.
* - Este método captura internamente instâncias de `AppException`, tratando o fluxo de erro de forma silenciosa para o chamador, retornando um status de erro estruturado em vez de propagar a exceção.
*
* @return true|array{status: string, response_code: int, message: string}
* Retorna `true` em caso de sucesso. Retorna um array com o erro caso o token seja inválido, ausente ou não possua a claim necessária.
*/
public static function isInitialLogin(): true|array {
try {
// Valida o token JWT presente no header Authorization (Bearer Token)
$bearerToken = HttpUtil::getBearerToken();
if (!$bearerToken) {
throw new AppException(message: 'Token de acesso inválido ou ausente.', code: 401);
}
$jwtUtil = new JwtUtil(
secretKey: $_ENV['JWT_SECRET_KEY'], algorithm: $_ENV['JWT_ALGORITHM'],
ttl: $_ENV['JWT_EXPIRATION_TIME'], issuer: $_ENV['JWT_ISSUER']
);
$decodedToken = $jwtUtil->validate(token: $bearerToken);
if (!$decodedToken) {
throw new AppException(message: 'Token de acesso inválido ou ausente.', code: 401);
}
$decodedToken = json_decode(json: json_encode($decodedToken, flags: JSON_THROW_ON_ERROR), associative: true, flags: JSON_THROW_ON_ERROR);
$userData = $decodedToken['user_data'] ?? null;
if (!$userData) {
throw new AppException(message: 'Token de acesso inválido ou ausente.', code: 401);
}
if (!isset($userData['initial_login']) || $userData['initial_login'] !== true) {
throw new AppException(message: 'Token de acesso não corresponde a um login inicial.', code: 403);
}
return true;
} catch(AppException $e) {
return [
'status' => 'error',
'response_code' => $e->getCode() ?? 500,
'message' => $e->getMessage()
];
}
}
/**
* Middleware de autenticação para validar sessões e tokens JWT em requisições protegidas.
*
* Este método atua como um "Gatekeeper" (Guard). Ele implementa uma estratégia híbrida
* de validação (Cache-First, seguida de DB e JWT) para equilibrar performance com
* segurança. Ele verifica a existência do token, a validade da assinatura JWT,
* a revogação da sessão no banco de dados e a integridade do dispositivo (IP/User-Agent).
*
* ---
* ## Fluxo de Execução
* 1. **Extração:** Obtém o token do header `Authorization`. Retorna erro 401 se ausente.
* 2. **Cache-First Check:** Verifica no Redis (`login:main:{token}`). Se encontrado, autoriza imediatamente (retorna `null`), evitando carga no banco de dados.
* 3. **Validação de Persistência:** Consulta o `UsuarioSessionRepo` (via hash do token). Valida revogação (`revoked_at`) e integridade.
* 4. **Validação JWT:** Decodifica e verifica a assinatura/expiração (`exp`) do JWT.
* 5. **Validação de Integridade (Guard):** Invoca `validateSessionData` para garantir que o IP e User-Agent atuais coincidem com a sessão original.
* 6. **Caching:** Se tudo estiver correto, armazena o payload no cache para acelerar futuras requisições e retorna `null` (acesso concedido).
*
* ---
* ## Arquitetura de Validação
*
*
* ---
* ## Observações Técnicas
* - **Interface de Retorno:**
* - `null`: A autenticação foi bem-sucedida; o fluxo de execução pode continuar.
* - `array`: A autenticação falhou; o array contém a resposta de erro estruturada para o cliente.
* - **Segurança:** Utiliza `CypherUtil` para garantir que tokens sensíveis não sejam comparados em texto puro contra o banco de dados.
*
* @return array|null Retorna `null` se o usuário estiver autenticado e autorizado. Retorna um `array` com `response_code`, `message` e `output` em caso de erro.
* @see JwtUtil::validate() Para detalhes sobre a validação da assinatura criptográfica.
* @see self::validateSessionData() Para a verificação de segurança de IP e User-Agent.
*/
public static function handle(): ?array {
try {
// Valida o token JWT presente no header Authorization (Bearer Token)
$bearerToken = HttpUtil::getBearerToken();
if (!$bearerToken) {
throw new AppException(message: 'Token de acesso inválido ou ausente.', code: 401);
}
$jwtUtil = new JwtUtil(
secretKey: $_ENV['JWT_SECRET_KEY'], algorithm: $_ENV['JWT_ALGORITHM'],
ttl: $_ENV['JWT_EXPIRATION_TIME'], issuer: $_ENV['JWT_ISSUER']
);
// Valida o token JWT e decodifica seu payload para extrair os dados do usuário e as informações de sessão.
$decodedToken = $jwtUtil->validate(token: $bearerToken);
if (!$decodedToken) {
throw new AppException(message: 'Token de acesso inválido ou ausente.', code: 401);
}
// Verifica se o token corresponde a um login inicial, bloqueando o acesso a rotas protegidas caso seja verdadeiro.
$decodedToken = json_decode(json: json_encode($decodedToken, flags: JSON_THROW_ON_ERROR), associative: true, flags: JSON_THROW_ON_ERROR);
if ($decodedToken['user_data']['initial_login'] == true) {
throw new AppException(message: 'Acesso negado. O token corresponde a um login inicial e não pode ser usado para acessar esta rota.', code: 403);
}
// Verifica se a sessão principal já está em cache para evitar consultas desnecessárias ao banco de dados e validações repetitivas.
// Se a sessão estiver em cache, considera o token como válido e retorna null para permitir o acesso à rota protegida.
if (CacheUtil::get(key: "login:main:{$bearerToken}")) {
return null;
}
$usuarioSessionRepo = ServiceFactory::makeUsuarioSessionRepo();
// Busca a sessão do usuário no banco de dados utilizando o hash do token para garantir que o token é válido e não foi revogado.
$usuarioSessionModel = $usuarioSessionRepo->findByIdentifier(identifier: 'token_hash', value: CypherUtil::hash(data: $bearerToken));
if (!$usuarioSessionModel || !CypherUtil::verify(data: $bearerToken, hash: $usuarioSessionModel->getTokenHash()) || $usuarioSessionModel->getRevokedAt()) {
throw new AppException(message: 'Sessão inválida ou expirada. Faça login novamente.', code: 401);
}
// Valida o token JWT e decodifica seu payload para extrair os dados do usuário e as informações de sessão.
// Se o token for inválido, expirado ou tiver sua assinatura comprometida, uma exceção é lançada.
$decodedToken = $jwtUtil->validate(token: $bearerToken);
if (!$decodedToken) {
throw new AppException(message: 'Token de acesso inválido ou ausente.', code: 401);
}
$decodedToken = json_decode(json: json_encode($decodedToken, JSON_THROW_ON_ERROR), associative: true, flags: JSON_THROW_ON_ERROR);
$remainingTime = $decodedToken['exp'] - time();
if ($remainingTime <= 0) {
throw new AppException(message: 'Sessão expirada. Faça login novamente.', code: 401);
}
self::validateSessionData(sessionModel: $usuarioSessionModel, tokenData: $decodedToken);
CacheUtil::set(key: "login:main:{$bearerToken}", ttl: $remainingTime, value: $decodedToken);
return null;
} catch (AppException $e) {
return [
'status' => 'error',
'response_code' => $e->getCode() ?: 500,
'message' => $e->getMessage(),
'output' => $e->getDetails() ? ['errors' => $e->getDetails()] : null,
];
}
}
/**
* Valida a integridade e autenticidade da sessão do usuário comparando metadados.
*
* Este método atua como uma barreira de segurança (Guard) que garante que a sessão
* atual está sendo utilizada pelo mesmo dispositivo e rede que a iniciou. Ao comparar
* o IP e o User-Agent da requisição atual com os dados persistidos no banco de dados
* (`UsuarioSessionModel`) e os metadados contidos no token JWT (`$tokenData`),
* protegemos o sistema contra ataques de sequestro de sessão (session hijacking).
*
* ---
* ## Fluxo de Execução
* 1. **Captura:** Extrai o IP (`REMOTE_ADDR`) e o User-Agent (`HTTP_USER_AGENT`) da variável superglobal `$_SERVER`.
* 2. **Comparação (Match):** Utiliza uma expressão `match` para validar quatro pontos de convergência:
* - IP da Sessão (DB) vs. IP Atual.
* - User-Agent da Sessão (DB) vs. User-Agent Atual.
* - IP registrado no Payload do Token vs. IP Atual.
* - User-Agent registrado no Payload do Token vs. User-Agent Atual.
* 3. **Bloqueio:** Caso qualquer divergência seja detectada, a sessão é considerada inválida e uma `AppException` (401 Unauthorized) é disparada.
*
* ---
* ## Observações Técnicas
* - O uso de `match` (PHP 8+) torna o código altamente legível e performático para a verificação booleana múltipla.
* - Este método é fundamental em cenários onde a segurança de acesso exige que o usuário não troque de rede ou navegador enquanto a sessão estiver ativa.
* - Caso a requisição venha através de um proxy, certifique-se de que a configuração de captura de IP (`REMOTE_ADDR`) esteja ajustada corretamente no ambiente.
*
* @param UsuarioSessionModel $sessionModel O modelo contendo os dados da sessão persistidos no banco.
* @param array $tokenData O payload decodificado do token JWT contendo os metadados da sessão.
* @return void
* @throws AppException Caso haja qualquer incompatibilidade nos dados (IP ou User-Agent), invalidando a sessão.
*/
private static function validateSessionData(UsuarioSessionModel $sessionModel, array $tokenData): void {
$ipAddress = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
$isInvalidSession = match(true) {
$sessionModel->getIpAddress() !== $ipAddress,
$sessionModel->getUserAgent() !== $userAgent,
$tokenData['session_data']['ip_address'] !== $ipAddress,
$tokenData['session_data']['user_agent'] !== $userAgent => true,
default => false
};
if ($isInvalidSession) {
throw new AppException(message: 'Sessão inválida ou expirada. Faça login novamente.', code: 401);
}
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
use KrothiumAPI\Http\Router;
use WorkbloomERP\Module\v0\Auth\Middlewares\AuthMiddleware;
use WorkbloomERP\Module\v0\Auth\Controllers\AuthController;
Router::group(
prefix: '/auth',
callback: function() {
// Rota de autenticação
Router::post(uri: '/login', handler: [AuthController::class, 'login']);
// Rota para seleção de empresa após login inicial
Router::post(uri: '/select-company', handler: [AuthController::class, 'selectCompany'], middlewares: [[AuthMiddleware::class, 'isInitialLogin']]);
}
);
+289
View File
@@ -0,0 +1,289 @@
<?php
namespace WorkbloomERP\Module\v0\Auth\Services;
use Ramsey\Uuid\Uuid;
use DateTimeImmutable;
use KrothiumAPI\Utils\HttpUtil;
use WorkbloomERP\Utils\JwtUtil;
use WorkbloomERP\Utils\CacheUtil;
use WorkbloomERP\Utils\CypherUtil;
use WorkbloomERP\Services\DBService;
use WorkbloomERP\Exceptions\AppException;
use WorkbloomERP\Module\v0\Auth\Utils\AuthUtil;
use WorkbloomERP\Module\v0\Empresa\Repos\EmpresaRepo;
use WorkbloomERP\Module\v0\Usuario\Repos\UsuarioRepo;
use WorkbloomERP\Module\v0\Usuario\Repos\UsuarioSessionRepo;
use WorkbloomERP\Module\v0\Usuario\Repos\UsuarioEmpresaRepo;
use WorkbloomERP\Module\v0\Empresa\Models\EmpresaModel;
use WorkbloomERP\Module\v0\Usuario\Models\UsuarioModel;
use WorkbloomERP\Module\v0\Usuario\Models\UsuarioSessionModel;
use WorkbloomERP\Module\v0\Usuario\Models\UsuarioEmpresaModel;
class AuthService {
public function __construct(
private DBService $db,
private UsuarioRepo $usuarioRepo,
private EmpresaRepo $empresaRepo,
private UsuarioSessionRepo $usuarioSessionRepo,
private UsuarioEmpresaRepo $usuarioEmpresaRepo,
) {}
/**
* Realiza a autenticação de usuário e inicia uma sessão no sistema.
*
* Este método é o coração do processo de autenticação. Ele valida as credenciais
* fornecidas, gera um token JWT (JSON Web Token) para comunicação segura, persiste
* a sessão no banco de dados para fins de auditoria/rastreabilidade e armazena
* o token no cache (Redis) para validação rápida de requests subsequentes.
*
* ---
* ## Fluxo de Execução
* 1. **Validação de Entrada:** Verifica se `login` e `senha` foram fornecidos.
* 2. **Autenticação:** Busca o usuário pelo login e utiliza `password_verify` para validar a hash da senha.
* 3. **Geração de Token:** Instancia o `JwtUtil` com configurações de ambiente e gera um novo token JWT.
* 4. **Persistência de Sessão (Atomicidade):**
* - Insere o registro de sessão na tabela `usuario_session` (contendo dados como IP e User-Agent).
* - O hash do token é armazenado para permitir revogação futura.
* 5. **Caching:** Armazena o token no Redis com a mesma TTL da expiração do token para otimizar a validação de acesso.
*
* ---
* ## Observações Técnicas
* - **Transacionalidade:** Toda a criação de sessão e persistência ocorre dentro de uma transação de banco de dados, garantindo que não existam sessões órfãs caso o cache ou a criação do token falhem.
* - **Segurança:** O token JWT gerado é hasheado (`CypherUtil::hash`) antes de ser salvo no banco para evitar a exposição do token real em caso de vazamento de dados.
* - **Infraestrutura:** Requer acesso funcional a: `JWT_SECRET_KEY`, `JWT_ALGORITHM`, `JWT_EXPIRATION_TIME`, `JWT_ISSUER`.
*
* @param string $login O nome de usuário ou e-mail.
* @param string $senha A senha em texto puro fornecida pelo usuário.
* @return array{response_code: int, message: string, output: array{token: string}} Resposta estruturada contendo o código de sucesso e o token JWT.
* @throws AppException Caso as credenciais sejam inválidas (401), o payload esteja vazio (400) ou falhas críticas ocorram na geração de token/sessão (500).
*/
public function login(string $login, string $senha): array {
try {
// Validação básica de entrada
if (empty($login) || empty($senha)) {
throw new AppException(message: 'Login e senha são obrigatórios.', code: 400);
}
// Inicia uma transação para garantir a atomicidade das operações de autenticação
return $this->db->transaction(
callback: function() use ($login, $senha) {
$usuarioModel = $this->usuarioRepo->findByLogin(login: $login);
if (!$usuarioModel || !password_verify(password: $senha, hash: $usuarioModel->getSenhaHash())) {
throw new AppException(message: 'Login ou senha inválidos.', code: 401);
}
$payload = [
'user_data' => [
'initial_login' => true,
'uuid' => $usuarioModel->getUuid(),
'nome_usuario' => $usuarioModel->getNomeCompleto(),
'email' => $usuarioModel->getEmail(),
],
'exp' => (new DateTimeImmutable())->modify("+5 minutes")->getTimestamp(),
];
$jwtUtil = new JwtUtil(
secretKey: $_ENV['JWT_SECRET_KEY'], algorithm: $_ENV['JWT_ALGORITHM'],
ttl: $_ENV['JWT_EXPIRATION_TIME'], issuer: $_ENV['JWT_ISSUER']
);
$jwtToken = $jwtUtil->generate(payload: $payload);
if (!$jwtToken) {
throw new AppException(message: 'Erro ao gerar token de acesso.', code: 500);
}
// Armazena o token JWT no cache Redis com uma TTL de 10 minutos (600 segundos)
CacheUtil::set(key: "login:init:{$jwtToken}", ttl: 600, value: $payload);
return [
'response_code' => 200,
'message' => 'Login realizado com sucesso.',
'output' => [
'tmp_token' => $jwtToken,
'empresas' => $this->getEmpresasByUsuarioId($usuarioModel->getId())
]
];
}
);
} catch(AppException $e) {
throw $e;
}
}
/**
* Recupera e organiza a lista de empresas vinculadas a um determinado usuário.
*
* Este método realiza o mapeamento entre o ID do usuário e suas respectivas
* empresas. Ele filtra as associações através do repositório `usuarioEmpresaRepo`,
* hidrata os objetos `EmpresaModel` e organiza os dados em uma estrutura
* associativa agrupada pelo nome empresarial.
*
* ---
* ## Fluxo de Execução
* 1. **Busca:** Recupera todos os registros de associação `usuario_id` -> `empresa_id`.
* 2. **Hidratação:** Itera sobre as associações, buscando o `EmpresaModel` completo para cada ID encontrado.
* 3. **Estruturação:** Monta um array multidimensional onde as chaves são os nomes das empresas e os valores são arrays contendo metadados (UUID, nome, CNPJ, tipo formatado).
* 4. **Tratamento de Dados:** Normaliza o campo `tipo` (MATRIZ/FILIAL) para o formato "Capitalizado" (ex: Matriz).
*
* ---
* ## Observações Técnicas
* - Caso o usuário não possua empresas vinculadas ou o repositório retorne vazio, o método retorna um array vazio.
* - Ignora silenciosamente qualquer `empresa_id` que não resulte em um `EmpresaModel` válido (falha de integridade referencial).
* - O agrupamento pelo nome empresarial facilita o consumo da estrutura pelo frontend.
*
* @param int $usuario_id O identificador numérico interno do usuário.
* @return array<string, array<int, array{uuid: string, nome_empresarial: string, nome_fantasia: string, cnpj: string, tipo: string}>> Array estruturado de empresas.
* @throws AppException Caso ocorra falha de acesso aos repositórios.
*/
private function getEmpresasByUsuarioId(int $usuario_id): array {
try {
$empresas = $this->usuarioEmpresaRepo->findAllByUsuarioId(usuario_id: $usuario_id);
if (empty($empresas)) {
return ['Nenhuma empresa vinculada ao usuário.'];
}
$preparedEmpresas = [];
foreach($empresas as $i => $empresa) {
$empresaModel = $this->empresaRepo->findByIdentifier(identifier: 'id', value: $empresa['empresa_id']);
if (!$empresaModel) {
continue;
}
$preparedEmpresas[$empresaModel->getNomeEmpresarial()][] = [
'uuid' => $empresaModel->getUuid(),
'nome_empresarial' => $empresaModel->getNomeEmpresarial(),
'nome_fantasia' => $empresaModel->getNomeFantasia(),
'cnpj' => $empresaModel->getDocumentCnpj(),
'tipo' => ucfirst(string: strtolower(string: $empresaModel->getTipo()))
];
}
return $preparedEmpresas;
} catch(AppException $e) {
throw $e;
}
}
/**
* Seleciona e define a empresa ativa para o contexto da sessão do usuário.
*
* Este método é utilizado em sistemas multi-empresa onde o usuário precisa alternar
* entre diferentes entidades (empresas) sem realizar um novo login completo.
* O método valida se o usuário possui vínculo com a empresa solicitada, gera
* um novo token JWT com o contexto da empresa atualizado e registra essa nova
* sessão no banco de dados e no cache (Redis).
*
* ---
* ## Fluxo de Execução
* 1. **Verificação de Sessão:** Garante que o usuário já possui um login ativo (leitura via `AuthUtil`).
* 2. **Validação de Acesso:** Verifica se o usuário solicitado possui associação ativa com a empresa (`usuarioEmpresaRepo`).
* 3. **Contextualização:** Monta um novo payload contendo os dados do usuário e os dados da empresa selecionada.
* 4. **Token Rotation:** Gera um novo token JWT com os novos dados de contexto e define um tempo de expiração (60 minutos).
* 5. **Persistência:** Registra a nova sessão no banco de dados (`usuario_session`) e atualiza o estado no cache (`CacheUtil`).
*
* ---
* ## Observações Técnicas
* - **Token Rotation:** O método invalida implicitamente o contexto anterior ao gerar um novo token baseado na nova seleção.
* - **Atomicidade:** A operação ocorre dentro de uma transação, garantindo que se o token não for gerado ou a sessão não for criada, nada é alterado.
* - **Segurança:** O hash do token gerado é armazenado para permitir rastreabilidade e segurança.
*
* @param string $empresa_uuid O Identificador Único Universal da empresa que o usuário deseja selecionar.
* @return array{response_code: int, message: string, output: array{token: string}} Resposta estruturada contendo o novo token de acesso com o contexto atualizado.
* @throws AppException Caso a sessão seja inválida, a empresa não exista, o usuário não tenha permissão de acesso à empresa ou falhas de persistência ocorram.
*/
public function selectCompany(string $empresa_uuid) {
try {
return $this->db->transaction(
callback: function() use ($empresa_uuid) {
if (empty($empresa_uuid)) {
throw new AppException(message: 'UUID da empresa é obrigatório.', code: 400);
}
// Lê os dados da sessão atual para validar a existência de um login prévio e obter o UUID do usuário
$sessionData = AuthUtil::readInitSession();
if (!$sessionData || !isset($sessionData['user_data']['uuid'])) {
throw new AppException(message: 'Sessão de usuário não encontrada. Faça login novamente.', code: 401);
}
// Valida se o usuário tem acesso à empresa selecionada
$usuarioModel = $this->usuarioRepo->findByIdentifier(identifier: 'uuid', value: $sessionData['user_data']['uuid']);
if (!$usuarioModel) {
throw new AppException(message: 'Ocorreu um erro ao selecionar a empresa. Tente novamente mais tarde.', code: 404);
}
// Valida se a empresa existe e se o usuário tem associação com ela
$empresaModel = $this->empresaRepo->findByIdentifier(identifier: 'uuid', value: $empresa_uuid);
if (!$empresaModel || !$this->usuarioEmpresaRepo->checkAssociationByUsuarioIdAndEmpresaId(usuario_id: $usuarioModel->getId(), empresa_id: $empresaModel->getId())) {
throw new AppException(message: 'Empresa não encontrada.', code: 404);
}
// Monta o payload da sessão principal, incluindo os dados do usuário e da empresa selecionada
$payload = [
'user_data' => [
'initial_login' => false,
'uuid' => $usuarioModel->getUuid(),
'nome_completo' => $usuarioModel->getNomeCompleto(),
'nome_usuario' => $usuarioModel->getNomeUsuario(),
'email' => $usuarioModel->getEmail(),
'is_root' => $usuarioModel->getIsRoot() ? true : false
],
'empresa_data' => [
'uuid' => $empresaModel->getUuid(),
'nome_empresarial' => $empresaModel->getNomeEmpresarial(),
'nome_fantasia' => $empresaModel->getNomeFantasia(),
'cnpj' => $empresaModel->getDocumentCnpj(),
'tipo' => ucfirst(string: strtolower(string: $empresaModel->getTipo()))
],
'session_data' => [
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
],
'iat' => (new DateTimeImmutable())->getTimestamp(),
'exp' => (new DateTimeImmutable())->modify("+{$_ENV['JWT_EXPIRATION_TIME']} seconds")->getTimestamp(),
];
$jwtUtil = new JwtUtil(
secretKey: $_ENV['JWT_SECRET_KEY'], algorithm: $_ENV['JWT_ALGORITHM'],
ttl: $_ENV['JWT_EXPIRATION_TIME'], issuer: $_ENV['JWT_ISSUER']
);
$jwtToken = $jwtUtil->generate(payload: $payload);
if (!$jwtToken) {
throw new AppException(message: 'Erro ao gerar token de acesso.', code: 500);
}
$usuarioSessionModel = $this->usuarioSessionRepo->insert(
new UsuarioSessionModel(
uuid: Uuid::uuid7()->toString(),
usuario_id: $usuarioModel->getId(),
token_hash: CypherUtil::hash(data: $jwtToken),
ip_address: $payload['session_data']['ip_address'],
user_agent: $payload['session_data']['user_agent'],
created_at: new DateTimeImmutable()
)
);
if (!$usuarioSessionModel) {
throw new AppException(message: 'Erro ao criar sessão de usuário.', code: 500);
}
if (!CacheUtil::set(key: "login:main:{$jwtToken}", ttl: $_ENV['JWT_EXPIRATION_TIME'], value: $payload)) {
throw new AppException(message: 'Erro ao armazenar sessão no cache.', code: 500);
}
$beforeToken = HttpUtil::getBearerToken();
if (!CacheUtil::delete(keys: ["login:init:{$beforeToken}"])) {
throw new AppException(message: 'Erro ao limpar sessão temporária do cache.', code: 500);
}
return [
'response_code' => 200,
'message' => 'Empresa selecionada com sucesso.',
'output' => [
'token' => $jwtToken
]
];
}
);
} catch(AppException $e) {
throw $e;
}
}
}
+83
View File
@@ -0,0 +1,83 @@
<?php
namespace WorkbloomERP\Module\v0\Auth\Utils;
use KrothiumAPI\Utils\HttpUtil;
use WorkbloomERP\Utils\JwtUtil;
use WorkbloomERP\Utils\CacheUtil;
use WorkbloomERP\Utils\CryptoUtil;
use WorkbloomERP\Module\v0\Usuario\Models\UsuarioModel;
use WorkbloomERP\Module\v0\Auth\Factories\ServiceFactory;
use WorkbloomERP\Module\v0\Usuario\Models\UsuarioSessionModel;
class AuthUtil {
/**
* Recupera os dados de uma sessão de "Login Inicial" a partir do cache.
*
* Este método é utilizado para verificar se existe uma sessão ativa vinculada ao
* token de acesso atual no fluxo de "Primeiro Acesso" (Initial Login). Ele permite
* identificar rapidamente se o usuário está em um estado que exige ações
* obrigatórias, como a alteração de senha inicial ou configuração de MFA,
* sem a necessidade de consultar o banco de dados.
*
* ---
* ## Fluxo de Execução
* 1. **Identificação:** Extrai o token Bearer da requisição atual (`HttpUtil::getBearerToken()`).
* 2. **Busca em Cache:** Consulta o serviço de cache (Redis) utilizando a chave prefixada `login:init:{token}`.
* 3. **Retorno:** Devolve os dados da sessão (array) caso o registro exista e esteja ativo; retorna `null` caso a sessão não exista ou tenha expirado.
*
* ---
* ## Observações Técnicas
* - **Performance:** Este método é altamente performático por consultar exclusivamente a camada de cache, sendo ideal para middlewares de validação de fluxo de login.
* - **Segurança:** A chave de busca utiliza o próprio token Bearer, garantindo que o acesso aos dados da sessão inicial seja restrito ao detentor do token correspondente.
* - **Integração:** Este método deve ser utilizado em conjunto com o fluxo de autenticação que persiste os dados do login inicial no cache.
*
* @return array|null Retorna o payload da sessão de login inicial se encontrada; retorna `null` se a sessão não existir no cache.
*/
public static function readInitSession(): ?array {
// Verifica se existe um login inicial
$bearerToken = HttpUtil::getBearerToken();
$initSession = CacheUtil::get(key: "login:init:{$bearerToken}");
if ($initSession) {
return $initSession;
}
return null;
}
/**
* Recupera a sessão principal do usuário ou uma chave específica do payload armazenado em cache.
*
* Este método centraliza o acesso aos dados de sessão ativos. Ele utiliza o
* token Bearer da requisição atual para localizar o payload no cache (Redis) e,
* caso uma chave específica seja fornecida, retorna apenas o subconjunto de dados
* solicitado, otimizando o acesso às informações de contexto.
*
* ---
* ## Fluxo de Execução
* 1. **Identificação:** Obtém o token de acesso (Bearer Token) da requisição atual via `HttpUtil`.
* 2. **Busca:** Consulta o cache usando a chave padronizada `login:main:{token}`.
* 3. **Filtragem:**
* - Se uma `$key` for fornecida e existir no payload, retorna apenas o valor correspondente (ex: `user_data`).
* - Se nenhuma chave for fornecida ou a chave não existir, retorna o payload completo.
* 4. **Retorno:** Devolve `null` caso a sessão não esteja em cache (sessão inexistente ou expirada).
*
* ---
* ## Observações Técnicas
* - **Performance:** A consulta ao cache é direta e rápida. O método evita redundância ao permitir filtrar os dados necessários logo na leitura.
* - **Flexibilidade:** A inclusão do parâmetro `$key` permite que outros serviços solicitem apenas o bloco de dados que precisam (ex: dados da empresa ou do usuário), reduzindo o processamento desnecessário de arrays grandes.
*
* @param string $key Chave específica a ser extraída do payload da sessão (opcional; se vazio, retorna o payload completo).
* @return array|null Retorna o payload da sessão ou um subconjunto dele; retorna `null` se a sessão não existir no cache.
*/
public static function readSession(string $key): ?array {
$bearerToken = HttpUtil::getBearerToken();
$result = CacheUtil::get(key: "login:main:{$bearerToken}");
if (!$result) {return null;}
if ($key && isset($result[$key])) {
return $result[$key];
}
return $result;
}
}
@@ -0,0 +1,22 @@
<?php
namespace WorkbloomERP\Module\v0\Contato\Constants;
class ContribuinteICMSConst {
public const CONTATO_CONTRIBUINTE_ICMS = [
1 => 'Contribuinte ICMS',
2 => 'Contribuinte Isento de Inscrição no Cadastro de Contribuintes',
9 => 'Não Contribuinte, que pode ou não possuir Inscrição Estadual no Cadastro de Contribuintes'
];
public static function getAll(): array {
return self::CONTATO_CONTRIBUINTE_ICMS;
}
public static function isValid(string $value): bool {
return array_key_exists(key: $value, array: self::CONTATO_CONTRIBUINTE_ICMS);
}
public static function getDescription(string $key): ?string {
return self::CONTATO_CONTRIBUINTE_ICMS[$key] ?? null;
}
}
@@ -0,0 +1,23 @@
<?php
namespace WorkbloomERP\Module\v0\Contato\Constants;
class NFSeConsumoIbsCbsConst {
public const CONTATO_USO_CONSUMO_IBS_CBS = [
'1' => 'Sim (Consumidor Final)',
'2' => 'Não informar',
'0' => 'Não (Operação B2B)',
];
public static function getAll(): array {
return self::CONTATO_USO_CONSUMO_IBS_CBS;
}
public static function getDescription(string|int|null $key): string {
return self::CONTATO_USO_CONSUMO_IBS_CBS[$key] ?? 'Valor desconhecido';
}
public static function isValid(string|int|null $key): bool {
return array_key_exists(key: $key, array: self::CONTATO_USO_CONSUMO_IBS_CBS);
}
}
@@ -0,0 +1,23 @@
<?php
namespace WorkbloomERP\Module\v0\Contato\Constants;
class OrgaoPublicoConst {
public const CONTATO_ORGAO_PUBLICO = [
'NAO' => 'Não',
'FEDERAL' => 'Órgão Público Federal',
'ESTADUAL' => 'Órgão Público Estadual',
'MUNICIPAL' => 'Órgão Público Municipal'
];
public static function getAll(): array {
return self::CONTATO_ORGAO_PUBLICO;
}
public static function isValid(string $value): bool {
return array_key_exists(key: $value, array: self::CONTATO_ORGAO_PUBLICO);
}
public static function getDescription(string $key): ?string {
return self::CONTATO_ORGAO_PUBLICO[$key] ?? null;
}
}
@@ -0,0 +1,22 @@
<?php
namespace WorkbloomERP\Module\v0\Contato\Constants;
class PersonalidadeConst {
public const CONTATO_PERSONALIDADE = [
'PF' => 'Pessoa Física',
'PJ' => 'Pessoa Jurídica',
'EX' => 'Estrangeiro'
];
public static function getAll(): array {
return self::CONTATO_PERSONALIDADE;
}
public static function isValid(string $value): bool {
return array_key_exists(key: $value, array: self::CONTATO_PERSONALIDADE);
}
public static function getDescription(string $key): ?string {
return self::CONTATO_PERSONALIDADE[$key] ?? null;
}
}
@@ -0,0 +1,21 @@
<?php
namespace WorkbloomERP\Module\v0\Contato\Constants;
class TipoConst {
public const CONTATO_TIPO = [
'CLIENTE' => 'Cliente',
'FORNECEDOR' => 'Fornecedor'
];
public static function getAll(): array {
return self::CONTATO_TIPO;
}
public static function isValid(string $value): bool {
return array_key_exists(key: $value, array: self::CONTATO_TIPO);
}
public static function getDescription(string $key): ?string {
return self::CONTATO_TIPO[$key] ?? null;
}
}
@@ -0,0 +1,107 @@
<?php
namespace WorkbloomERP\Module\v0\Contato\Controllers;
use KrothiumAPI\Utils\HttpUtil;
use WorkbloomERP\Utils\SanitizeUtil;
use WorkbloomERP\Exceptions\AppException;
use WorkbloomERP\Module\v0\Contato\DTOs\ContatoCreateDTO;
use WorkbloomERP\Module\v0\Contato\Factories\ContatoServiceFactory;
class ContatoController {
public function createOptions(): void {
try {
$contatoService = ContatoServiceFactory::makeContatoService();
$options = $contatoService->createOptions();
HttpUtil::jsonResponse(
response_code: 200,
message: 'Opções para criação de contato.',
output: ['options' => $options]
);
} catch (AppException $e) {
HttpUtil::jsonResponse(
response_code: $e->getCode() ?? 500,
message: $e->getMessage() ?? 'Ocorreu um erro ao obter as opções de contato.',
output: $e->getDetails() ? ['errors' => $e->getDetails()] : null
);
}
}
/**
* Endpoint de API para a criação de um novo **Contato**.
*
* Este controlador atua como o ponto de entrada para o registro de novos contatos
* (parceiros de negócio, fornecedores ou clientes) no sistema. O método é
* responsável por extrair os dados da requisição HTTP (POST), aplicar a sanitização
* necessária para garantir a integridade dos dados e delegar a criação à camada de
* serviço utilizando um `ContatoCreateDTO`.
*
* ---
* ## Fluxo de Execução
* 1. **Captura e Validação:** Obtém o corpo da requisição (JSON/Form). Valida a presença de dados, retornando erro 422 caso esteja vazio.
* 2. **Normalização (DTO):** Instancia um `ContatoCreateDTO` populando-o com os dados sanitizados através de `SanitizeUtil`.
* 3. **Orquestração de Serviço:** Invoca `ContatoService::create()` para validar regras de negócio, persistência e possíveis integrações.
* 4. **Resposta:** Retorna um JSON estruturado com o status da operação (201 para sucesso ou status de erro correspondente).
*
* ---
* ## Observações Técnicas
* - O controlador segue o padrão de "Thin Controller", garantindo que as regras de validação complexas e a persistência de dados ocorram na camada de serviço.
* - Todos os campos de entrada são submetidos a métodos de `SanitizeUtil` para prevenir injeções e garantir o tipo esperado antes de atingir o DTO.
* - Utiliza `ContatoServiceFactory` para a injeção de dependência do serviço.
*
* @return void O método encerra o processamento enviando uma resposta HTTP JSON ao cliente.
* @see ContatoCreateDTO Para a estrutura de dados esperada para a criação.
* @see ContatoService::create() Para a lógica de negócio de persistência do contato.
*/
public function create(): void {
try {
$form = HttpUtil::getRequestBody(form_type: 'POST');
if (empty($form)) {
throw new AppException(message: 'Requisição inválida.', code: 422);
}
$contaoService = ContatoServiceFactory::makeContatoService();
$response = $contaoService->create(
contatoCreateDTO: new ContatoCreateDTO(
is_active: SanitizeUtil::boolean(value: $form['is_active'] ?? null),
tipo: SanitizeUtil::string(value: $form['tipo'] ?? null),
nome_empresarial: SanitizeUtil::string(value: $form['nome_empresarial'] ?? null),
nome_fantasia: SanitizeUtil::string(value: $form['nome_fantasia'] ?? null),
personalidade: SanitizeUtil::string(value: $form['personalidade'] ?? null),
document_cpf: SanitizeUtil::string(value: $form['document_cpf'] ?? null),
document_cnpj: SanitizeUtil::string(value: $form['document_cnpj'] ?? null),
regime_tributario: SanitizeUtil::int(value: $form['regime_tributario'] ?? null),
contribuinte_icms: SanitizeUtil::int(value: $form['contribuinte_icms'] ?? null),
orgao_publico: SanitizeUtil::string(value: $form['orgao_publico'] ?? null),
document_ie: SanitizeUtil::string(value: $form['document_ie'] ?? null),
document_im: SanitizeUtil::string(value: $form['document_im'] ?? null),
document_is: SanitizeUtil::string(value: $form['document_is'] ?? null),
end_cep: SanitizeUtil::string(value: $form['end_cep'] ?? null),
end_ibge: SanitizeUtil::string(value: $form['end_ibge'] ?? null),
end_logradouro: SanitizeUtil::string(value: $form['end_logradouro'] ?? null),
end_numero: SanitizeUtil::string(value: $form['end_numero'] ?? null),
end_complemento: SanitizeUtil::string(value: $form['end_complemento'] ?? null),
end_bairro: SanitizeUtil::string(value: $form['end_bairro'] ?? null),
end_cidade: SanitizeUtil::string(value: $form['end_cidade'] ?? null),
end_uf: SanitizeUtil::string(value: $form['end_uf'] ?? null),
info_email: SanitizeUtil::string(value: $form['info_email'] ?? null),
info_email_nfe: SanitizeUtil::string(value: $form['info_email_nfe'] ?? null),
info_observacao: SanitizeUtil::string(value: $form['info_observacao'] ?? null),
info_telefone: SanitizeUtil::string(value: $form['info_telefone'] ?? null),
info_uso_consumo_ibs_cbs: SanitizeUtil::string(value: $form['info_uso_consumo_ibs_cbs'] ?? null),
)
);
HttpUtil::jsonResponse(
response_code: $response['response_code'] ?? 201,
message: $response['message'] ?? 'Contato criado com sucesso.',
output: $response['output'] ?? null
);
} catch (AppException $e) {
HttpUtil::jsonResponse(
response_code: $e->getCode() ?? 500,
message: $e->getMessage() ?? 'Ocorreu um erro ao criar o contato.',
output: $e->getDetails() ? ['errors' => $e->getDetails()] : null
);
}
}
}
@@ -0,0 +1,246 @@
<?php
namespace WorkbloomERP\Module\v0\Contato\DTOs;
class ContatoCreateDTO {
public function __construct(
private ?int $is_active = null,
private ?string $tipo = null,
private ?string $nome_empresarial = null,
private ?string $nome_fantasia = null,
private ?string $personalidade = null,
private ?string $document_cpf = null,
private ?string $document_cnpj = null,
private ?int $regime_tributario = null,
private ?int $contribuinte_icms = null,
private ?string $orgao_publico = null,
private ?string $document_ie = null,
private ?string $document_im = null,
private ?string $document_is = null,
private ?string $end_cep = null,
private ?string $end_ibge = null,
private ?string $end_logradouro = null,
private ?string $end_numero = null,
private ?string $end_complemento = null,
private ?string $end_bairro = null,
private ?string $end_cidade = null,
private ?string $end_uf = null,
private ?string $info_email = null,
private ?string $info_email_nfe = null,
private ?string $info_observacao = null,
private ?string $info_telefone = null,
private ?string $info_uso_consumo_ibs_cbs = null,
) {}
public function toArray(): array {
return [
'is_active' => $this->getIsActive(),
'tipo' => $this->getTipo(),
'nome_empresarial' => $this->getNomeEmpresarial(),
'nome_fantasia' => $this->getNomeFantasia(),
'personalidade' => $this->getPersonalidade(),
'document_cpf' => $this->getDocumentCpf(),
'document_cnpj' => $this->getDocumentCnpj(),
'regime_tributario' => $this->getRegimeTributario(),
'contribuinte_icms' => $this->getContribuinteIcms(),
'orgao_publico' => $this->getOrgaoPublico(),
'document_ie' => $this->getDocumentIe(),
'document_im' => $this->getDocumentIm(),
'document_is' => $this->getDocumentIs(),
'end_cep' => $this->getEndCep(),
'end_ibge' => $this->getEndIbge(),
'end_logradouro' => $this->getEndLogradouro(),
'end_numero' => $this->getEndNumero(),
'end_complemento' => $this->getEndComplemento(),
'end_bairro' => $this->getEndBairro(),
'end_cidade' => $this->getEndCidade(),
'end_uf' => $this->getEndUf(),
'info_email' => $this->getInfoEmail(),
'info_email_nfe' => $this->getInfoEmailNfe(),
'info_observacao' => $this->getInfoObservacao(),
'info_telefone' => $this->getInfoTelefone(),
'info_uso_consumo_ibs_cbs' => $this->getInfoUsoConsumoIbsCbs(),
];
}
public function setIsActive(?int $is_active): void {
$this->is_active = $is_active;
}
public function getIsActive(): ?int {
return $this->is_active;
}
public function setTipo(?string $tipo): void {
$this->tipo = $tipo;
}
public function getTipo(): ?string {
return $this->tipo;
}
public function setNomeEmpresarial(?string $nome_empresarial): void {
$this->nome_empresarial = $nome_empresarial;
}
public function getNomeEmpresarial(): ?string {
return $this->nome_empresarial;
}
public function setNomeFantasia(?string $nome_fantasia): void {
$this->nome_fantasia = $nome_fantasia;
}
public function getNomeFantasia(): ?string {
return $this->nome_fantasia;
}
public function setPersonalidade(?string $personalidade): void {
$this->personalidade = $personalidade;
}
public function getPersonalidade(): ?string {
return $this->personalidade;
}
public function setDocumentCpf(?string $document_cpf): void {
$this->document_cpf = $document_cpf;
}
public function getDocumentCpf(): ?string {
return $this->document_cpf;
}
public function setDocumentCnpj(?string $document_cnpj): void {
$this->document_cnpj = $document_cnpj;
}
public function getDocumentCnpj(): ?string {
return $this->document_cnpj;
}
public function setRegimeTributario(?int $regime_tributario): void {
$this->regime_tributario = $regime_tributario;
}
public function getRegimeTributario(): ?int {
return $this->regime_tributario;
}
public function setContribuinteIcms(?int $contribuinte_icms): void {
$this->contribuinte_icms = $contribuinte_icms;
}
public function getContribuinteIcms(): ?int {
return $this->contribuinte_icms;
}
public function setOrgaoPublico(?string $orgao_publico): void {
$this->orgao_publico = $orgao_publico;
}
public function getOrgaoPublico(): ?string {
return $this->orgao_publico;
}
public function setDocumentIe(?string $document_ie): void {
$this->document_ie = $document_ie;
}
public function getDocumentIe(): ?string {
return $this->document_ie;
}
public function setDocumentIm(?string $document_im): void {
$this->document_im = $document_im;
}
public function getDocumentIm(): ?string {
return $this->document_im;
}
public function setDocumentIs(?string $document_is): void {
$this->document_is = $document_is;
}
public function getDocumentIs(): ?string {
return $this->document_is;
}
public function setEndCep(?string $end_cep): void {
$this->end_cep = $end_cep;
}
public function getEndCep(): ?string {
return $this->end_cep;
}
public function setEndIbge(?string $end_ibge): void {
$this->end_ibge = $end_ibge;
}
public function getEndIbge(): ?string {
return $this->end_ibge;
}
public function setEndLogradouro(?string $end_logradouro): void {
$this->end_logradouro = $end_logradouro;
}
public function getEndLogradouro(): ?string {
return $this->end_logradouro;
}
public function setEndNumero(?string $end_numero): void {
$this->end_numero = $end_numero;
}
public function getEndNumero(): ?string {
return $this->end_numero;
}
public function setEndComplemento(?string $end_complemento): void {
$this->end_complemento = $end_complemento;
}
public function getEndComplemento(): ?string {
return $this->end_complemento;
}
public function setEndBairro(?string $end_bairro): void {
$this->end_bairro = $end_bairro;
}
public function getEndBairro(): ?string {
return $this->end_bairro;
}
public function setEndCidade(?string $end_cidade): void {
$this->end_cidade = $end_cidade;
}
public function getEndCidade(): ?string {
return $this->end_cidade;
}
public function setEndUf(?string $end_uf): void {
$this->end_uf = $end_uf;
}
public function getEndUf(): ?string {
return $this->end_uf;
}
public function setInfoEmail(?string $info_email): void {
$this->info_email = $info_email;
}
public function getInfoEmail(): ?string {
return $this->info_email;
}
public function setInfoEmailNfe(?string $info_email_nfe): void {
$this->info_email_nfe = $info_email_nfe;
}
public function getInfoEmailNfe(): ?string {
return $this->info_email_nfe;
}
public function setInfoObservacao(?string $info_observacao): void {
$this->info_observacao = $info_observacao;
}
public function getInfoObservacao(): ?string {
return $this->info_observacao;
}
public function setInfoTelefone(?string $info_telefone): void {
$this->info_telefone = $info_telefone;
}
public function getInfoTelefone(): ?string {
return $this->info_telefone;
}
public function setInfoUsoConsumoIbsCbs(?string $info_uso_consumo_ibs_cbs): void {
$this->info_uso_consumo_ibs_cbs = $info_uso_consumo_ibs_cbs;
}
public function getInfoUsoConsumoIbsCbs(): ?string {
return $this->info_uso_consumo_ibs_cbs;
}
}
@@ -0,0 +1,173 @@
<?php
namespace WorkbloomERP\Module\v0\Contato\Factories;
use DateTimeImmutable;
use Ramsey\Uuid\Uuid;
use WorkbloomERP\Services\DBService;
use WorkbloomERP\Utils\ValidateUtil;
use WorkbloomERP\Exceptions\AppException;
use WorkbloomERP\Module\v0\Empresa\Repos\EmpresaRepo;
use WorkbloomERP\Module\v0\Contato\Repos\ContatoRepo;
use WorkbloomERP\Module\v0\Contato\Models\ContatoModel;
use WorkbloomERP\Module\v0\Empresa\Models\EmpresaModel;
use WorkbloomERP\Module\v0\Contato\DTOs\ContatoCreateDTO;
// Constantes para opções de contato
use WorkbloomERP\Constants\SpedCRTConst;
use WorkbloomERP\Constants\BrasilUfsConst;
use WorkbloomERP\Constants\SpedPaisesConst;
use WorkbloomERP\Module\v0\Contato\Constants\OrgaoPublicoConst;
use WorkbloomERP\Module\v0\Contato\Constants\PersonalidadeConst;
use WorkbloomERP\Module\v0\Contato\Constants\ContribuinteICMSConst;
use WorkbloomERP\Module\v0\Contato\Constants\NFSeConsumoIbsCbsConst;
use WorkbloomERP\Module\v0\Contato\Constants\TipoConst;
class ContatoFactory {
public function __construct(private EmpresaModel $empresaModel, private ContatoCreateDTO $contatoCreateDTO, private ContatoRepo $contatoRepo, private EmpresaRepo $empresaRepo, private DBService $db) {
if (!$empresaModel) {
throw new AppException(message: 'Empresa não encontrada.', code: 404);
}
if (!$contatoCreateDTO) {
throw new AppException(message: 'Dados do contato inválidos.', code: 400);
}
}
public function create(ContatoCreateDTO $contatoCreateDTO, EmpresaModel $empresaModel): ?ContatoModel {
try {
// Valida os campos comuns a ambos os tipos de contato (PF e PJ)
$this->validateCommonFields(contatoCreateDTO: $contatoCreateDTO);
// Cria o contato com base na personalidade (PF ou PJ)
$contatoModel = match($contatoCreateDTO->getPersonalidade()) {
'PF' => $this->createPF(contatoCreateDTO: $contatoCreateDTO, empresaModel: $empresaModel),
'PJ' => $this->createPJ(contatoCreateDTO: $contatoCreateDTO, empresaModel: $empresaModel)
};
return $contatoModel;
} catch(AppException $e) {
throw $e;
}
}
private function validateCommonFields(ContatoCreateDTO $contatoCreateDTO): void {
$errors = [];
$commonFields = [
'is_active',
'tipo',
'nome_empresarial',
'personalidade',
'regime_tributario',
'contribuinte_icms',
'orgao_publico',
'end_cep',
'end_logradouro',
'end_numero',
'end_bairro',
'end_cidade',
'end_uf',
'info_email',
'info_email_nfe',
'info_telefone',
'info_uso_consumo_ibs_cbs',
];
foreach ($commonFields as $field) {
if (!array_key_exists(key: $field, array: $contatoCreateDTO->toArray()) || $contatoCreateDTO->toArray()[$field] === '') {
$errors[$field] = 'Campo obrigatório.';
}
}
// Verifica se o campo 'tipo' tem um valor válido
$tipo = $contatoCreateDTO->getTipo() ? strtoupper(string: $contatoCreateDTO->getTipo()) : null;
if ($tipo && !TipoConst::isValid(value: $tipo)) {
$errors['tipo'] = "Valor inválido.";
}
// Verifica se o campo 'personalidade' tem um valor válido
$personalidade = $contatoCreateDTO->getPersonalidade() ? strtoupper(string: $contatoCreateDTO->getPersonalidade()) : null;
if ($personalidade && !PersonalidadeConst::isValid(value: $personalidade)) {
$errors['personalidade'] = "Valor inválido.";
}
// Valida o regime tributário
if (!in_array($contatoCreateDTO->getRegimeTributario(), [1, 2, 3, 4, null], true)) {
$errors['regime_tributario'] = "Valor inválido.";
}
if ($errors) {
throw new AppException(message: 'Dados de contato inválidos.',code: 422, details: $errors);
}
}
private function createPF(ContatoCreateDTO $contatoCreateDTO, EmpresaModel $empresaModel): ContatoModel {
try {
// Valida os campos específicos para pessoa física
$this->validatePFFields(contatoCreateDTO: $contatoCreateDTO, empresaModel: $empresaModel);
$contatoModel = new ContatoModel();
$contatoModel->setUuid(Uuid::uuid7()->toString());
$contatoModel->setEmpresaId($empresaModel->getId());
$contatoModel->setIsActive($contatoCreateDTO->getIsActive());
$contatoModel->setTipo($contatoCreateDTO->getTipo());
$contatoModel->setNomeEmpresarial($contatoCreateDTO->getNomeEmpresarial());
$contatoModel->setPersonalidade($contatoCreateDTO->getPersonalidade());
$contatoModel->setRegimeTributario($contatoCreateDTO->getRegimeTributario());
$contatoModel->setContribuinteIcms($contatoCreateDTO->getContribuinteIcms());
$contatoModel->setOrgaoPublico($contatoCreateDTO->getOrgaoPublico());
$contatoModel->setEndCep($contatoCreateDTO->getEndCep());
$contatoModel->setEndIbge($contatoCreateDTO->getEndIbge());
$contatoModel->setEndLogradouro($contatoCreateDTO->getEndLogradouro());
$contatoModel->setEndNumero($contatoCreateDTO->getEndNumero());
$contatoModel->setEndBairro($contatoCreateDTO->getEndBairro());
$contatoModel->setEndCidade($contatoCreateDTO->getEndCidade());
$contatoModel->setEndUf($contatoCreateDTO->getEndUf());
$contatoModel->setInfoEmail($contatoCreateDTO->getInfoEmail());
$contatoModel->setInfoEmailNfe($contatoCreateDTO->getInfoEmailNfe());
$contatoModel->setInfoObservacao($contatoCreateDTO->getInfoObservacao());
$contatoModel->setInfoTelefone($contatoCreateDTO->getInfoTelefone());
$contatoModel->setInfoUsoConsumoIbsCbs($contatoCreateDTO->getInfoUsoConsumoIbsCbs());
$contatoModel->setCreatedAt(new DateTimeImmutable());
return $contatoModel;
} catch(AppException $e) {
throw $e;
}
}
private function validatePFFields(ContatoCreateDTO $contatoCreateDTO, EmpresaModel $empresaModel): void {
$errors = [];
if (($contatoCreateDTO->getDocumentCpf() === '') || !ValidateUtil::cpf(cpf: $contatoCreateDTO->getDocumentCpf())) {
$errors['document_cpf'] = 'CPF informado é inválido.';
throw new AppException(message: 'Dados de contato inválidos.', code: 422, details: $errors);
}
$existingContato = $this->contatoRepo->findOneByConditions(
empresa_id: $empresaModel->getId(),
conditions: [
['field' => 'document_cpf', 'operator' => '=', 'value' => $contatoCreateDTO->getDocumentCpf()]
]
);
if ($existingContato) {
$errors['document_cpf'] = 'CPF já cadastrado para esta empresa.';
throw new AppException(message: 'Dados de contato inválidos.', code: 422, details: $errors);
}
if ($errors) {
throw new AppException(message: 'Dados de contato inválidos.', code: 422, details: $errors);
}
}
private function createPJ(ContatoCreateDTO $contatoCreateDTO, EmpresaModel $empresaModel): ContatoModel {
try {
// Valida os campos específicos para pessoa física
// $this->validatePFFields(contatoCreateDTO: $contatoCreateDTO);
return new ContatoModel();
} catch(AppException $e) {
throw $e;
}
}
}
@@ -0,0 +1,31 @@
<?php
namespace WorkbloomERP\Module\v0\Contato\Factories;
use WorkbloomERP\Services\DBService;
use WorkbloomERP\Module\v0\Empresa\Repos\EmpresaRepo;
use WorkbloomERP\Module\v0\Contato\Repos\ContatoRepo;
use WorkbloomERP\Module\v0\Empresa\Models\EmpresaModel;
use WorkbloomERP\Module\v0\Contato\DTOs\ContatoCreateDTO;
use WorkbloomERP\Module\v0\Contato\Services\ContatoService;
class ContatoServiceFactory {
public static function makeContatoService(): ContatoService {
$db = new DBService();
return new ContatoService(
db: $db,
contatoRepo: new ContatoRepo(db: $db),
empresaRepo: new EmpresaRepo(db: $db),
);
}
public static function makeContratoFactory(EmpresaModel $empresaModel, ContatoCreateDTO $contatoCreateDTO): ContatoFactory {
$db = new DBService();
return new ContatoFactory(
db: $db,
empresaModel: $empresaModel,
contatoCreateDTO: $contatoCreateDTO,
contatoRepo: new ContatoRepo(db: $db),
empresaRepo: new EmpresaRepo(db: $db),
);
}
}
@@ -0,0 +1,312 @@
<?php
namespace WorkbloomERP\Module\v0\Contato\Models;
use DateTimeImmutable;
use WorkbloomERP\Exceptions\AppException;
class ContatoModel {
public function __construct(
private ?int $id = null,
private ?string $uuid = null,
private ?int $empresa_id = null,
private ?bool $is_active = null,
private ?string $tipo = null,
private ?string $nome_empresarial = null,
private ?string $nome_fantasia = null,
private ?string $personalidade = null,
private ?string $document_cpf = null,
private ?string $document_cnpj = null,
private ?int $regime_tributario = null,
private ?int $contribuinte_icms = null,
private ?string $orgao_publico = null,
private ?string $document_ie = null,
private ?string $document_im = null,
private ?string $document_is = null,
private ?string $end_pais = null,
private ?string $end_cep = null,
private ?string $end_ibge = null,
private ?string $end_logradouro = null,
private ?string $end_numero = null,
private ?string $end_complemento = null,
private ?string $end_bairro = null,
private ?string $end_cidade = null,
private ?string $end_uf = null,
private ?string $info_email = null,
private ?string $info_email_nfe = null,
private ?string $info_observacao = null,
private ?string $info_telefone = null,
private ?int $info_uso_consumo_ibs_cbs = null,
private ?DateTimeImmutable $created_at = null,
private ?DateTimeImmutable $updated_at = null,
private ?DateTimeImmutable $deleted_at = null,
) {}
public function toArray(): array {
return [
'id' => $this->getId(),
'uuid' => $this->getUuid(),
'empresa_id' => $this->getEmpresaId(),
'is_active' => $this->getIsActive(),
'tipo' => $this->getTipo(),
'nome_empresarial' => $this->getNomeEmpresarial(),
'nome_fantasia' => $this->getNomeFantasia(),
'personalidade' => $this->getPersonalidade(),
'document_cpf' => $this->getDocumentCpf(),
'document_cnpj' => $this->getDocumentCnpj(),
'regime_tributario' => $this->getRegimeTributario(),
'contribuinte_icms' => $this->getContribuinteIcms(),
'orgao_publico' => $this->getOrgaoPublico(),
'document_ie' => $this->getDocumentIe(),
'document_im' => $this->getDocumentIm(),
'document_is' => $this->getDocumentIs(),
'end_pais' => $this->getEndPais(),
'end_cep' => $this->getEndCep(),
'end_ibge' => $this->getEndIbge(),
'end_logradouro' => $this->getEndLogradouro(),
'end_numero' => $this->getEndNumero(),
'end_complemento' => $this->getEndComplemento(),
'end_bairro' => $this->getEndBairro(),
'end_cidade' => $this->getEndCidade(),
'end_uf' => $this->getEndUf(),
'info_email' => $this->getInfoEmail(),
'info_email_nfe' => $this->getInfoEmailNfe(),
'info_observacao' => $this->getInfoObservacao(),
'info_telefone' => $this->getInfoTelefone(),
'info_uso_consumo_ibs_cbs' => $this->getInfoUsoConsumoIbsCbs(),
'created_at' => $this->getCreatedAt() ? $this->getCreatedAt()->format(format: 'Y-m-d H:i:s') : null,
'updated_at' => $this->getUpdatedAt() ? $this->getUpdatedAt()->format(format: 'Y-m-d H:i:s') : null,
'deleted_at' => $this->getDeletedAt() ? $this->getDeletedAt()->format(format: 'Y-m-d H:i:s') : null,
];
}
public function setId(?int $id): void {
$this->id = $id;
}
public function getId(): ?int {
return $this->id;
}
public function setUuid(?string $uuid): void {
$this->uuid = $uuid;
}
public function getUuid(): ?string {
return $this->uuid;
}
public function setEmpresaId(?int $empresa_id): void {
$this->empresa_id = $empresa_id;
}
public function getEmpresaId(): ?int {
return $this->empresa_id;
}
public function setIsActive(?bool $is_active): void {
$this->is_active = $is_active;
}
public function getIsActive(): ?bool {
return $this->is_active;
}
public function setTipo(?string $tipo): void {
$this->tipo = $tipo;
}
public function getTipo(): ?string {
return $this->tipo;
}
public function setNomeEmpresarial(?string $nome_empresarial): void {
$this->nome_empresarial = $nome_empresarial;
}
public function getNomeEmpresarial(): ?string {
return $this->nome_empresarial;
}
public function setNomeFantasia(?string $nome_fantasia): void {
$this->nome_fantasia = $nome_fantasia;
}
public function getNomeFantasia(): ?string {
return $this->nome_fantasia;
}
public function setPersonalidade(?string $personalidade): void {
$this->personalidade = $personalidade;
}
public function getPersonalidade(): ?string {
return $this->personalidade;
}
public function setDocumentCpf(?string $document_cpf): void {
$this->document_cpf = $document_cpf;
}
public function getDocumentCpf(): ?string {
return $this->document_cpf;
}
public function setDocumentCnpj(?string $document_cnpj): void {
$this->document_cnpj = $document_cnpj;
}
public function getDocumentCnpj(): ?string {
return $this->document_cnpj;
}
public function setRegimeTributario(?int $regime_tributario): void {
$this->regime_tributario = $regime_tributario;
}
public function getRegimeTributario(): ?int {
return $this->regime_tributario;
}
public function setContribuinteIcms(?int $contribuinte_icms): void {
$this->contribuinte_icms = $contribuinte_icms;
}
public function getContribuinteIcms(): ?int {
return $this->contribuinte_icms;
}
public function setOrgaoPublico(?string $orgao_publico): void {
$this->orgao_publico = $orgao_publico;
}
public function getOrgaoPublico(): ?string {
return $this->orgao_publico;
}
public function setDocumentIe(?string $document_ie): void {
$this->document_ie = $document_ie;
}
public function getDocumentIe(): ?string {
return $this->document_ie;
}
public function setDocumentIm(?string $document_im): void {
$this->document_im = $document_im;
}
public function getDocumentIm(): ?string {
return $this->document_im;
}
public function setDocumentIs(?string $document_is): void {
$this->document_is = $document_is;
}
public function getDocumentIs(): ?string {
return $this->document_is;
}
public function setEndPais(?string $end_pais): void {
$this->end_pais = $end_pais;
}
public function getEndPais(): ?string {
return $this->end_pais;
}
public function setEndCep(?string $end_cep): void {
$this->end_cep = $end_cep;
}
public function getEndCep(): ?string {
return $this->end_cep;
}
public function setEndIbge(?string $end_ibge): void {
$this->end_ibge = $end_ibge;
}
public function getEndIbge(): ?string {
return $this->end_ibge;
}
public function setEndLogradouro(?string $end_logradouro): void {
$this->end_logradouro = $end_logradouro;
}
public function getEndLogradouro(): ?string {
return $this->end_logradouro;
}
public function setEndNumero(?string $end_numero): void {
$this->end_numero = $end_numero;
}
public function getEndNumero(): ?string {
return $this->end_numero;
}
public function setEndComplemento(?string $end_complemento): void {
$this->end_complemento = $end_complemento;
}
public function getEndComplemento(): ?string {
return $this->end_complemento;
}
public function setEndBairro(?string $end_bairro): void {
$this->end_bairro = $end_bairro;
}
public function getEndBairro(): ?string {
return $this->end_bairro;
}
public function setEndCidade(?string $end_cidade): void {
$this->end_cidade = $end_cidade;
}
public function getEndCidade(): ?string {
return $this->end_cidade;
}
public function setEndUf(?string $end_uf): void {
$this->end_uf = $end_uf;
}
public function getEndUf(): ?string {
return $this->end_uf;
}
public function setInfoEmail(?string $info_email): void {
$this->info_email = $info_email;
}
public function getInfoEmail(): ?string {
return $this->info_email;
}
public function setInfoEmailNfe(?string $info_email_nfe): void {
$this->info_email_nfe = $info_email_nfe;
}
public function getInfoEmailNfe(): ?string {
return $this->info_email_nfe;
}
public function setInfoObservacao(?string $info_observacao): void {
$this->info_observacao = $info_observacao;
}
public function getInfoObservacao(): ?string {
return $this->info_observacao;
}
public function setInfoTelefone(?string $info_telefone): void {
$this->info_telefone = $info_telefone;
}
public function getInfoTelefone(): ?string {
return $this->info_telefone;
}
public function setInfoUsoConsumoIbsCbs(?int $info_uso_consumo_ibs_cbs): void {
$this->info_uso_consumo_ibs_cbs = $info_uso_consumo_ibs_cbs;
}
public function getInfoUsoConsumoIbsCbs(): ?int {
return $this->info_uso_consumo_ibs_cbs;
}
public function setCreatedAt(?DateTimeImmutable $created_at): void {
$this->created_at = $created_at;
}
public function getCreatedAt(): ?DateTimeImmutable {
return $this->created_at;
}
public function setUpdatedAt(?DateTimeImmutable $updated_at): void {
$this->updated_at = $updated_at;
}
public function getUpdatedAt(): ?DateTimeImmutable {
return $this->updated_at;
}
public function setDeletedAt(?DateTimeImmutable $deleted_at): void {
$this->deleted_at = $deleted_at;
}
public function getDeletedAt(): ?DateTimeImmutable {
return $this->deleted_at;
}
}
+377
View File
@@ -0,0 +1,377 @@
<?php
namespace WorkbloomERP\Module\v0\Contato\Repos;
use DateTimeImmutable;
use WorkbloomERP\Services\DBService;
use WorkbloomERP\Module\v0\Contato\Models\ContatoModel;
class ContatoRepo {
protected string $contatoTable = 'contato';
public function __construct(
private DBService $db
) {}
public function insert(ContatoModel $contatoModel): ContatoModel {
$query =
"INSERT INTO {$this->contatoTable} (
uuid,
empresa_id,
is_active,
tipo,
nome_empresarial,
nome_fantasia,
personalidade,
document_cpf,
document_cnpj,
regime_tributario,
contribuinte_icms,
orgao_publico,
document_ie,
document_im,
document_is,
end_pais,
end_cep,
end_ibge,
end_logradouro,
end_numero,
end_complemento,
end_bairro,
end_cidade,
end_uf,
info_email,
info_email_nfe,
info_observacao,
info_telefone,
info_uso_consumo_ibs_cbs,
created_at
) VALUES (
:uuid,
:empresa_id,
:is_active,
:tipo,
:nome_empresarial,
:nome_fantasia,
:personalidade,
:document_cpf,
:document_cnpj,
:regime_tributario,
:contribuinte_icms,
:orgao_publico,
:document_ie,
:document_im,
:document_is,
:end_pais,
:end_cep,
:end_ibge,
:end_logradouro,
:end_numero,
:end_complemento,
:end_bairro,
:end_cidade,
:end_uf,
:info_email,
:info_email_nfe,
:info_observacao,
:info_telefone,
:info_uso_consumo_ibs_cbs,
:created_at
)";
$contatoModel->setCreatedAt(new DateTimeImmutable());
$this->db->execute(
sql: $query,
params: [
':uuid' => $contatoModel->getUuid(),
':empresa_id' => $contatoModel->getEmpresaId(),
':is_active' => $contatoModel->getIsActive(),
':tipo' => $contatoModel->getTipo(),
':nome_empresarial' => $contatoModel->getNomeEmpresarial(),
':nome_fantasia' => $contatoModel->getNomeFantasia(),
':personalidade' => $contatoModel->getPersonalidade(),
':document_cpf' => $contatoModel->getDocumentCpf(),
':document_cnpj' => $contatoModel->getDocumentCnpj(),
':regime_tributario' => $contatoModel->getRegimeTributario(),
':contribuinte_icms' => $contatoModel->getContribuinteIcms(),
':orgao_publico' => $contatoModel->getOrgaoPublico(),
':document_ie' => $contatoModel->getDocumentIe(),
':document_im' => $contatoModel->getDocumentIm(),
':document_is' => $contatoModel->getDocumentIs(),
':end_pais' => $contatoModel->getEndPais(),
':end_cep' => $contatoModel->getEndCep(),
':end_ibge' => $contatoModel->getEndIbge(),
':end_logradouro' => $contatoModel->getEndLogradouro(),
':end_numero' => $contatoModel->getEndNumero(),
':end_complemento' => $contatoModel->getEndComplemento(),
':end_bairro' => $contatoModel->getEndBairro(),
':end_cidade' => $contatoModel->getEndCidade(),
':end_uf' => $contatoModel->getEndUf(),
':info_email' => $contatoModel->getInfoEmail(),
':info_email_nfe' => $contatoModel->getInfoEmailNfe(),
':info_observacao' => $contatoModel->getInfoObservacao(),
':info_telefone' => $contatoModel->getInfoTelefone(),
':info_uso_consumo_ibs_cbs' => $contatoModel->getInfoUsoConsumoIbsCbs(),
':created_at' => $contatoModel->getCreatedAt()->format('Y-m-d H:i:s'),
]
);
$contatoModel->setId(id: $this->db->lastInsertId());
return $contatoModel;
}
public function update(ContatoModel $contatoModel): bool {
$query =
"UPDATE {$this->contatoTable} SET
is_active = :is_active,
tipo = :tipo,
nome_empresarial = :nome_empresarial,
nome_fantasia = :nome_fantasia,
personalidade = :personalidade,
document_cpf = :document_cpf,
document_cnpj = :document_cnpj,
regime_tributario = :regime_tributario,
contribuinte_icms = :contribuinte_icms,
orgao_publico = :orgao_publico,
document_ie = :document_ie,
document_im = :document_im,
document_is = :document_is,
end_pais = :end_pais,
end_cep = :end_cep,
end_ibge = :end_ibge,
end_logradouro = :end_logradouro,
end_numero = :end_numero,
end_complemento = :end_complemento,
end_bairro = :end_bairro,
end_cidade = :end_cidade,
end_uf = :end_uf,
info_email = :info_email,
info_email_nfe = :info_email_nfe,
info_observacao = :info_observacao,
info_telefone = :info_telefone,
info_uso_consumo_ibs_cbs = :info_uso_consumo_ibs_cbs,
updated_at = :updated_at
WHERE id = :id OR uuid = :uuid";
return $this->db->execute(
sql: $query,
params: [
':id' => $contatoModel->getId(),
':uuid' => $contatoModel->getUuid(),
':empresa_id' => $contatoModel->getEmpresaId(),
':is_active' => $contatoModel->getIsActive(),
':tipo' => $contatoModel->getTipo(),
':nome_empresarial' => $contatoModel->getNomeEmpresarial(),
':nome_fantasia' => $contatoModel->getNomeFantasia(),
':personalidade' => $contatoModel->getPersonalidade(),
':document_cpf' => $contatoModel->getDocumentCpf(),
':document_cnpj' => $contatoModel->getDocumentCnpj(),
':regime_tributario' => $contatoModel->getRegimeTributario(),
':contribuinte_icms' => $contatoModel->getContribuinteIcms(),
':orgao_publico' => $contatoModel->getOrgaoPublico(),
':document_ie' => $contatoModel->getDocumentIe(),
':document_im' => $contatoModel->getDocumentIm(),
':document_is' => $contatoModel->getDocumentIs(),
':end_pais' => $contatoModel->getEndPais(),
':end_cep' => $contatoModel->getEndCep(),
':end_ibge' => $contatoModel->getEndIbge(),
':end_logradouro' => $contatoModel->getEndLogradouro(),
':end_numero' => $contatoModel->getEndNumero(),
':end_complemento' => $contatoModel->getEndComplemento(),
':end_bairro' => $contatoModel->getEndBairro(),
':end_cidade' => $contatoModel->getEndCidade(),
':end_uf' => $contatoModel->getEndUf(),
':info_email' => $contatoModel->getInfoEmail(),
':info_email_nfe' => $contatoModel->getInfoEmailNfe(),
':info_observacao' => $contatoModel->getInfoObservacao(),
':info_telefone' => $contatoModel->getInfoTelefone(),
':info_uso_consumo_ibs_cbs' => $contatoModel->getInfoUsoConsumoIbsCbs(),
':updated_at' => $contatoModel->getUpdatedAt()->format('Y-m-d H:i:s'),
]
);
}
public function delete(ContatoModel $contatoModel): bool {
$query =
"UPDATE {$this->contatoTable} SET
deleted_at = :deleted_at
WHERE id = :id OR uuid = :uuid";
return $this->db->execute(
sql: $query,
params: [
':id' => $contatoModel->getId(),
':uuid' => $contatoModel->getUuid(),
':deleted_at' => (new DateTimeImmutable())->format('Y-m-d H:i:s'),
]
);
}
public function findByIdentifier(int $empresa_id, string $identifier, mixed $value) {
$query =
"SELECT
id,
uuid,
empresa_id,
is_active,
nome_empresarial,
nome_fantasia,
personalidade,
document_cpf,
document_cnpj,
regime_tributario,
contribuinte_icms,
orgao_publico,
document_ie,
document_im,
document_is,
end_cep,
end_ibge,
end_logradouro,
end_numero,
end_complemento,
end_bairro,
end_cidade,
end_uf,
info_email,
info_email_nfe,
info_observacao,
info_telefone,
info_uso_consumo_ibs_cbs,
created_at,
updated_at,
deleted_at
FROM {$this->contatoTable}
WHERE empresa_id = :empresa_id
AND {$identifier} = :value
AND deleted_at IS NULL
LIMIT 1";
$result = $this->db->fetchOne(
sql: $query,
params: [
':empresa_id' => $empresa_id,
':value' => $value,
]
);
return $result ? $this->mapToModel($result) : null;
}
public function findOneByConditions(int $empresa_id, array $conditions): ?ContatoModel {
if (empty($conditions)) {
throw new \InvalidArgumentException('O array de condições não pode estar vazio.');
}
// Começa com as condições fixas
$whereClauses = [
'empresa_id = :empresa_id',
'deleted_at IS NULL'
];
$params = [
':empresa_id' => $empresa_id
];
// Adiciona as condições dinâmicas
foreach ($conditions as $condition) {
if (!isset($condition['field'], $condition['value'])) {
throw new \InvalidArgumentException(
"Cada condição deve conter 'field' e 'value'."
);
}
$field = $condition['field'];
$operator = $condition['operator'] ?? '=';
$value = $condition['value'];
$whereClauses[] = "{$field} {$operator} :{$field}";
$params[":{$field}"] = $value;
}
$whereSql = implode(' AND ', $whereClauses);
$query =
"SELECT
id,
uuid,
empresa_id,
is_active,
tipo,
nome_empresarial,
nome_fantasia,
personalidade,
document_cpf,
document_cnpj,
regime_tributario,
contribuinte_icms,
orgao_publico,
document_ie,
document_im,
document_is,
end_pais,
end_cep,
end_ibge,
end_logradouro,
end_numero,
end_complemento,
end_bairro,
end_cidade,
end_uf,
info_email,
info_email_nfe,
info_observacao,
info_telefone,
info_uso_consumo_ibs_cbs,
created_at,
updated_at,
deleted_at
FROM {$this->contatoTable}
WHERE {$whereSql}
LIMIT 1";
$result = $this->db->fetchOne(
sql: $query,
params: $params
);
return $result ? $this->mapToModel($result) : null;
}
private function mapToModel(array $data): ContatoModel {
return new ContatoModel(
id: $data['id'],
uuid: $data['uuid'],
empresa_id: $data['empresa_id'],
is_active: $data['is_active'],
tipo: $data['tipo'],
nome_empresarial: $data['nome_empresarial'],
nome_fantasia: $data['nome_fantasia'],
personalidade: $data['personalidade'],
document_cpf: $data['document_cpf'],
document_cnpj: $data['document_cnpj'],
regime_tributario: $data['regime_tributario'],
contribuinte_icms: $data['contribuinte_icms'],
orgao_publico: $data['orgao_publico'],
document_ie: $data['document_ie'],
document_im: $data['document_im'],
document_is: $data['document_is'],
end_pais: $data['end_pais'],
end_cep: $data['end_cep'],
end_ibge: $data['end_ibge'],
end_logradouro: $data['end_logradouro'],
end_numero: $data['end_numero'],
end_complemento: $data['end_complemento'],
end_bairro: $data['end_bairro'],
end_cidade: $data['end_cidade'],
end_uf: $data['end_uf'],
info_email: $data['info_email'],
info_email_nfe: $data['info_email_nfe'],
info_observacao: $data['info_observacao'],
info_telefone: $data['info_telefone'],
info_uso_consumo_ibs_cbs: $data['info_uso_consumo_ibs_cbs'],
created_at: $data['created_at'] ? new DateTimeImmutable($data['created_at']) : null,
updated_at: $data['updated_at'] ? new DateTimeImmutable($data['updated_at']) : null,
deleted_at: $data['deleted_at'] ? new DateTimeImmutable($data['deleted_at']) : null,
);
}
}
+18
View File
@@ -0,0 +1,18 @@
<?php
use KrothiumAPI\Http\Router;
use WorkbloomERP\Module\v0\Auth\Middlewares\AuthMiddleware;
use WorkbloomERP\Module\v0\Contato\Controllers\ContatoController;
Router::group(
prefix: '/contacts',
callback: function() {
// Endpoint para obter as opções de criação de contato (UF, regime tributário, etc.)
Router::get('/create-options', [ContatoController::class, 'createOptions']);
// Rota para cadastrar um novo contato
Router::post('/create', [ContatoController::class, 'create']);
},
middlewares: [
[AuthMiddleware::class, 'handle']
]
);
@@ -0,0 +1,69 @@
<?php
namespace WorkbloomERP\Module\v0\Contato\Services;
use Ramsey\Uuid\Uuid;
use WorkbloomERP\Utils\SanitizeUtil;
use WorkbloomERP\Utils\ValidateUtil;
use WorkbloomERP\Services\DBService;
use WorkbloomERP\Exceptions\AppException;
use WorkbloomERP\Module\v0\Auth\Utils\AuthUtil;
use WorkbloomERP\Module\v0\Empresa\Repos\EmpresaRepo;
use WorkbloomERP\Module\v0\Contato\Repos\ContatoRepo;
use WorkbloomERP\Module\v0\Empresa\Models\EmpresaModel;
use WorkbloomERP\Module\v0\Contato\Models\ContatoModel;
use WorkbloomERP\Module\v0\Contato\DTOs\ContatoCreateDTO;
use WorkbloomERP\Module\v0\Contato\Factories\ContatoServiceFactory;
use WorkbloomERP\Constants\SpedCRTConst;
use WorkbloomERP\Constants\BrasilUfsConst;
use WorkbloomERP\Constants\SpedPaisesConst;
use WorkbloomERP\Module\v0\Contato\Constants\OrgaoPublicoConst;
use WorkbloomERP\Module\v0\Contato\Constants\PersonalidadeConst;
use WorkbloomERP\Module\v0\Contato\Constants\ContribuinteICMSConst;
use WorkbloomERP\Module\v0\Contato\Constants\NFSeConsumoIbsCbsConst;
class ContatoService {
public function __construct (
protected DBService $db,
protected ContatoRepo $contatoRepo,
protected EmpresaRepo $empresaRepo,
) {}
public function createOptions(): array {
return [
'tipo' => PersonalidadeConst::getAll(),
'personalidade' => PersonalidadeConst::getAll(),
'uf' => BrasilUfsConst::getAll(),
'regime_tributario' => SpedCRTConst::getAll(),
'contribuinte_icms' => ContribuinteICMSConst::getAll(),
'info_uso_consumo_ibs_cbs' => NFSeConsumoIbsCbsConst::getAll(),
'orgao_publico' => OrgaoPublicoConst::getAll(),
'paises_sped' => SpedPaisesConst::getAll()
];
}
public function create(ContatoCreateDTO $contatoCreateDTO): array {
try {
return $this->db->transaction(
callback: function() use ($contatoCreateDTO) {
// Pega as informações da sessão para associar o contato à empresa correta
$empresaData = AuthUtil::readSession(key: 'empresa_data');
// Verifica se a empresa existe
$empresaModel = $this->empresaRepo->findByIdentifier(identifier: 'uuid', value: $empresaData['uuid']);
// Cria a fábrica de contato e valida os dados de criação do contato
$contatoFactory = ContatoServiceFactory::makeContratoFactory(empresaModel: $empresaModel, contatoCreateDTO: $contatoCreateDTO);
$contatoModel = $contatoFactory->create(contatoCreateDTO: $contatoCreateDTO, empresaModel: $empresaModel);
return [
'response_code' => 201,
'message' => 'Contato criado com sucesso.',
'output' => ['data' => $contatoModel->toArray()]
];
}
);
} catch(AppException $e) {
throw $e;
}
}
}
@@ -0,0 +1,212 @@
<?php
namespace WorkbloomERP\Module\v0\Empresa\Models;
use DateTimeImmutable;
class EmpresaModel {
public function __construct(
private ?int $id = null,
private ?string $uuid = null,
private ?bool $is_active = null,
private ?string $nome_empresarial = null,
private ?string $nome_fantasia = null,
private ?string $tipo = null,
private ?int $matriz_id = null,
private ?string $document_cnpj = null,
private ?string $document_ie = null,
private ?string $document_im = null,
private ?string $regime_tributario = null,
private ?string $end_cep = null,
private ?int $end_ibge = null,
private ?string $end_logradouro = null,
private ?string $end_numero = null,
private ?string $end_complemento = null,
private ?string $end_bairro = null,
private ?string $end_cidade = null,
private ?string $end_uf = null,
private ?DateTimeImmutable $created_at = null,
private ?DateTimeImmutable $updated_at = null,
private ?DateTimeImmutable $deleted_at = null,
) {}
public function toArray(): array {
return [
'id' => $this->getId(),
'uuid' => $this->getUuid(),
'is_active' => $this->getIsActive(),
'nome_empresarial' => $this->getNomeEmpresarial(),
'nome_fantasia' => $this->getNomeFantasia(),
'tipo' => $this->getTipo(),
'matriz_id' => $this->getMatrizId(),
'document_cnpj' => $this->getDocumentCnpj(),
'document_ie' => $this->getDocumentIe(),
'document_im' => $this->getDocumentIm(),
'regime_tributario' => $this->getRegimeTributario(),
'end_cep' => $this->getEndCep(),
'end_ibge' => $this->getEndIbge(),
'end_logradouro' => $this->getEndLogradouro(),
'end_numero' => $this->getEndNumero(),
'end_complemento' => $this->getEndComplemento(),
'end_bairro' => $this->getEndBairro(),
'end_cidade' => $this->getEndCidade(),
'end_uf' => $this->getEndUf(),
'created_at' => $this->getCreatedAt() ? $this->getCreatedAt()->format('Y-m-d H:i:s') : null,
'updated_at' => $this->getUpdatedAt() ? $this->getUpdatedAt()->format('Y-m-d H:i:s') : null,
'deleted_at' => $this->getDeletedAt() ? $this->getDeletedAt()->format('Y-m-d H:i:s') : null,
];
}
public function setId(?int $id): void {
$this->id = $id;
}
public function getId(): ?int {
return $this->id;
}
public function setUuid(?string $uuid): void {
$this->uuid = $uuid;
}
public function getUuid(): ?string {
return $this->uuid;
}
public function setIsActive(?bool $is_active): void {
$this->is_active = $is_active;
}
public function getIsActive(): ?bool {
return $this->is_active;
}
public function setNomeEmpresarial(?string $nome_empresarial): void {
$this->nome_empresarial = $nome_empresarial;
}
public function getNomeEmpresarial(): ?string {
return $this->nome_empresarial;
}
public function setNomeFantasia(?string $nome_fantasia): void {
$this->nome_fantasia = $nome_fantasia;
}
public function getNomeFantasia(): ?string {
return $this->nome_fantasia;
}
public function setTipo(?string $tipo): void {
$this->tipo = $tipo;
}
public function getTipo(): ?string {
return $this->tipo;
}
public function setMatrizId(?int $matriz_id): void {
$this->matriz_id = $matriz_id;
}
public function getMatrizId(): ?int {
return $this->matriz_id;
}
public function setDocumentCnpj(?string $document_cnpj): void {
$this->document_cnpj = $document_cnpj;
}
public function getDocumentCnpj(): ?string {
return $this->document_cnpj;
}
public function setDocumentIe(?string $document_ie): void {
$this->document_ie = $document_ie;
}
public function getDocumentIe(): ?string {
return $this->document_ie;
}
public function setDocumentIm(?string $document_im): void {
$this->document_im = $document_im;
}
public function getDocumentIm(): ?string {
return $this->document_im;
}
public function setRegimeTributario(?string $regime_tributario): void {
$this->regime_tributario = $regime_tributario;
}
public function getRegimeTributario(): ?string {
return $this->regime_tributario;
}
public function setEndCep(?string $end_cep): void {
$this->end_cep = $end_cep;
}
public function getEndCep(): ?string {
return $this->end_cep;
}
public function setEndIbge(?int $end_ibge): void {
$this->end_ibge = $end_ibge;
}
public function getEndIbge(): ?int {
return $this->end_ibge;
}
public function setEndLogradouro(?string $end_logradouro): void {
$this->end_logradouro = $end_logradouro;
}
public function getEndLogradouro(): ?string {
return $this->end_logradouro;
}
public function setEndNumero(?string $end_numero): void {
$this->end_numero = $end_numero;
}
public function getEndNumero(): ?string {
return $this->end_numero;
}
public function setEndComplemento(?string $end_complemento): void {
$this->end_complemento = $end_complemento;
}
public function getEndComplemento(): ?string {
return $this->end_complemento;
}
public function setEndBairro(?string $end_bairro): void {
$this->end_bairro = $end_bairro;
}
public function getEndBairro(): ?string {
return $this->end_bairro;
}
public function setEndCidade(?string $end_cidade): void {
$this->end_cidade = $end_cidade;
}
public function getEndCidade(): ?string {
return $this->end_cidade;
}
public function setEndUf(?string $end_uf): void {
$this->end_uf = $end_uf;
}
public function getEndUf(): ?string {
return $this->end_uf;
}
public function setCreatedAt(?DateTimeImmutable $created_at): void {
$this->created_at = $created_at;
}
public function getCreatedAt(): ?DateTimeImmutable {
return $this->created_at;
}
public function setUpdatedAt(?DateTimeImmutable $updated_at): void {
$this->updated_at = $updated_at;
}
public function getUpdatedAt(): ?DateTimeImmutable {
return $this->updated_at;
}
public function setDeletedAt(?DateTimeImmutable $deleted_at): void {
$this->deleted_at = $deleted_at;
}
public function getDeletedAt(): ?DateTimeImmutable {
return $this->deleted_at;
}
}
+268
View File
@@ -0,0 +1,268 @@
<?php
namespace WorkbloomERP\Module\v0\Empresa\Repos;
use DateTimeImmutable;
use WorkbloomERP\Services\DBService;
use WorkbloomERP\Module\v0\Empresa\Models\EmpresaModel;
class EmpresaRepo {
protected string $empresaTable = 'empresa';
public function __construct(
protected DBService $db
) {}
public function insert(EmpresaModel $empresaModel): ?EmpresaModel {
$query =
"INSERT INTO {$this->empresaTable} (
uuid,
is_active,
nome_empresarial,
nome_fantasia,
tipo,
matriz_id,
document_cnpj,
document_ie,
document_im,
regime_tributario,
end_cep,
end_ibge,
end_logradouro,
end_numero,
end_complemento,
end_bairro,
end_cidade,
end_uf,
created_at
) VALUES (
:uuid,
:is_active,
:nome_empresarial,
:nome_fantasia,
:tipo,
:matriz_id,
:document_cnpj,
:document_ie,
:document_im,
:regime_tributario,
:end_cep,
:end_ibge,
:end_logradouro,
:end_numero,
:end_complemento,
:end_bairro,
:end_cidade,
:end_uf,
:created_at
)";
$empresaModel->setCreatedAt(new DateTimeImmutable());
$this->db->execute(
sql: $query,
params: [
':uuid' => $empresaModel->getUuid(),
':is_active' => $empresaModel->getIsActive(),
':nome_empresarial' => $empresaModel->getNomeEmpresarial(),
':nome_fantasia' => $empresaModel->getNomeFantasia(),
':tipo' => $empresaModel->getTipo(),
':matriz_id' => $empresaModel->getMatrizId(),
':document_cnpj' => $empresaModel->getDocumentCnpj(),
':document_ie' => $empresaModel->getDocumentIe(),
':document_im' => $empresaModel->getDocumentIm(),
':regime_tributario' => $empresaModel->getRegimeTributario(),
':end_cep' => $empresaModel->getEndCep(),
':end_ibge' => $empresaModel->getEndIbge(),
':end_logradouro' => $empresaModel->getEndLogradouro(),
':end_numero' => $empresaModel->getEndNumero(),
':end_complemento' => $empresaModel->getEndComplemento(),
':end_bairro' => $empresaModel->getEndBairro(),
':end_cidade' => $empresaModel->getEndCidade(),
':end_uf' => $empresaModel->getEndUf(),
':created_at' => $empresaModel->getCreatedAt()->format('Y-m-d H:i:s'),
]
);
$empresaModel->setId($this->db->lastInsertId());
return $empresaModel;
}
public function update(EmpresaModel $empresaModel): bool {
$query =
"UPDATE {$this->empresaTable} SET
is_active = :is_active,
nome_empresarial = :nome_empresarial,
nome_fantasia = :nome_fantasia,
tipo = :tipo,
matriz_id = :matriz_id,
document_cnpj = :document_cnpj,
document_ie = :document_ie,
document_im = :document_im,
regime_tributario = :regime_tributario,
end_cep = :end_cep,
end_ibge = :end_ibge,
end_logradouro = :end_logradouro,
end_numero = :end_numero,
end_complemento = :end_complemento,
end_bairro = :end_bairro,
end_cidade = :end_cidade,
end_uf = :end_uf,
updated_at = :updated_at
WHERE id = :id OR uuid = :uuid";
$empresaModel->setUpdatedAt(new DateTimeImmutable());
return $this->db->execute(
sql: $query,
params: [
':id' => $empresaModel->getId(),
':uuid' => $empresaModel->getUuid(),
':is_active' => $empresaModel->getIsActive(),
':nome_empresarial' => $empresaModel->getNomeEmpresarial(),
':nome_fantasia' => $empresaModel->getNomeFantasia(),
':tipo' => $empresaModel->getTipo(),
':matriz_id' => $empresaModel->getMatrizId(),
':document_cnpj' => $empresaModel->getDocumentCnpj(),
':document_ie' => $empresaModel->getDocumentIe(),
':document_im' => $empresaModel->getDocumentIm(),
':regime_tributario' => $empresaModel->getRegimeTributario(),
':end_cep' => $empresaModel->getEndCep(),
':end_ibge' => $empresaModel->getEndIbge(),
':end_logradouro' => $empresaModel->getEndLogradouro(),
':end_numero' => $empresaModel->getEndNumero(),
':end_complemento' => $empresaModel->getEndComplemento(),
':end_bairro' => $empresaModel->getEndBairro(),
':end_cidade' => $empresaModel->getEndCidade(),
':end_uf' => $empresaModel->getEndUf(),
':updated_at' => $empresaModel->getUpdatedAt()->format('Y-m-d H:i:s'),
]
);
}
public function delete(EmpresaModel $empresaModel): bool {
$query =
"UPDATE {$this->empresaTable} SET
deleted_at = :deleted_at
WHERE id = :id OR uuid = :uuid";
$empresaModel->setDeletedAt(new DateTimeImmutable());
return $this->db->execute(
sql: $query,
params: [
':id' => $empresaModel->getId(),
':uuid' => $empresaModel->getUuid(),
':deleted_at' => $empresaModel->getDeletedAt()->format('Y-m-d H:i:s'),
]
);
}
public function findByIdentifier(string $identifier, mixed $value): ?EmpresaModel {
$query =
"SELECT
id,
uuid,
is_active,
nome_empresarial,
nome_fantasia,
tipo,
matriz_id,
document_cnpj,
document_ie,
document_im,
regime_tributario,
end_cep,
end_ibge,
end_logradouro,
end_numero,
end_complemento,
end_bairro,
end_cidade,
end_uf,
created_at,
updated_at,
deleted_at
FROM {$this->empresaTable}
WHERE $identifier = :value
AND deleted_at IS NULL
LIMIT 1";
$result = $this->db->fetchOne(
sql: $query,
params: [
':value' => $value
]
);
return $result ? $this->mapToModel($result) : null;
}
public function findAllByMatrizId(int $matriz_id): array {
$query =
"SELECT
id,
uuid,
is_active,
nome_empresarial,
nome_fantasia,
tipo,
matriz_id,
document_cnpj,
document_ie,
document_im,
regime_tributario,
end_cep,
end_ibge,
end_logradouro,
end_numero,
end_complemento,
end_bairro,
end_cidade,
end_uf,
created_at,
updated_at,
deleted_at
FROM {$this->empresaTable}
WHERE (
id = :matriz_id OR
matriz_id = :matriz_id
)
AND deleted_at IS NULL";
$results = $this->db->fetchAll(
sql: $query,
params: [
':matriz_id' => $matriz_id
]
);
return array_map(fn($data) => $this->mapToModel($data), $results);
}
private function mapToModel(array $data): EmpresaModel {
return new EmpresaModel(
id: $data['id'],
uuid: $data['uuid'],
is_active: $data['is_active'],
nome_empresarial: $data['nome_empresarial'],
nome_fantasia: $data['nome_fantasia'],
tipo: $data['tipo'],
matriz_id: $data['matriz_id'],
document_cnpj: $data['document_cnpj'],
document_ie: $data['document_ie'],
document_im: $data['document_im'],
regime_tributario: $data['regime_tributario'],
end_cep: $data['end_cep'],
end_ibge: $data['end_ibge'],
end_logradouro: $data['end_logradouro'],
end_numero: $data['end_numero'],
end_complemento: $data['end_complemento'],
end_bairro: $data['end_bairro'],
end_cidade: $data['end_cidade'],
end_uf: $data['end_uf'],
created_at: $data['created_at'] ? new DateTimeImmutable($data['created_at']) : null,
updated_at: $data['updated_at'] ? new DateTimeImmutable($data['updated_at']) : null,
deleted_at: $data['deleted_at'] ? new DateTimeImmutable($data['deleted_at']) : null,
);
}
}
@@ -0,0 +1,32 @@
<?php
namespace WorkbloomERP\Module\v0\Usuario\Models;
use DateTimeImmutable;
class UsuarioEmpresaModel {
public function __construct(
private ?int $usuario_id = null,
private ?int $empresa_id = null,
) {}
public function toArray(): array {
return [
'usuario_id' => $this->getUsuarioId(),
'empresa_id' => $this->getEmpresaId(),
];
}
public function getUsuarioId(): ?int {
return $this->usuario_id;
}
public function setUsuarioId(?int $usuario_id): void {
$this->usuario_id = $usuario_id;
}
public function getEmpresaId(): ?int {
return $this->empresa_id;
}
public function setEmpresaId(?int $empresa_id): void {
$this->empresa_id = $empresa_id;
}
}
@@ -0,0 +1,113 @@
<?php
namespace WorkbloomERP\Module\v0\Usuario\Models;
use DateTimeImmutable;
class UsuarioModel {
public function __construct(
private ?int $id = null,
private ?string $uuid = null,
private ?bool $is_active = null,
private ?bool $is_root = null,
private ?string $nome_completo = null,
private ?string $nome_usuario = null,
private ?string $email = null,
private ?string $senha_hash = null,
private ?DateTimeImmutable $created_at = null,
private ?DateTimeImmutable $updated_at = null,
private ?DateTimeImmutable $deleted_at = null,
) {}
public function toArray(): array {
return [
'id' => $this->getId(),
'uuid' => $this->getUuid(),
'is_active' => $this->getIsActive(),
'is_root' => $this->getIsRoot(),
'nome_completo' => $this->getNomeCompleto(),
'nome_usuario' => $this->getNomeUsuario(),
'email' => $this->getEmail(),
'senha_hash' => $this->getSenhaHash(),
'created_at' => $this->getCreatedAt() ? $this->getCreatedAt()->format('Y-m-d H:i:s') : null,
'updated_at' => $this->getUpdatedAt() ? $this->getUpdatedAt()->format('Y-m-d H:i:s') : null,
'deleted_at' => $this->getDeletedAt() ? $this->getDeletedAt()->format('Y-m-d H:i:s') : null,
];
}
public function setId(?int $id): void {
$this->id = $id;
}
public function getId(): ?int {
return $this->id;
}
public function setUuid(?string $uuid): void {
$this->uuid = $uuid;
}
public function getUuid(): ?string {
return $this->uuid;
}
public function setIsActive(?bool $is_active): void {
$this->is_active = $is_active;
}
public function getIsActive(): ?bool {
return $this->is_active;
}
public function setIsRoot(?bool $is_root): void {
$this->is_root = $is_root;
}
public function getIsRoot(): ?bool {
return $this->is_root;
}
public function setNomeCompleto(?string $nome_completo): void {
$this->nome_completo = $nome_completo;
}
public function getNomeCompleto(): ?string {
return $this->nome_completo;
}
public function setNomeUsuario(?string $nome_usuario): void {
$this->nome_usuario = $nome_usuario;
}
public function getNomeUsuario(): ?string {
return $this->nome_usuario;
}
public function setEmail(?string $email): void {
$this->email = $email;
}
public function getEmail(): ?string {
return $this->email;
}
public function setSenhaHash(?string $senha_hash): void {
$this->senha_hash = $senha_hash;
}
public function getSenhaHash(): ?string {
return $this->senha_hash;
}
public function setCreatedAt(?DateTimeImmutable $created_at): void {
$this->created_at = $created_at;
}
public function getCreatedAt(): ?DateTimeImmutable {
return $this->created_at;
}
public function setUpdatedAt(?DateTimeImmutable $updated_at): void {
$this->updated_at = $updated_at;
}
public function getUpdatedAt(): ?DateTimeImmutable {
return $this->updated_at;
}
public function setDeletedAt(?DateTimeImmutable $deleted_at): void {
$this->deleted_at = $deleted_at;
}
public function getDeletedAt(): ?DateTimeImmutable {
return $this->deleted_at;
}
}
@@ -0,0 +1,86 @@
<?php
namespace WorkbloomERP\Module\v0\Usuario\Models;
use DateTimeImmutable;
class UsuarioSessionModel {
public function __construct(
private ?int $id = null,
private ?string $uuid = null,
private ?int $usuario_id = null,
private ?string $user_agent = null,
private ?string $ip_address = null,
private ?string $token_hash = null,
private ?DateTimeImmutable $created_at = null,
private ?DateTimeImmutable $revoked_at = null,
) {}
public function toArray(): array {
return [
'id' => $this->getId(),
'uuid' => $this->getUuid(),
'usuario_id' => $this->getUsuarioId(),
'user_agent' => $this->getUserAgent(),
'ip_address' => $this->getIpAddress(),
'token_hash' => $this->getTokenHash(),
'created_at' => $this->getCreatedAt() ? $this->getCreatedAt()->format('Y-m-d H:i:s') : null,
'revoked_at' => $this->getRevokedAt() ? $this->getRevokedAt()->format('Y-m-d H:i:s') : null,
];
}
public function setId(?int $id): void {
$this->id = $id;
}
public function getId(): ?int {
return $this->id;
}
public function setUuid(?string $uuid): void {
$this->uuid = $uuid;
}
public function getUuid(): ?string {
return $this->uuid;
}
public function setUsuarioId(?int $usuario_id): void {
$this->usuario_id = $usuario_id;
}
public function getUsuarioId(): ?int {
return $this->usuario_id;
}
public function setUserAgent(?string $user_agent): void {
$this->user_agent = $user_agent;
}
public function getUserAgent(): ?string {
return $this->user_agent;
}
public function setIpAddress(?string $ip_address): void {
$this->ip_address = $ip_address;
}
public function getIpAddress(): ?string {
return $this->ip_address;
}
public function setTokenHash(?string $token_hash): void {
$this->token_hash = $token_hash;
}
public function getTokenHash(): ?string {
return $this->token_hash;
}
public function setCreatedAt(?DateTimeImmutable $created_at): void {
$this->created_at = $created_at;
}
public function getCreatedAt(): ?DateTimeImmutable {
return $this->created_at;
}
public function setRevokedAt(?DateTimeImmutable $revoked_at): void {
$this->revoked_at = $revoked_at;
}
public function getRevokedAt(): ?DateTimeImmutable {
return $this->revoked_at;
}
}
@@ -0,0 +1,77 @@
<?php
namespace WorkbloomERP\Module\v0\Usuario\Repos;
use DateTimeImmutable;
use WorkbloomERP\Services\DBService;
use WorkbloomERP\Module\v0\Usuario\Models\UsuarioEmpresaModel;
class UsuarioEmpresaRepo {
protected string $usuarioEmpresaTable = 'shared.usuario_empresa';
public function __construct(
private DBService $db
) {}
public function insert(UsuarioEmpresaModel $usuarioEmpresaModel): bool {
$query =
"INSERT INTO {$this->usuarioEmpresaTable} (
usuario_id,
empresa_id
) VALUES (
:usuario_id,
:empresa_id
)";
return $this->db->execute(
sql: $query,
params: [
'usuario_id' => $usuarioEmpresaModel->getUsuarioId(),
'empresa_id' => $usuarioEmpresaModel->getEmpresaId()
]
);
}
public function delete(UsuarioEmpresaModel $usuarioEmpresaModel): bool {
$query =
"DELETE FROM {$this->usuarioEmpresaTable} WHERE usuario_id = :usuario_id AND empresa_id = :empresa_id";
return $this->db->execute(
sql: $query,
params: [
'usuario_id' => $usuarioEmpresaModel->getUsuarioId(),
'empresa_id' => $usuarioEmpresaModel->getEmpresaId()
]
);
}
public function findAllByUsuarioId(int $usuario_id): array {
$query =
"SELECT
usuario_id,
empresa_id
FROM {$this->usuarioEmpresaTable}
WHERE usuario_id = :usuario_id";
return $this->db->fetchAll(
sql: $query,
params: [
'usuario_id' => $usuario_id
]
);
}
public function checkAssociationByUsuarioIdAndEmpresaId(int $usuario_id, int $empresa_id): bool {
$query =
"SELECT 1 FROM {$this->usuarioEmpresaTable} WHERE usuario_id = :usuario_id AND empresa_id = :empresa_id";
$result = $this->db->fetchOne(
sql: $query,
params: [
'usuario_id' => $usuario_id,
'empresa_id' => $empresa_id
]
);
return !empty($result);
}
}
+182
View File
@@ -0,0 +1,182 @@
<?php
namespace WorkbloomERP\Module\v0\Usuario\Repos;
use DateTimeImmutable;
use WorkbloomERP\Services\DBService;
use WorkbloomERP\Module\v0\Usuario\Models\UsuarioModel;
class UsuarioRepo {
protected string $usuarioTable = 'usuario';
public function __construct(
private DBService $db
) {}
public function insert(UsuarioModel $usuarioModel): ?UsuarioModel {
$query =
"INSERT INTO {$this->usuarioTable} (
uuid,
is_active,
is_root,
nome_completo,
nome_usuario,
email,
senha_hash,
created_at
) VALUES (
:uuid,
:is_active,
:is_root,
:nome_completo,
:nome_usuario,
:email,
:senha_hash,
:created_at
)";
$usuarioModel->setCreatedAt(created_at: new DateTimeImmutable());
$this->db->execute(
sql: $query,
params: [
'uuid' => $usuarioModel->getUuid(),
'is_active' => $usuarioModel->getIsActive(),
'is_root' => $usuarioModel->getIsRoot(),
'nome_completo' => $usuarioModel->getNomeCompleto(),
'nome_usuario' => $usuarioModel->getNomeUsuario(),
'email' => $usuarioModel->getEmail(),
'senha_hash' => $usuarioModel->getSenhaHash(),
'created_at' => $usuarioModel->getCreatedAt()->format(format: 'Y-m-d H:i:s')
]
);
$usuarioModel->setId(id: $this->db->lastInsertId());
return $usuarioModel;
}
public function update(UsuarioModel $usuarioModel): ?UsuarioModel {
$query =
"UPDATE {$this->usuarioTable} SET
is_active = :is_active,
is_root = :is_root,
nome_completo = :nome_completo,
nome_usuario = :nome_usuario,
email = :email,
senha_hash = :senha_hash,
updated_at = :updated_at
WHERE id = :id OR uuid = :uuid";
$usuarioModel->setUpdatedAt(updated_at: new DateTimeImmutable());
$this->db->execute(
sql: $query,
params: [
':id' => $usuarioModel->getId(),
':uuid' => $usuarioModel->getUuid(),
':is_active' => $usuarioModel->getIsActive(),
':is_root' => $usuarioModel->getIsRoot(),
':nome_completo' => $usuarioModel->getNomeCompleto(),
':nome_usuario' => $usuarioModel->getNomeUsuario(),
':email' => $usuarioModel->getEmail(),
':senha_hash' => $usuarioModel->getSenhaHash(),
':updated_at' => $usuarioModel->getUpdatedAt()->format(format: 'Y-m-d H:i:s')
]
);
return $usuarioModel;
}
public function delete(UsuarioModel $usuarioModel): ?UsuarioModel {
$query =
"UPDATE {$this->usuarioTable} SET
is_active = 0,
deleted_at = :deleted_at
WHERE id = :id OR uuid = :uuid";
$usuarioModel->setDeletedAt(deleted_at: new DateTimeImmutable());
$this->db->execute(
sql: $query,
params: [
':id' => $usuarioModel->getId(),
':uuid' => $usuarioModel->getUuid(),
':deleted_at' => $usuarioModel->getDeletedAt()->format(format: 'Y-m-d H:i:s')
]
);
return $usuarioModel;
}
public function findByIdentifier(string $identifier, mixed $value): ?UsuarioModel {
$query =
"SELECT
id,
uuid,
is_active,
is_root,
nome_completo,
nome_usuario,
email,
senha_hash,
created_at,
updated_at,
deleted_at
FROM {$this->usuarioTable}
WHERE {$identifier} = :value
AND deleted_at IS NULL
LIMIT 1";
$result = $this->db->fetchOne(
sql: $query,
params: [
':value' => $value
]
);
return $result ? $this->mapToModel($result) : null;
}
public function findByLogin(string $login): ?UsuarioModel {
$query =
"SELECT
id,
uuid,
is_active,
is_root,
nome_completo,
nome_usuario,
email,
senha_hash,
created_at,
updated_at,
deleted_at
FROM {$this->usuarioTable}
WHERE (nome_usuario = :login OR email = :login)
AND deleted_at IS NULL
LIMIT 1";
$result = $this->db->fetchOne(
sql: $query,
params: [
':login' => $login
]
);
return $result ? $this->mapToModel($result) : null;
}
private function mapToModel(array $data): UsuarioModel {
return new UsuarioModel(
id: $data['id'],
uuid: $data['uuid'],
is_active: $data['is_active'],
is_root: $data['is_root'],
nome_completo: $data['nome_completo'],
nome_usuario: $data['nome_usuario'],
email: $data['email'],
senha_hash: $data['senha_hash'],
created_at: $data['created_at'] ? new DateTimeImmutable($data['created_at']) : null,
updated_at: $data['updated_at'] ? new DateTimeImmutable($data['updated_at']) : null,
deleted_at: $data['deleted_at'] ? new DateTimeImmutable($data['deleted_at']) : null,
);
}
}
@@ -0,0 +1,110 @@
<?php
namespace WorkbloomERP\Module\v0\Usuario\Repos;
use DateTimeImmutable;
use WorkbloomERP\Services\DBService;
use WorkbloomERP\Module\v0\Usuario\Models\UsuarioSessionModel;
class UsuarioSessionRepo {
protected string $usuarioSessionTable = 'usuario_session';
public function __construct(
private DBService $db
) {}
public function insert(UsuarioSessionModel $usuarioSessionModel): ?UsuarioSessionModel {
$query =
"INSERT INTO {$this->usuarioSessionTable} (
uuid,
usuario_id,
user_agent,
ip_address,
token_hash,
created_at
) VALUES (
:uuid,
:usuario_id,
:user_agent,
:ip_address,
:token_hash,
:created_at
)";
$usuarioSessionModel->setCreatedAt(new DateTimeImmutable());
$this->db->execute(
sql: $query,
params: [
':uuid' => $usuarioSessionModel->getUuid(),
':usuario_id' => $usuarioSessionModel->getUsuarioId(),
':user_agent' => $usuarioSessionModel->getUserAgent(),
':ip_address' => $usuarioSessionModel->getIpAddress(),
':token_hash' => $usuarioSessionModel->getTokenHash(),
':created_at' => $usuarioSessionModel->getCreatedAt()->format('Y-m-d H:i:s')
]
);
$usuarioSessionModel->setId($this->db->lastInsertId());
return $usuarioSessionModel;
}
public function update(UsuarioSessionModel $usuarioSessionModel): bool {
$query =
"UPDATE {$this->usuarioSessionTable} SET
usuario_id = :usuario_id,
user_agent = :user_agent,
ip_address = :ip_address,
token_hash = :token_hash,
revoked_at = :revoked_at
WHERE id = :id OR uuid = :uuid";
return $this->db->execute(
sql: $query,
params: [
':id' => $usuarioSessionModel->getId(),
':uuid' => $usuarioSessionModel->getUuid(),
':usuario_id' => $usuarioSessionModel->getUsuarioId(),
':user_agent' => $usuarioSessionModel->getUserAgent(),
':ip_address' => $usuarioSessionModel->getIpAddress(),
':token_hash' => $usuarioSessionModel->getTokenHash(),
':revoked_at' => $usuarioSessionModel->getRevokedAt() ? $usuarioSessionModel->getRevokedAt()->format('Y-m-d H:i:s') : null
]
);
}
public function findByIdentifier(string $identifier, mixed $value): ?UsuarioSessionModel {
$query =
"SELECT
id,
uuid,
usuario_id,
user_agent,
ip_address,
token_hash,
created_at,
revoked_at
FROM {$this->usuarioSessionTable}
WHERE $identifier = :value
LIMIT 1";
$result = $this->db->fetchOne(
sql: $query,
params: [':value' => $value]
);
return $result ? $this->mapToModel($result) : null;
}
private function mapToModel(array $data) {
return new UsuarioSessionModel(
id: $data['id'],
uuid: $data['uuid'],
usuario_id: $data['usuario_id'],
user_agent: $data['user_agent'],
ip_address: $data['ip_address'],
token_hash: $data['token_hash'],
created_at: $data['created_at'] ? new DateTimeImmutable($data['created_at']) : null,
revoked_at: $data['revoked_at'] ? new DateTimeImmutable($data['revoked_at']) : null,
);
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace WorkbloomERP\Constants;
class BrasilUfsConst {
public const UFS = [
'AC' => 'Acre',
'AL' => 'Alagoas',
'AP' => 'Amapá',
'AM' => 'Amazonas',
'BA' => 'Bahia',
'CE' => 'Ceará',
'DF' => 'Distrito Federal',
'ES' => 'Espírito Santo',
'EX' => 'Estrangeiro',
'GO' => 'Goiás',
'MA' => 'Maranhão',
'MT' => 'Mato Grosso',
'MS' => 'Mato Grosso do Sul',
'MG' => 'Minas Gerais',
'PA' => 'Pará',
'PB' => 'Paraíba',
'PR' => 'Paraná',
'PE' => 'Pernambuco',
'PI' => 'Piauí',
'RJ' => 'Rio de Janeiro',
'RN' => 'Rio Grande do Norte',
'RS' => 'Rio Grande do Sul',
'RO' => 'Rondônia',
'RR' => 'Roraima',
'SC' => 'Santa Catarina',
'SP' => 'São Paulo',
'SE' => 'Sergipe',
'TO' => 'Tocantins'
];
public static function getAll(): array {
return self::UFS;
}
public static function exists(string $uf): bool {
return isset(self::UFS[strtoupper($uf)]);
}
public static function getName(string $uf): ?string {
return self::UFS[strtoupper($uf)] ?? null;
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace WorkbloomERP\Constants;
class SpedCRTConst {
public const CRT = [
"1" => "Simples Nacional",
"2" => "Simples Nacional - Excesso de Sublimite de Receita Bruta",
"3" => "Regime Normal",
"4" => "MEI - Microempreendedor Individual"
];
public static function getAll(): array {
return self::CRT;
}
public static function getDescription(string|int|null $code): string {
return self::CRT[$code] ?? 'Código de regime tributário desconhecido.';
}
public static function exists(string|int|null $code): bool {
return isset(self::CRT[$code]);
}
}
+262
View File
@@ -0,0 +1,262 @@
<?php
namespace WorkbloomERP\Constants;
class SpedPaisesConst {
public const PAISES = [
"0132" => "AFEGANISTAO",
"7560" => "AFRICA DO SUL",
"0175" => "ALBANIA",
"0230" => "ALEMANHA",
"0370" => "ANDORRA",
"0400" => "ANGOLA",
"0418" => "ANGUILLA",
"0434" => "ANTIGUA E BARBUDA",
"0531" => "ARABIA SAUDITA",
"0590" => "ARGELIA",
"0639" => "ARGENTINA",
"0647" => "ARMENIA",
"0655" => "ARUBA",
"0698" => "AUSTRALIA",
"0728" => "AUSTRIA",
"0736" => "AZERBAIDJAO",
"0779" => "BAHAMAS, ILHAS",
"0817" => "BANGLADESH",
"0833" => "BARBADOS",
"0809" => "BAREIN",
"0850" => "BELARUS",
"0876" => "BELGICA",
"0884" => "BELIZE",
"2291" => "BENIN",
"0906" => "BERMUDAS",
"0973" => "BOLIVIA",
"0990" => "BONAIRE, SAINT EUSTATIUS E SABA",
"0981" => "BOSNIA-HERZEGOVINA",
"1015" => "BOTSUANA",
"1098" => "BOUVET, ILHA",
"1058" => "BRASIL",
"1082" => "BRUNEI",
"1112" => "BULGARIA",
"0310" => "BURKINA FASO",
"1155" => "BURUNDI",
"1198" => "BUTAO",
"1279" => "CABO VERDE",
"1457" => "CAMAROES",
"1414" => "CAMBOJA",
"1490" => "CANADA",
"1546" => "CATAR",
"1538" => "CAZAQUISTAO",
"7889" => "CHADE",
"1589" => "CHILE",
"1600" => "CHINA",
"1635" => "CHIPRE",
"5118" => "CHRISTMAS, ILHA (NAVIDAD)",
"7412" => "CINGAPURA",
"1651" => "COCOS (KEELINGS)",
"1694" => "COLOMBIA",
"1775" => "CONGO",
"1872" => "COREIA DO NORTE",
"1937" => "COSTA DO MARFIM",
"1376" => "CAYMAN",
"1732" => "COMORES",
"1830" => "COOK",
"1902" => "COREIA DO SUL",
"1961" => "COSTA RICA",
"1953" => "CROACIA",
"1996" => "CUBA",
"2003" => "CURACAO",
"2321" => "DINAMARCA",
"7838" => "DJIBUTI",
"2356" => "DOMINICA",
"2402" => "EGITO",
"6874" => "EL SALVADOR",
"2445" => "EMIRADOS ARABES UNIDOS",
"2399" => "EQUADOR",
"2437" => "ERITREIA",
"2470" => "ESLOVAQUIA",
"2461" => "ESLOVENIA",
"2453" => "ESPANHA",
"2496" => "ESTADOS UNIDOS",
"2518" => "ESTONIA",
"7544" => "ESWATINI (ANTIGA SUAZILANDIA)",
"2534" => "ETIOPIA",
"2550" => "FALKLAND (MALVINAS)",
"2593" => "FAROE",
"8702" => "FIJI",
"2674" => "FILIPINAS",
"2712" => "FINLANDIA",
"2755" => "FRANCA",
"2810" => "GABAO",
"2852" => "GAMBIA",
"2895" => "GANA",
"2917" => "GEORGIA",
"2933" => "GIBRALTAR",
"2976" => "GRANADA",
"3018" => "GRECIA",
"3050" => "GROENLANDIA",
"3093" => "GUADALUPE",
"3131" => "GUAM",
"3174" => "GUATEMALA",
"1504" => "GUERNSEY, ILHA DO CANAL (INCLUI ALDERNEY E SARK)",
"3379" => "GUIANA",
"3255" => "GUIANA FRANCESA",
"3298" => "GUINE",
"3344" => "GUINE-BISSAU",
"3310" => "GUINE-EQUATORIAL",
"3417" => "HAITI",
"5738" => "HOLANDA (PAISES BAIXOS)",
"3450" => "HONDURAS",
"3514" => "HONG KONG",
"3557" => "HUNGRIA",
"3573" => "IEMEN",
"3611" => "INDIA",
"3654" => "INDONESIA",
"3727" => "IRA",
"3697" => "IRAQUE",
"3751" => "IRLANDA",
"3794" => "ISLANDIA",
"3832" => "ISRAEL",
"3867" => "ITALIA",
"3913" => "JAMAICA",
"3999" => "JAPAO",
"1508" => "JERSEY, ILHA DO CANAL",
"3964" => "JOHNSTON",
"4030" => "JORDANIA",
"4111" => "KIRIBATI",
"1988" => "KUWEIT (ou Coveite)",
"4200" => "LAOS",
"4260" => "LESOTO",
"4278" => "LETONIA",
"4316" => "LIBANO",
"4340" => "LIBERIA",
"4383" => "LIBIA",
"4405" => "LIECHTENSTEIN",
"4421" => "LITUANIA",
"4456" => "LUXEMBURGO",
"4472" => "MACAU",
"4499" => "MACEDONIA",
"4502" => "MADAGASCAR",
"4553" => "MALASIA",
"4588" => "MALAVI",
"4618" => "MALDIVAS",
"4642" => "MALI",
"4677" => "MALTA",
"3595" => "MAN, ILHA DE",
"4723" => "MARIANAS DO NORTE",
"4740" => "MARROCOS",
"4766" => "MARSHALL, ILHAS",
"4774" => "MARTINICA",
"4855" => "MAURICIO",
"4880" => "MAURITANIA",
"4936" => "MEXICO",
"0930" => "MIANMAR",
"4995" => "MICRONESIA",
"5053" => "MOCAMBIQUE",
"4944" => "MOLDAVIA",
"4952" => "MONACO",
"4979" => "MONGOLIA",
"4985" => "MONTENEGRO",
"5010" => "MONTSERRAT",
"5070" => "NAMIBIA",
"5088" => "NAURU",
"5177" => "NEPAL",
"5215" => "NICARAGUA",
"5258" => "NIGER",
"5282" => "NIGERIA",
"5312" => "NIUE",
"5355" => "NORFOLK, ILHA",
"5380" => "NORUEGA",
"5428" => "NOVA CALEDONIA",
"5487" => "NOVA ZELANDIA",
"5568" => "OMA",
"5665" => "PACIFICO, ILHAS DO (POSSESSAO DOS EUA)",
"5754" => "PALAU",
"5780" => "PALESTINA",
"5800" => "PANAMA",
"5452" => "PAPUA NOVA GUINE",
"5762" => "PAQUISTAO",
"5860" => "PARAGUAI",
"5894" => "PERU",
"5932" => "PITCAIRN",
"5991" => "POLINESIA FRANCESA",
"6033" => "POLONIA",
"6114" => "PORTO RICO",
"6076" => "PORTUGAL",
"6238" => "QUENIA",
"6254" => "QUIRGUISTAO",
"6289" => "REINO UNIDO",
"6408" => "REPUBLICA CENTRO-AFRICANA",
"8885" => "REPUBLICA DEMOCRATICA DO CONGO",
"6475" => "REPUBLICA DOMINICANA",
"7919" => "REPUBLICA TCHECA",
"6602" => "REUNIAO",
"6700" => "ROMENIA",
"6750" => "RUANDA",
"6769" => "RUSSIA",
"6858" => "SAARA OCIDENTAL",
"6777" => "SALOMAO, ILHAS",
"6904" => "SAMOA",
"6912" => "SAMOA AMERICANA",
"6971" => "SAN MARINO",
"7102" => "SANTA HELENA",
"7153" => "SANTA LUCIA",
"6955" => "SAO CRISTOVAO E NEVES",
"6980" => "SAO MARTINHO, ILHA DE (PARTE FRANCESA)",
"6998" => "SAO MARTINHO, ILHA DE (PARTE HOLANDESA)",
"7005" => "SAO PEDRO E MIQUELON",
"7200" => "SAO TOME E PRINCIPE",
"7056" => "SAO VICENTE E GRANADINAS",
"7315" => "SEICHELES",
"7285" => "SENEGAL",
"7358" => "SERRA LEOA",
"7370" => "SERVIA",
"7447" => "SIRIA",
"7480" => "SOMALIA",
"7501" => "SRI LANKA",
"7595" => "SUDAO",
"7600" => "SUDÃO DO SUL",
"7641" => "SUECIA",
"7676" => "SUICA",
"7706" => "SURINAME",
"7552" => "SVALBARD E JAN MAYEN",
"7722" => "TADJIQUISTAO",
"7765" => "TAILANDIA",
"1619" => "TAIWAN",
"7803" => "TANZANIA",
"7820" => "TERRITORIO BRITANICO OCEANO INDICO",
"7951" => "TIMOR LESTE",
"8001" => "TOGO",
"8109" => "TONGA",
"8052" => "TOQUELAU",
"8150" => "TRINIDAD E TOBAGO",
"8206" => "TUNISIA",
"8230" => "TURCAS E CAICOS",
"8249" => "TURCOMENISTAO",
"8273" => "TURQUIA",
"8281" => "TUVALU",
"8311" => "UCRANIA",
"8338" => "UGANDA",
"8451" => "URUGUAI",
"8478" => "UZBEQUISTAO",
"5517" => "VANUATU",
"8486" => "VATICANO",
"8508" => "VENEZUELA",
"8583" => "VIETNA",
"8630" => "VIRGENS, ILHAS (BRITANICAS)",
"8664" => "VIRGENS, ILHAS (EUA)",
"8753" => "WALLIS E FUTUNA, ILHAS",
"8907" => "ZAMBIA",
"6653" => "ZIMBABUE"
];
public static function getAll(): array {
return self::PAISES;
}
public static function getNomeByCod(string $codigo): ?string {
return self::PAISES[$codigo] ?? null;
}
public static function exists(string $codigo): bool {
return isset(self::PAISES[$codigo]);
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace WorkbloomERP\Database;
use WorkbloomERP\Services\DBService;
class DBFactory {
/**
* Método para criação simplificada de instâncias do serviço de banco de dados.
*
* Este método implementa o padrão de projeto Factory, permitindo que o desenvolvedor obtenha
* uma instância pronta do **DBService** sem a necessidade de gerenciar manualmente o operador `new`.
* Sua principal vantagem é a flexibilidade para alternar entre diferentes conexões e esquemas
* (schemas) de banco de dados através de parâmetros opcionais, facilitando a operação em
* ambientes multi-banco ou arquiteturas multi-tenant.
*
*
*
* ---
* ## Funcionalidades Principais
* 1. **Abstração de Instanciação:** Centraliza a criação do objeto, permitindo futuras implementações de Singletons ou Service Locators sem quebrar o código cliente.
* 2. **Configuração On-the-fly:** Permite definir a conexão (ex: 'mysql_prod', 'sqlite_test') e o schema de trabalho no momento da criação.
* 3. **Fluidez:** Ideal para ser utilizado em chamadas encadeadas (ex: `DBService::make()->select(...)`), tornando o código mais legível e conciso.
*
* ---
* ## Exemplos de Uso
* - **Conexão Padrão:** `DBService::make();` (Utiliza as definições default da classe).
* - **Esquema Específico:** `DBService::make('', 'vendas');` (Mantém a conexão padrão, mas aponta para o schema de vendas).
* - **Conexão Externa:** `DBService::make('external_db', 'public');`
*
* @param string $connection Identificador da conexão configurada no sistema.
* @param string $schema Nome do esquema de banco de dados a ser selecionado.
* @return DBService Uma nova instância do serviço de banco de dados configurada.
*/
public static function make(string $connection = 'DEFAULT', string $schema = 'shared'): DBService {
return new DBService(connection: $connection, schema: $schema);
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace WorkbloomERP\Exceptions;
use Exception;
use Throwable;
class AppException extends Exception {
protected array $details = [];
public function __construct(string $message, int $code = 500, array $details = [], ?Throwable $previous = null) {
parent::__construct($message, $code, $previous);
$this->details = $details;
}
public function getDetails(): array {
return $this->details;
}
}
+7
View File
@@ -0,0 +1,7 @@
<?php
namespace WorkbloomERP\Services;
use KrothiumAPI\Database\RedisManager;
class CacheService extends RedisManager {
}
+76
View File
@@ -0,0 +1,76 @@
<?php
namespace WorkbloomERP\Services;
use Throwable;
use KrothiumAPI\Database\DBManager;
class DBService {
protected string $connection;
protected ?string $schema;
public function __construct(string $connection = 'DEFAULT', ?string $schema = 'shared') {
$this->schema = $schema;
$this->connection = strtoupper(string: $connection);
}
/**
* Executa dentro de transação
*/
public function transaction(callable $callback): mixed {
DBManager::beginTransaction(connectionName: $this->connection, schema: $this->schema);
try {
$result = $callback($this);
DBManager::commit(connectionName: $this->connection, schema: $this->schema);
return $result;
} catch (Throwable $e) {
DBManager::rollback(connectionName: $this->connection, schema: $this->schema);
throw $e;
}
}
/**
* Executa query sem retorno
*/
public function execute(string $sql, array $params = []): bool {
return DBManager::execute(
sql: $sql,
params: $params,
connectionName: $this->connection,
schema: $this->schema
);
}
/**
* Retorna um único registro
*/
public function fetchOne(string $sql, array $params = []): ?array {
return DBManager::fetchOne(
sql: $sql,
params: $params,
connectionName: $this->connection,
schema: $this->schema
);
}
/**
* Retorna múltiplos registros
*/
public function fetchAll(string $sql, array $params = []): array {
return DBManager::fetchAll(
sql: $sql,
params: $params,
connectionName: $this->connection,
schema: $this->schema
);
}
/**
* Último ID inserido (via PDO interno do driver)
*/
public function lastInsertId(): string {
return DBManager::lastInsertId(
connectionName: $this->connection,
schema: $this->schema
);
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace WorkbloomERP\Utils;
use WorkbloomERP\Services\CacheService;
use WorkbloomERP\Exceptions\AppException;
class CacheUtil {
public static function set(string $key, int $ttl, mixed $value): bool {
return CacheService::set(
key: $key,
value: is_string($value) ? $value : json_encode(
value: match (true) {
is_array($value), is_object($value) => $value,
default => throw new AppException(message: 'Valor para cache deve ser string, array ou objeto.', code: 400)
},
flags: JSON_UNESCAPED_UNICODE
),
ttl: max(-1, $ttl)
);
}
public static function get(string $key): ?array {
$cachedValue = CacheService::get(key: $key);
return $cachedValue ? json_decode($cachedValue, true) : null;
}
public static function delete(array $keys): bool {
return CacheService::del(keys: $keys);
}
}
+150
View File
@@ -0,0 +1,150 @@
<?php
namespace WorkbloomERP\Utils;
use Exception;
use JsonException;
class CryptoUtil {
/**
* Criptografa uma string.
*
* @param string $data
* @return string
* @throws Exception
*/
public static function encrypt(string $data): string {
$algorithm = self::getAlgorithm();
$key = self::getKey();
$ivLength = openssl_cipher_iv_length($algorithm);
if ($ivLength === false) {
throw new Exception('Unable to determine IV length');
}
$iv = random_bytes($ivLength);
$cipherText = openssl_encrypt(
$data,
$algorithm,
$key,
OPENSSL_RAW_DATA,
$iv,
$tag
);
if ($cipherText === false) {
throw new Exception('Failed to encrypt data');
}
return self::encodePayload([
'iv' => base64_encode($iv),
'tag' => base64_encode($tag),
'value' => base64_encode($cipherText)
]);
}
/**
* Descriptografa uma string.
*
* @param string $payload
* @return string
* @throws Exception
*/
public static function decrypt(string $payload): string {
$algorithm = self::getAlgorithm();
$key = self::getKey();
$decodedPayload = self::decodePayload($payload);
$plainText = openssl_decrypt(
base64_decode($decodedPayload['value']),
$algorithm,
$key,
OPENSSL_RAW_DATA,
base64_decode($decodedPayload['iv']),
base64_decode($decodedPayload['tag'])
);
if ($plainText === false) {
throw new Exception('Failed to decrypt data');
}
return $plainText;
}
/**
* Retorna o algoritmo configurado.
*
* @return string
* @throws Exception
*/
private static function getAlgorithm(): string {
$algorithm = $_ENV['SYSTEM_CRYPTO_ALGO'];
if (!in_array($algorithm, openssl_get_cipher_methods(), true)) {
throw new Exception('Invalid cipher algorithm');
}
return $algorithm;
}
/**
* Retorna a chave de criptografia.
*
* A chave deve ser uma string hexadecimal de 64 caracteres (32 bytes).
*
* @return string
* @throws Exception
*/
private static function getKey(): string {
$hexKey = $_ENV['SYSTEM_CRYPTO_KEY'];
if (empty($hexKey)) {
throw new Exception('Encryption key not configured');
}
$key = hex2bin($hexKey);
if ($key === false || strlen($key) !== 32) {
throw new Exception(message: 'Invalid encryption key. Expected 32 bytes (64 hex characters).');
}
return $key;
}
/**
* Codifica o payload para armazenamento.
*
* @param array $payload
* @return string
* @throws JsonException
*/
private static function encodePayload(array $payload): string {
return base64_encode(
json_encode(
$payload,
JSON_THROW_ON_ERROR
)
);
}
/**
* Decodifica o payload criptografado.
*
* @param string $payload
* @return array
* @throws Exception
*/
private static function decodePayload(string $payload): array {
try {
$decoded = json_decode(
base64_decode($payload),
true,
512,
JSON_THROW_ON_ERROR
);
} catch (JsonException $e) {
throw new Exception(message: 'Invalid encrypted payload', previous: $e);
}
foreach (['iv', 'tag', 'value'] as $field) {
if (!isset($decoded[$field])) {
throw new Exception(sprintf('Missing "%s" field in encrypted payload', $field));
}
}
return $decoded;
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace WorkbloomERP\Utils;
use Exception;
class CypherUtil {
public static function hash(string $data): string {
if (!isset($_ENV['SYSTEM_CYPHER_ALGO']) || !isset($_ENV['SYSTEM_CYPHER_PEPPER'])) {
throw new Exception('Missing required environment variables for hashing');
}
return hash_hmac(algo: $_ENV['SYSTEM_CYPHER_ALGO'], data: $data, key: $_ENV['SYSTEM_CYPHER_PEPPER']);
}
public static function verify(string $data, string $hash): bool {
return hash_equals(known_string: self::hash($data), user_string: $hash);
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace WorkbloomERP\Utils;
use Exception;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use DateTimeImmutable;
class JwtUtil {
private int $ttl;
private string $issuer;
private string $secretKey;
private string $algorithm;
public function __construct(string $secretKey, string $algorithm = 'HS256', int $ttl = 3600, string $issuer = 'workbloomerp') {
if (empty($secretKey)) {
throw new Exception(message: 'JWT secret não configurado.');
}
$this->ttl = $ttl;
$this->secretKey = $secretKey;
$this->algorithm = $algorithm;
$this->issuer = $issuer;
}
public function generate(array $payload): string {
$defaultClaims = [];
$defaultClaims['iat'] = $payload['iat'] ?? (new DateTimeImmutable())->getTimestamp();
$defaultClaims['exp'] = $payload['exp'] ?? ((new DateTimeImmutable())->modify("+{$this->ttl} seconds")->getTimestamp());
$finalPayload = array_merge($payload, $defaultClaims);
return JWT::encode(
payload: $finalPayload,
key: $this->secretKey,
alg: $this->algorithm
);
}
public function validate(string $token): object {
return JWT::decode(
jwt: $token,
keyOrKeyArray: new Key(
keyMaterial: $this->secretKey,
algorithm: $this->algorithm
)
);
}
}
+93
View File
@@ -0,0 +1,93 @@
<?php
namespace WorkbloomERP\Utils;
class SanitizeUtil {
public static function string(mixed $value): ?string {
if ($value === null) {
return null;
}
$value = trim(string: $value);
$value = strip_tags(string: $value);
return $value;
}
public static function email(mixed $value): ?string {
if ($value === null) {
return null;
}
$value = filter_var(value: $value, filter: FILTER_SANITIZE_EMAIL);
return $value ?: '';
}
public static function int(mixed $value): ?int {
if ($value === null) {
return null;
}
return (int) filter_var(value: $value, filter: FILTER_SANITIZE_NUMBER_INT);
}
public static function float(mixed $value): ?float {
if($value === null) {
return null;
}
return (float) filter_var(
value: $value,
filter: FILTER_SANITIZE_NUMBER_FLOAT,
options: FILTER_FLAG_ALLOW_FRACTION | FILTER_FLAG_ALLOW_THOUSAND
);
}
public static function document(mixed $value): ?string {
if ($value === null) {
return null;
}
// remove espaços
$value = trim($value);
// converte caracteres especiais (Ç -> C, Á -> A, etc.)
$value = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value);
// mantém apenas letras e números
$value = preg_replace('/[^a-zA-Z0-9]/', '', $value);
return $value;
}
public static function boolean(mixed $value): ?bool {
if ($value === null) {
return null;
}
if (is_bool($value)) {
return $value;
}
if (is_string($value)) {
$value = strtolower($value);
if (in_array($value, ['true', '1', 'yes'], true)) {
return true;
}
if (in_array($value, ['false', '0', 'no'], true)) {
return false;
}
}
if (is_int($value)) {
return $value === 1;
}
// Se não for possível converter, retorna null
return null;
}
public static function phone(mixed $value, bool $withCountryCode = false): ?string {
if ($value === null) {
return null;
}
// remove tudo que não for número
$value = preg_replace('/\D/', '', $value);
if (!$value) {
return '';
}
// adiciona DDI do Brasil se não tiver
if ($withCountryCode) {
if (strlen($value) === 10 || strlen($value) === 11) {
$value = "55{$value}";
}
}
return $value;
}
}
+89
View File
@@ -0,0 +1,89 @@
<?php
namespace WorkbloomERP\Utils;
class ValidateUtil {
public static function cpf(?string $cpf): bool {
if ($cpf == null) {
return false;
}
// Remove tudo que não for número
$cpf = preg_replace('/\D/', '', $cpf);
// Precisa ter 11 dígitos
if (strlen($cpf) !== 11) {
return false;
}
// Bloqueia CPFs com todos os dígitos iguais (11111111111, 00000000000, etc)
if (preg_match('/^(\d)\1{10}$/', $cpf)) {
return false;
}
// Valida primeiro dígito verificador
for ($t = 9; $t < 11; $t++) {
$soma = 0;
for ($i = 0; $i < $t; $i++) {
$soma += $cpf[$i] * (($t + 1) - $i);
}
$digito = ((10 * $soma) % 11) % 10;
if ($cpf[$t] != $digito) {
return false;
}
}
return true;
}
public static function cnpj(?string $cnpj): bool {
if ($cnpj == null) {
return false;
}
// Remove máscara
$cnpj = strtoupper(preg_replace('/[^A-Z0-9]/', '', $cnpj));
// Tamanho fixo
if (strlen($cnpj) !== 14) {
return false;
}
// Bloqueia CNPJs com todos os dígitos iguais (11111111111111, 00000000000000, etc)
if (preg_match('/^(\d)\1{13}$/', $cnpj)) {
return false;
}
// Regex: 12 alfanum + 2 numéricos
if (!preg_match('/^[A-Z0-9]{12}[0-9]{2}$/', $cnpj)) {
return false;
}
// Converte para valores numéricos
$valores = [];
for ($i = 0; $i < 14; $i++) {
$char = $cnpj[$i];
if (ctype_digit($char)) {
$valores[$i] = (int)$char;
} else {
$valores[$i] = ord($char) - 48;
}
}
// === Primeiro DV ===
$pesos1 = [5,4,3,2,9,8,7,6,5,4,3,2];
$soma = 0;
for ($i = 0; $i < 12; $i++) {
$soma += $valores[$i] * $pesos1[$i];
}
$resto = $soma % 11;
$dv1 = ($resto < 2) ? 0 : 11 - $resto;
if ($dv1 !== $valores[12]) {
return false;
}
// === Segundo DV ===
$pesos2 = [6,5,4,3,2,9,8,7,6,5,4,3,2];
$soma = 0;
for ($i = 0; $i < 13; $i++) {
$soma += $valores[$i] * $pesos2[$i];
}
$resto = $soma % 11;
$dv2 = ($resto < 2) ? 0 : 11 - $resto;
return $dv2 === $valores[13];
}
}
+44
View File
@@ -0,0 +1,44 @@
{
"name": "cybercore-systems/workbloom-erp-backend",
"type": "project",
"require": {
"php": ">=8.2",
"ext-pdo": "*",
"ext-json": "*",
"ext-openssl": "*",
"ext-dom": "*",
"ext-libxml": "*",
"ext-simplexml": "*",
"ext-soap": "*",
"ext-zip": "*",
"ext-fileinfo": "*",
"ext-gd": "*",
"ext-bcmath": "*",
"claudecio/krothiumapi": "dev-dev/new-http",
"vlucas/phpdotenv": "v5.6.2",
"ramsey/uuid": "4.9.2",
"lcobucci/jwt": "5.6.0",
"defuse/php-encryption": "v2.4.0",
"symfony/var-dumper": "v7.4.8",
"monolog/monolog": "3.10.0",
"nesbot/carbon": "3.11.4",
"guzzlehttp/guzzle": "7.10.4",
"respect/validation": "2.4.12",
"league/flysystem": "3.34.0",
"league/flysystem-local": "3.31.0",
"tecnickcom/tcpdf": "6.11.3",
"bacon/bacon-qr-code": "v3.1.1",
"firebase/php-jwt": "v6.11.1"
},
"autoload": {
"psr-4": {
"WorkbloomERP\\": "app/Shared/",
"WorkbloomERP\\Module\\": "app/Module/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
}
}
Generated
+2985
View File
File diff suppressed because it is too large Load Diff
+101
View File
@@ -0,0 +1,101 @@
DROP SCHEMA IF EXISTS shared CASCADE;
CREATE SCHEMA IF NOT EXISTS shared;
CREATE TABLE IF NOT EXISTS shared.usuario (
"id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"uuid" UUID NOT NULL UNIQUE,
"is_active" SMALLINT NOT NULL DEFAULT 1,
"is_root" SMALLINT NOT NULL DEFAULT 0,
"nome_completo" VARCHAR(150) NOT NULL,
"nome_usuario" VARCHAR(50) NOT NULL UNIQUE,
"email" VARCHAR(255) NOT NULL UNIQUE,
"senha_hash" VARCHAR(255) NOT NULL,
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP DEFAULT NULL,
"deleted_at" TIMESTAMP DEFAULT NULL,
PRIMARY KEY("id")
);
CREATE TABLE IF NOT EXISTS shared.empresa (
"id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"uuid" UUID NOT NULL UNIQUE,
"is_active" SMALLINT NOT NULL DEFAULT 1,
"nome_empresarial" VARCHAR(150) NOT NULL,
"nome_fantasia" VARCHAR(60) DEFAULT NULL,
"tipo" VARCHAR(50) NOT NULL DEFAULT 'MATRIZ',
"matriz_id" INTEGER DEFAULT NULL,
"document_cnpj" CHAR(14) NOT NULL UNIQUE,
"document_ie" CHAR(14) DEFAULT NULL,
"document_im" CHAR(14) DEFAULT NULL,
"regime_tributario" CHAR(1) NOT NULL,
"end_cep" VARCHAR(8) NOT NULL,
"end_ibge" VARCHAR(8) NOT NULL,
"end_logradouro" VARCHAR(150) NOT NULL,
"end_numero" VARCHAR(20) NOT NULL,
"end_complemento" VARCHAR(50) DEFAULT NULL,
"end_bairro" VARCHAR(50) NOT NULL,
"end_cidade" VARCHAR(150) NOT NULL,
"end_uf" CHAR(2) NOT NULL,
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP DEFAULT NULL,
"deleted_at" TIMESTAMP DEFAULT NULL,
PRIMARY KEY("id")
);
CREATE TABLE IF NOT EXISTS shared.usuario_empresa (
"usuario_id" INTEGER NOT NULL,
"empresa_id" INTEGER NOT NULL,
PRIMARY KEY("usuario_id", "empresa_id")
);
CREATE TABLE IF NOT EXISTS shared.usuario_sessao (
"id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"uuid" UUID NOT NULL UNIQUE,
"usuario_id" INTEGER NOT NULL,
"user_agent" TEXT DEFAULT NULL,
"ip_address" VARCHAR(45) DEFAULT NULL,
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"revoked_at" TIMESTAMP DEFAULT NULL,
PRIMARY KEY("id")
);
CREATE TABLE IF NOT EXISTS shared.contato (
"id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"uuid" UUID NOT NULL UNIQUE,
"empresa_id" INTEGER NOT NULL,
"is_active" SMALLINT NOT NULL DEFAULT 1,
"tipo" VARCHAR(50) NOT NULL,
"nome_empresarial" VARCHAR(150) NOT NULL,
"nome_fantasia" VARCHAR(60) DEFAULT NULL,
"personalidade" CHAR(2) NOT NULL DEFAULT 'PJ',
"document_cpf" CHAR(11) DEFAULT NULL,
"document_cnpj" CHAR(14) DEFAULT NULL,
"regime_tributario" CHAR(1) DEFAULT NULL,
"contribuinte_icms" CHAR(1) DEFAULT NULL,
"orgao_publico" VARCHAR(9) NOT NULL DEFAULT 'NAO',
"document_ie" CHAR(14) DEFAULT NULL,
"document_im" CHAR(14) DEFAULT NULL,
"document_is" CHAR(9) DEFAULT NULL,
"end_pais" VARCHAR(255) DEFAULT NULL,
"end_cep" VARCHAR(8) NOT NULL,
"end_ibge" VARCHAR(8) DEFAULT NULL,
"end_logradouro" VARCHAR(150) NOT NULL,
"end_numero" VARCHAR(20) NOT NULL,
"end_complemento" VARCHAR(50) DEFAULT NULL,
"end_bairro" VARCHAR(50) NOT NULL,
"end_cidade" VARCHAR(150) NOT NULL,
"end_uf" CHAR(2) NOT NULL,
"info_email" VARCHAR(255) DEFAULT NULL,
"info_email_nfe" VARCHAR(80) DEFAULT NULL,
"info_observacao" TEXT DEFAULT NULL,
"info_telefone" VARCHAR(11) DEFAULT NULL,
"info_uso_consumo_ibs_cbs" CHAR(1) NOT NULL DEFAULT '0',
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP DEFAULT NULL,
"deleted_at" TIMESTAMP DEFAULT NULL,
PRIMARY KEY("id")
);
ALTER TABLE shared.usuario_empresa ADD FOREIGN KEY("usuario_id") REFERENCES shared.usuario("id") ON UPDATE NO ACTION ON DELETE NO ACTION;
ALTER TABLE shared.usuario_empresa ADD FOREIGN KEY("empresa_id") REFERENCES shared.empresa("id") ON UPDATE NO ACTION ON DELETE NO ACTION;
ALTER TABLE shared.usuario_sessao ADD FOREIGN KEY("usuario_id") REFERENCES shared.usuario("id") ON UPDATE NO ACTION ON DELETE NO ACTION;
ALTER TABLE shared.contato ADD FOREIGN KEY("empresa_id") REFERENCES shared.empresa("id") ON UPDATE NO ACTION ON DELETE NO ACTION;
+22
View File
@@ -0,0 +1,22 @@
-- Insere um usuário administrador para facilitar o desenvolvimento e testes iniciais do sistema.
INSERT INTO shared.usuario (id, uuid, is_active, is_root, nome_completo, nome_usuario, email, senha_hash) VALUES
(1, '019ea98d-8876-75c5-a39f-dcb3ad56faad', 1, 1, 'Claudecio Santos da Costa Martins Júnior', 'claudecio.junior', 'claudecio@cybercore.com.br', '$2a$12$13xi5h7mS5NJ/jdG9DarC.SsuMq/3gQoDBwmOjt2xVLLXRhLQ2exC'),
(2, '019eb148-946b-7665-99bd-eb6bc5923582', 1, 1, 'Administastror', 'adm', 'adm@cybercore.com.br', '$2a$12$cnblvb71zjc1f3lWIIBsXundwrNOdyiqssft.g.kaNBQAkrNnFhJC');
SELECT setval(pg_get_serial_sequence('shared.usuario', 'id'), (SELECT MAX(id) FROM shared.usuario));
-- Insere uma empresa para facilitar o desenvolvimento e testes iniciais do sistema.
INSERT INTO shared.empresa (id, uuid, is_active, nome_empresarial, nome_fantasia, tipo, matriz_id, document_cnpj, document_ie, document_im, regime_tributario, end_cep, end_ibge, end_logradouro, end_numero, end_complemento, end_bairro, end_cidade, end_uf) VALUES
(1, '019ea998-7fc6-7fba-b433-2836943db898', 1, 'Yuri e Luan Alimentos ME', 'Yuri e Luan Alimentos', 'MATRIZ', NULL, '0KMTA6PD000101', '012303712', '631763333', 1, '60130180', '2304400', 'Travessa Manuel Maia', '966', NULL, 'Joaquim Távora', 'Fortaleza', 'CE'),
(2, '019ea998-bc6a-73d7-82a8-63a29aa033be', 1, 'Yuri e Luan Alimentos ME', 'Yuri e Luan Alimentos', 'FILIAL', 1, '0KMTA6PD9ZK308', '632566779', '803316', 1, '60600970', '2303709', 'Rodovia CE-090', 'S/N', 'Km 01', 'Itambém', 'Caucaia', 'CE'),
(3, '019ea9a6-69d7-7fc9-b624-2f45b129a07c', 1, 'Antonio e Sueli Marcenaria Ltda', 'Antonio e Sueli Marcenaria', 'MATRIZ', NULL, '53449352000195', '092745032', '023897856', 1, '60346005', '2304400', 'Rua Cecil Salgado', '868', NULL, 'Vila Velha', 'Fortaleza', 'CE'),
(4, '019ea9a6-69d7-7fc9-b624-2f45b129a07d', 1, 'Antonio e Sueli Marcenaria Ltda', 'Antonio e Sueli Marcenaria Vila Velha', 'FILIAL', 3, '53449352000276', '092745032', '023897856', 1, '60346005', '2304400', 'Travessa Colinas', '998', NULL, 'Edson Queiroz', 'Fortaleza', 'CE');
SELECT setval(pg_get_serial_sequence('shared.empresa', 'id'), (SELECT MAX(id) FROM shared.empresa));
-- Associa o usuário administrador à empresa criada.
INSERT INTO shared.usuario_empresa (usuario_id, empresa_id) VALUES
(1, 1),
(1, 2),
(1, 3),
(1, 4);
+99
View File
@@ -0,0 +1,99 @@
CREATE TABLE IF NOT EXISTS shared.usuario (
"id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"uuid" UUID NOT NULL UNIQUE,
"is_active" SMALLINT NOT NULL DEFAULT 1,
"is_root" SMALLINT NOT NULL DEFAULT 0,
"nome_completo" VARCHAR(150) NOT NULL,
"nome_usuario" VARCHAR(50) NOT NULL UNIQUE,
"email" VARCHAR(255) NOT NULL UNIQUE,
"senha_hash" VARCHAR(255) NOT NULL,
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP DEFAULT NULL,
"deleted_at" TIMESTAMP DEFAULT NULL,
PRIMARY KEY("id")
);
CREATE TABLE IF NOT EXISTS shared.empresa (
"id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"uuid" UUID NOT NULL UNIQUE,
"is_active" SMALLINT NOT NULL DEFAULT 1,
"nome_empresarial" VARCHAR(150) NOT NULL,
"nome_fantasia" VARCHAR(60) DEFAULT NULL,
"tipo" VARCHAR(50) NOT NULL DEFAULT 'MATRIZ',
"matriz_id" INTEGER DEFAULT NULL,
"document_cnpj" CHAR(14) NOT NULL UNIQUE,
"document_ie" CHAR(14) DEFAULT NULL,
"document_im" CHAR(14) DEFAULT NULL,
"regime_tributario" CHAR(1) NOT NULL,
"end_cep" VARCHAR(8) NOT NULL,
"end_ibge" VARCHAR(8) NOT NULL,
"end_logradouro" VARCHAR(150) NOT NULL,
"end_numero" VARCHAR(20) NOT NULL,
"end_complemento" VARCHAR(50) DEFAULT NULL,
"end_bairro" VARCHAR(50) NOT NULL,
"end_cidade" VARCHAR(150) NOT NULL,
"end_uf" CHAR(2) NOT NULL,
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP DEFAULT NULL,
"deleted_at" TIMESTAMP DEFAULT NULL,
PRIMARY KEY("id")
);
CREATE TABLE IF NOT EXISTS shared.usuario_empresa (
"usuario_id" INTEGER NOT NULL,
"empresa_id" INTEGER NOT NULL,
PRIMARY KEY("usuario_id", "empresa_id")
);
CREATE TABLE IF NOT EXISTS shared.usuario_sessao (
"id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"uuid" UUID NOT NULL UNIQUE,
"usuario_id" INTEGER NOT NULL,
"user_agent" TEXT DEFAULT NULL,
"ip_address" VARCHAR(45) DEFAULT NULL,
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"revoked_at" TIMESTAMP DEFAULT NULL,
PRIMARY KEY("id")
);
CREATE TABLE IF NOT EXISTS shared.contato (
"id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"uuid" UUID NOT NULL UNIQUE,
"empresa_id" INTEGER NOT NULL,
"is_active" SMALLINT NOT NULL DEFAULT 1,
"tipo" VARCHAR(50) NOT NULL,
"nome_empresarial" VARCHAR(150) NOT NULL,
"nome_fantasia" VARCHAR(60) DEFAULT NULL,
"personalidade" CHAR(2) NOT NULL DEFAULT 'PJ',
"document_cpf" CHAR(11) DEFAULT NULL,
"document_cnpj" CHAR(14) DEFAULT NULL,
"regime_tributario" CHAR(1) DEFAULT NULL,
"contribuinte_icms" CHAR(1) DEFAULT NULL,
"orgao_publico" VARCHAR(9) NOT NULL DEFAULT 'NAO',
"document_ie" CHAR(14) DEFAULT NULL,
"document_im" CHAR(14) DEFAULT NULL,
"document_is" CHAR(9) DEFAULT NULL,
"end_pais" VARCHAR(255) DEFAULT NULL,
"end_cep" VARCHAR(8) NOT NULL,
"end_ibge" VARCHAR(8) DEFAULT NULL,
"end_logradouro" VARCHAR(150) NOT NULL,
"end_numero" VARCHAR(20) NOT NULL,
"end_complemento" VARCHAR(50) DEFAULT NULL,
"end_bairro" VARCHAR(50) NOT NULL,
"end_cidade" VARCHAR(150) NOT NULL,
"end_uf" CHAR(2) NOT NULL,
"info_email" VARCHAR(255) DEFAULT NULL,
"info_email_nfe" VARCHAR(80) DEFAULT NULL,
"info_observacao" TEXT DEFAULT NULL,
"info_telefone" VARCHAR(11) DEFAULT NULL,
"info_uso_consumo_ibs_cbs" CHAR(1) NOT NULL DEFAULT '0',
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP DEFAULT NULL,
"deleted_at" TIMESTAMP DEFAULT NULL,
PRIMARY KEY("id")
);
ALTER TABLE shared.usuario_empresa ADD FOREIGN KEY("usuario_id") REFERENCES shared.usuario("id") ON UPDATE NO ACTION ON DELETE NO ACTION;
ALTER TABLE shared.usuario_empresa ADD FOREIGN KEY("empresa_id") REFERENCES shared.empresa("id") ON UPDATE NO ACTION ON DELETE NO ACTION;
ALTER TABLE shared.usuario_sessao ADD FOREIGN KEY("usuario_id") REFERENCES shared.usuario("id") ON UPDATE NO ACTION ON DELETE NO ACTION;
ALTER TABLE shared.contato ADD FOREIGN KEY("empresa_id") REFERENCES shared.empresa("id") ON UPDATE NO ACTION ON DELETE NO ACTION;
+8
View File
@@ -0,0 +1,8 @@
<IfModule mod_rewrite.c>
RewriteEngine On
# Se for arquivo ou pasta real, deixa passar
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# Tudo vai pro controller
RewriteRule ^ index.php [L,QSA]
</IfModule>
+52
View File
@@ -0,0 +1,52 @@
<?php
// Importa autoload do Composer
require_once realpath(path: __DIR__ . '/../vendor/autoload.php');
use Dotenv\Dotenv;
use KrothiumAPI\KrothiumAPI;
use KrothiumAPI\Http\Router;
// Carrega variáveis de ambiente
$dotenv = Dotenv::createImmutable(paths: realpath(path: __DIR__ . '/../'));
$dotenv->load();
// ======================================
// Inicializa KrothiumAPI com configs
// ======================================
KrothiumAPI::init(config: [
'errors' => [
'error_log' => realpath(path: __DIR__ . '/../storage/Logs/php-error.log'),
],
'constants' => [
'APP_SYS_MODE' => 'DEV', // DEV | PROD
'ROOT_SYSTEM_PATH' => realpath(path: __DIR__ . "/.."),
'INI_SYSTEM_PATH' => realpath(path: __DIR__ . "/../src"),
'MODULE_PATH' => realpath(path: __DIR__ . "/../src/Module"),
'STORAGE_FOLDER_PATH' => realpath(path: __DIR__ . "/../storage"),
'ROUTER_ALLOWED_ORIGINS' => [
'*'
]
],
'system' => [
'enable_session' => true,
'default_timezone' => 'America/Fortaleza',
],
'logger' => [
'driver' => 'FILE',
'logDir' => realpath(path: __DIR__ . '/../storage/Logs')
]
]);
// Importa Rotas da v0
Router::group(
prefix: '/v0',
callback: function() {
require_once realpath(path: __DIR__ . '/../app/Module/v0/Auth/Routes/Routes.php');
require_once realpath(path: __DIR__ . '/../app/Module/v0/Contato/Routes/Routes.php');
}
);
// ============================
// Dispara o roteador
// ============================
KrothiumAPI::routerDispatch();
View File
View File
+25
View File
@@ -0,0 +1,25 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInitcdaa22da496807f150a36aa282af66ef::getLoader();
+22
View File
@@ -0,0 +1,22 @@
Copyright (c) 2017-present, Ben Scholzen 'DASPRiD'
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+82
View File
@@ -0,0 +1,82 @@
# QR Code generator
[![PHP CI](https://github.com/Bacon/BaconQrCode/actions/workflows/ci.yml/badge.svg)](https://github.com/Bacon/BaconQrCode/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/Bacon/BaconQrCode/branch/master/graph/badge.svg?token=rD0HcAiEEx)](https://codecov.io/gh/Bacon/BaconQrCode)
[![Latest Stable Version](https://poser.pugx.org/bacon/bacon-qr-code/v/stable)](https://packagist.org/packages/bacon/bacon-qr-code)
[![Total Downloads](https://poser.pugx.org/bacon/bacon-qr-code/downloads)](https://packagist.org/packages/bacon/bacon-qr-code)
[![License](https://poser.pugx.org/bacon/bacon-qr-code/license)](https://packagist.org/packages/bacon/bacon-qr-code)
## Introduction
BaconQrCode is a port of QR code portion of the ZXing library. It currently
only features the encoder part, but could later receive the decoder part as
well.
As the Reed Solomon codec implementation of the ZXing library performs quite
slow in PHP, it was exchanged with the implementation by Phil Karn.
## Example usage
```php
use BaconQrCode\Renderer\ImageRenderer;
use BaconQrCode\Renderer\Image\ImagickImageBackEnd;
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
use BaconQrCode\Writer;
$renderer = new ImageRenderer(
new RendererStyle(400),
new ImagickImageBackEnd()
);
$writer = new Writer($renderer);
$writer->writeFile('Hello World!', 'qrcode.png');
```
## Available image renderer back ends
BaconQrCode comes with multiple back ends for rendering images. Currently included are the following:
- `ImagickImageBackEnd`: renders raster images using the Imagick library
- `SvgImageBackEnd`: renders SVG files using XMLWriter
- `EpsImageBackEnd`: renders EPS files
### GDLib Renderer
GD library has so many limitations, that GD support is not added as backend, but as separated renderer.
Use `GDLibRenderer` instead of `ImageRenderer`. These are the limitations:
- Does not support gradient.
- Does not support any curves, so you QR code is always squared.
Example usage:
```php
use BaconQrCode\Renderer\GDLibRenderer;
use BaconQrCode\Writer;
$renderer = new GDLibRenderer(400);
$writer = new Writer($renderer);
$writer->writeFile('Hello World!', 'qrcode.png');
```
## Known issues
### ImagickImageBackEnd: white pixel artifacts
When using `ImagickImageBackEnd`, single white pixels may appear inside filled regions. This is
most visible with margin 0 (where artifacts appear at the image edge), but can in theory occur at
any position. The cause is a bug in ImageMagick's path fill rasterizer (`GetFillAlpha` in
`MagickCore/draw.c`): an off-by-one error in the winding number calculation combined with an edge
skipping bug in the scanline processing can incorrectly classify pixels as outside the polygon.
The bug cannot be reliably worked around in this library:
- **Canvas padding** (rendering on a larger canvas and cropping) does not work because the required
padding depends on the scale factor, path complexity, and ImageMagick's internal edge processing
state. No fixed padding value is safe for all inputs.
- **Post-processing** (scanning for and fixing isolated white pixels) risks corrupting legitimate
rendering features such as curved module edges.
For artifact-free output, use `SvgImageBackEnd` or `GDLibRenderer` instead.
## Development
To run unit tests, you need to have [Node.js](https://nodejs.org/en) and the pixelmatch library installed. Running
`npm install` will install this for you.
+51
View File
@@ -0,0 +1,51 @@
{
"name": "bacon/bacon-qr-code",
"description": "BaconQrCode is a QR code generator for PHP.",
"license": "BSD-2-Clause",
"homepage": "https://github.com/Bacon/BaconQrCode",
"require": {
"php": "^8.1",
"ext-iconv": "*",
"dasprid/enum": "^1.0.3"
},
"suggest": {
"ext-imagick": "to generate QR code images"
},
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "https://dasprids.de/",
"role": "Developer"
}
],
"autoload": {
"psr-4": {
"BaconQrCode\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"BaconQrCodeTest\\": "test/"
}
},
"require-dev": {
"phpunit/phpunit": "^10.5.11 || ^11.0.4",
"spatie/phpunit-snapshot-assertions": "^5.1.5",
"spatie/pixelmatch-php": "^1.2.0",
"squizlabs/php_codesniffer": "^3.9",
"phly/keep-a-changelog": "^2.12"
},
"config": {
"allow-plugins": {
"ocramius/package-versions": true,
"php-http/discovery": true
}
},
"archive": {
"exclude": [
"/test",
"/phpunit.xml.dist"
]
}
}
+364
View File
@@ -0,0 +1,364 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
use BaconQrCode\Exception\InvalidArgumentException;
use SplFixedArray;
/**
* A simple, fast array of bits.
*/
final class BitArray
{
/**
* Bits represented as an array of integers.
*
* @var SplFixedArray<int>
*/
private SplFixedArray $bits;
/**
* Creates a new bit array with a given size.
*/
public function __construct(private int $size = 0)
{
$this->bits = SplFixedArray::fromArray(array_fill(0, ($this->size + 31) >> 3, 0));
}
/**
* Gets the size in bits.
*/
public function getSize() : int
{
return $this->size;
}
/**
* Gets the size in bytes.
*/
public function getSizeInBytes() : int
{
return ($this->size + 7) >> 3;
}
/**
* Ensures that the array has a minimum capacity.
*/
public function ensureCapacity(int $size) : void
{
if ($size > count($this->bits) << 5) {
$this->bits->setSize(($size + 31) >> 5);
}
}
/**
* Gets a specific bit.
*/
public function get(int $i) : bool
{
return 0 !== ($this->bits[$i >> 5] & (1 << ($i & 0x1f)));
}
/**
* Sets a specific bit.
*/
public function set(int $i) : void
{
$this->bits[$i >> 5] = $this->bits[$i >> 5] | 1 << ($i & 0x1f);
}
/**
* Flips a specific bit.
*/
public function flip(int $i) : void
{
$this->bits[$i >> 5] ^= 1 << ($i & 0x1f);
}
/**
* Gets the next set bit position from a given position.
*/
public function getNextSet(int $from) : int
{
if ($from >= $this->size) {
return $this->size;
}
$bitsOffset = $from >> 5;
$currentBits = $this->bits[$bitsOffset];
$bitsLength = count($this->bits);
$currentBits &= ~((1 << ($from & 0x1f)) - 1);
while (0 === $currentBits) {
if (++$bitsOffset === $bitsLength) {
return $this->size;
}
$currentBits = $this->bits[$bitsOffset];
}
$result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits);
return min($result, $this->size);
}
/**
* Gets the next unset bit position from a given position.
*/
public function getNextUnset(int $from) : int
{
if ($from >= $this->size) {
return $this->size;
}
$bitsOffset = $from >> 5;
$currentBits = ~$this->bits[$bitsOffset];
$bitsLength = count($this->bits);
$currentBits &= ~((1 << ($from & 0x1f)) - 1);
while (0 === $currentBits) {
if (++$bitsOffset === $bitsLength) {
return $this->size;
}
$currentBits = ~$this->bits[$bitsOffset];
}
$result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits);
return min($result, $this->size);
}
/**
* Sets a bulk of bits.
*/
public function setBulk(int $i, int $newBits) : void
{
$this->bits[$i >> 5] = $newBits;
}
/**
* Sets a range of bits.
*
* @throws InvalidArgumentException if end is smaller than start
*/
public function setRange(int $start, int $end) : void
{
if ($end < $start) {
throw new InvalidArgumentException('End must be greater or equal to start');
}
if ($end === $start) {
return;
}
--$end;
$firstInt = $start >> 5;
$lastInt = $end >> 5;
for ($i = $firstInt; $i <= $lastInt; ++$i) {
$firstBit = $i > $firstInt ? 0 : $start & 0x1f;
$lastBit = $i < $lastInt ? 31 : $end & 0x1f;
if (0 === $firstBit && 31 === $lastBit) {
$mask = 0x7fffffff;
} else {
$mask = 0;
for ($j = $firstBit; $j < $lastBit; ++$j) {
$mask |= 1 << $j;
}
}
$this->bits[$i] = $this->bits[$i] | $mask;
}
}
/**
* Clears the bit array, unsetting every bit.
*/
public function clear() : void
{
$bitsLength = count($this->bits);
for ($i = 0; $i < $bitsLength; ++$i) {
$this->bits[$i] = 0;
}
}
/**
* Checks if a range of bits is set or not set.
* @throws InvalidArgumentException if end is smaller than start
*/
public function isRange(int $start, int $end, bool $value) : bool
{
if ($end < $start) {
throw new InvalidArgumentException('End must be greater or equal to start');
}
if ($end === $start) {
return true;
}
--$end;
$firstInt = $start >> 5;
$lastInt = $end >> 5;
for ($i = $firstInt; $i <= $lastInt; ++$i) {
$firstBit = $i > $firstInt ? 0 : $start & 0x1f;
$lastBit = $i < $lastInt ? 31 : $end & 0x1f;
if (0 === $firstBit && 31 === $lastBit) {
$mask = 0x7fffffff;
} else {
$mask = 0;
for ($j = $firstBit; $j <= $lastBit; ++$j) {
$mask |= 1 << $j;
}
}
if (($this->bits[$i] & $mask) !== ($value ? $mask : 0)) {
return false;
}
}
return true;
}
/**
* Appends a bit to the array.
*/
public function appendBit(bool $bit) : void
{
$this->ensureCapacity($this->size + 1);
if ($bit) {
$this->bits[$this->size >> 5] = $this->bits[$this->size >> 5] | (1 << ($this->size & 0x1f));
}
++$this->size;
}
/**
* Appends a number of bits (up to 32) to the array.
* @throws InvalidArgumentException if num bits is not between 0 and 32
*/
public function appendBits(int $value, int $numBits) : void
{
if ($numBits < 0 || $numBits > 32) {
throw new InvalidArgumentException('Num bits must be between 0 and 32');
}
$this->ensureCapacity($this->size + $numBits);
for ($numBitsLeft = $numBits; $numBitsLeft > 0; $numBitsLeft--) {
$this->appendBit((($value >> ($numBitsLeft - 1)) & 0x01) === 1);
}
}
/**
* Appends another bit array to this array.
*/
public function appendBitArray(self $other) : void
{
$otherSize = $other->getSize();
$this->ensureCapacity($this->size + $other->getSize());
for ($i = 0; $i < $otherSize; ++$i) {
$this->appendBit($other->get($i));
}
}
/**
* Makes an exclusive-or comparision on the current bit array.
*
* @throws InvalidArgumentException if sizes don't match
*/
public function xorBits(self $other) : void
{
$bitsLength = count($this->bits);
$otherBits = $other->getBitArray();
if ($bitsLength !== count($otherBits)) {
throw new InvalidArgumentException('Sizes don\'t match');
}
for ($i = 0; $i < $bitsLength; ++$i) {
$this->bits[$i] = $this->bits[$i] ^ $otherBits[$i];
}
}
/**
* Converts the bit array to a byte array.
*
* @return SplFixedArray<int>
*/
public function toBytes(int $bitOffset, int $numBytes) : SplFixedArray
{
$bytes = new SplFixedArray($numBytes);
for ($i = 0; $i < $numBytes; ++$i) {
$byte = 0;
for ($j = 0; $j < 8; ++$j) {
if ($this->get($bitOffset)) {
$byte |= 1 << (7 - $j);
}
++$bitOffset;
}
$bytes[$i] = $byte;
}
return $bytes;
}
/**
* Gets the internal bit array.
*
* @return SplFixedArray<int>
*/
public function getBitArray() : SplFixedArray
{
return $this->bits;
}
/**
* Reverses the array.
*/
public function reverse() : void
{
$newBits = new SplFixedArray(count($this->bits));
for ($i = 0; $i < $this->size; ++$i) {
if ($this->get($this->size - $i - 1)) {
$newBits[$i >> 5] = $newBits[$i >> 5] | (1 << ($i & 0x1f));
}
}
$this->bits = $newBits;
}
/**
* Returns a string representation of the bit array.
*/
public function __toString() : string
{
$result = '';
for ($i = 0; $i < $this->size; ++$i) {
if (0 === ($i & 0x07)) {
$result .= ' ';
}
$result .= $this->get($i) ? 'X' : '.';
}
return $result;
}
}
+307
View File
@@ -0,0 +1,307 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
use BaconQrCode\Exception\InvalidArgumentException;
use SplFixedArray;
/**
* Bit matrix.
*
* Represents a 2D matrix of bits. In function arguments below, and throughout
* the common module, x is the column position, and y is the row position. The
* ordering is always x, y. The origin is at the top-left.
*/
class BitMatrix
{
/**
* Width of the bit matrix.
*/
private int $width;
/**
* Height of the bit matrix.
*/
private ?int $height;
/**
* Size in bits of each individual row.
*/
private int $rowSize;
/**
* Bits representation.
*
* @var SplFixedArray<int>
*/
private SplFixedArray $bits;
/**
* @throws InvalidArgumentException if a dimension is smaller than zero
*/
public function __construct(int $width, ?int $height = null)
{
if (null === $height) {
$height = $width;
}
if ($width < 1 || $height < 1) {
throw new InvalidArgumentException('Both dimensions must be greater than zero');
}
$this->width = $width;
$this->height = $height;
$this->rowSize = ($width + 31) >> 5;
$this->bits = SplFixedArray::fromArray(array_fill(0, $this->rowSize * $height, 0));
}
/**
* Gets the requested bit, where true means black.
*/
public function get(int $x, int $y) : bool
{
$offset = $y * $this->rowSize + ($x >> 5);
return 0 !== (BitUtils::unsignedRightShift($this->bits[$offset], ($x & 0x1f)) & 1);
}
/**
* Sets the given bit to true.
*/
public function set(int $x, int $y) : void
{
$offset = $y * $this->rowSize + ($x >> 5);
$this->bits[$offset] = $this->bits[$offset] | (1 << ($x & 0x1f));
}
/**
* Flips the given bit.
*/
public function flip(int $x, int $y) : void
{
$offset = $y * $this->rowSize + ($x >> 5);
$this->bits[$offset] = $this->bits[$offset] ^ (1 << ($x & 0x1f));
}
/**
* Clears all bits (set to false).
*/
public function clear() : void
{
$max = count($this->bits);
for ($i = 0; $i < $max; ++$i) {
$this->bits[$i] = 0;
}
}
/**
* Sets a square region of the bit matrix to true.
*
* @throws InvalidArgumentException if left or top are negative
* @throws InvalidArgumentException if width or height are smaller than 1
* @throws InvalidArgumentException if region does not fit into the matix
*/
public function setRegion(int $left, int $top, int $width, int $height) : void
{
if ($top < 0 || $left < 0) {
throw new InvalidArgumentException('Left and top must be non-negative');
}
if ($height < 1 || $width < 1) {
throw new InvalidArgumentException('Width and height must be at least 1');
}
$right = $left + $width;
$bottom = $top + $height;
if ($bottom > $this->height || $right > $this->width) {
throw new InvalidArgumentException('The region must fit inside the matrix');
}
for ($y = $top; $y < $bottom; ++$y) {
$offset = $y * $this->rowSize;
for ($x = $left; $x < $right; ++$x) {
$index = $offset + ($x >> 5);
$this->bits[$index] = $this->bits[$index] | (1 << ($x & 0x1f));
}
}
}
/**
* A fast method to retrieve one row of data from the matrix as a BitArray.
*/
public function getRow(int $y, ?BitArray $row = null) : BitArray
{
if (null === $row || $row->getSize() < $this->width) {
$row = new BitArray($this->width);
}
$offset = $y * $this->rowSize;
for ($x = 0; $x < $this->rowSize; ++$x) {
$row->setBulk($x << 5, $this->bits[$offset + $x]);
}
return $row;
}
/**
* Sets a row of data from a BitArray.
*/
public function setRow(int $y, BitArray $row) : void
{
$bits = $row->getBitArray();
for ($i = 0; $i < $this->rowSize; ++$i) {
$this->bits[$y * $this->rowSize + $i] = $bits[$i];
}
}
/**
* This is useful in detecting the enclosing rectangle of a 'pure' barcode.
*
* @return int[]|null
*/
public function getEnclosingRectangle() : ?array
{
$left = $this->width;
$top = $this->height;
$right = -1;
$bottom = -1;
for ($y = 0; $y < $this->height; ++$y) {
for ($x32 = 0; $x32 < $this->rowSize; ++$x32) {
$bits = $this->bits[$y * $this->rowSize + $x32];
if (0 !== $bits) {
if ($y < $top) {
$top = $y;
}
if ($y > $bottom) {
$bottom = $y;
}
if ($x32 * 32 < $left) {
$bit = 0;
while (($bits << (31 - $bit)) === 0) {
$bit++;
}
if (($x32 * 32 + $bit) < $left) {
$left = $x32 * 32 + $bit;
}
}
}
if ($x32 * 32 + 31 > $right) {
$bit = 31;
while (0 === BitUtils::unsignedRightShift($bits, $bit)) {
--$bit;
}
if (($x32 * 32 + $bit) > $right) {
$right = $x32 * 32 + $bit;
}
}
}
}
$width = $right - $left;
$height = $bottom - $top;
if ($width < 0 || $height < 0) {
return null;
}
return [$left, $top, $width, $height];
}
/**
* Gets the most top left set bit.
*
* This is useful in detecting a corner of a 'pure' barcode.
*
* @return int[]|null
*/
public function getTopLeftOnBit() : ?array
{
$bitsOffset = 0;
while ($bitsOffset < count($this->bits) && 0 === $this->bits[$bitsOffset]) {
++$bitsOffset;
}
if (count($this->bits) === $bitsOffset) {
return null;
}
$x = intdiv($bitsOffset, $this->rowSize);
$y = ($bitsOffset % $this->rowSize) << 5;
$bits = $this->bits[$bitsOffset];
$bit = 0;
while (0 === ($bits << (31 - $bit))) {
++$bit;
}
$x += $bit;
return [$x, $y];
}
/**
* Gets the most bottom right set bit.
*
* This is useful in detecting a corner of a 'pure' barcode.
*
* @return int[]|null
*/
public function getBottomRightOnBit() : ?array
{
$bitsOffset = count($this->bits) - 1;
while ($bitsOffset >= 0 && 0 === $this->bits[$bitsOffset]) {
--$bitsOffset;
}
if ($bitsOffset < 0) {
return null;
}
$x = intdiv($bitsOffset, $this->rowSize);
$y = ($bitsOffset % $this->rowSize) << 5;
$bits = $this->bits[$bitsOffset];
$bit = 0;
while (0 === BitUtils::unsignedRightShift($bits, $bit)) {
--$bit;
}
$x += $bit;
return [$x, $y];
}
/**
* Gets the width of the matrix,
*/
public function getWidth() : int
{
return $this->width;
}
/**
* Gets the height of the matrix.
*/
public function getHeight() : int
{
return $this->height;
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
/**
* General bit utilities.
*
* All utility methods are based on 32-bit integers and also work on 64-bit
* systems.
*/
final class BitUtils
{
private function __construct()
{
}
/**
* Performs an unsigned right shift.
*
* This is the same as the unsigned right shift operator ">>>" in other
* languages.
*/
public static function unsignedRightShift(int $a, int $b) : int
{
return (
$a >= 0
? $a >> $b
: (($a & 0x7fffffff) >> $b) | (0x40000000 >> ($b - 1))
);
}
/**
* Gets the number of trailing zeros.
*/
public static function numberOfTrailingZeros(int $i) : int
{
$lastPos = strrpos(str_pad(decbin($i), 32, '0', STR_PAD_LEFT), '1');
return $lastPos === false ? 32 : 31 - $lastPos;
}
}
@@ -0,0 +1,177 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
use BaconQrCode\Exception\InvalidArgumentException;
use DASPRiD\Enum\AbstractEnum;
/**
* Encapsulates a Character Set ECI, according to "Extended Channel Interpretations" 5.3.1.1 of ISO 18004.
*
* @method static self CP437()
* @method static self ISO8859_1()
* @method static self ISO8859_2()
* @method static self ISO8859_3()
* @method static self ISO8859_4()
* @method static self ISO8859_5()
* @method static self ISO8859_6()
* @method static self ISO8859_7()
* @method static self ISO8859_8()
* @method static self ISO8859_9()
* @method static self ISO8859_10()
* @method static self ISO8859_11()
* @method static self ISO8859_12()
* @method static self ISO8859_13()
* @method static self ISO8859_14()
* @method static self ISO8859_15()
* @method static self ISO8859_16()
* @method static self SJIS()
* @method static self CP1250()
* @method static self CP1251()
* @method static self CP1252()
* @method static self CP1256()
* @method static self UNICODE_BIG_UNMARKED()
* @method static self UTF8()
* @method static self ASCII()
* @method static self BIG5()
* @method static self GB18030()
* @method static self EUC_KR()
*/
final class CharacterSetEci extends AbstractEnum
{
protected const CP437 = [[0, 2]];
protected const ISO8859_1 = [[1, 3], 'ISO-8859-1'];
protected const ISO8859_2 = [[4], 'ISO-8859-2'];
protected const ISO8859_3 = [[5], 'ISO-8859-3'];
protected const ISO8859_4 = [[6], 'ISO-8859-4'];
protected const ISO8859_5 = [[7], 'ISO-8859-5'];
protected const ISO8859_6 = [[8], 'ISO-8859-6'];
protected const ISO8859_7 = [[9], 'ISO-8859-7'];
protected const ISO8859_8 = [[10], 'ISO-8859-8'];
protected const ISO8859_9 = [[11], 'ISO-8859-9'];
protected const ISO8859_10 = [[12], 'ISO-8859-10'];
protected const ISO8859_11 = [[13], 'ISO-8859-11'];
protected const ISO8859_12 = [[14], 'ISO-8859-12'];
protected const ISO8859_13 = [[15], 'ISO-8859-13'];
protected const ISO8859_14 = [[16], 'ISO-8859-14'];
protected const ISO8859_15 = [[17], 'ISO-8859-15'];
protected const ISO8859_16 = [[18], 'ISO-8859-16'];
protected const SJIS = [[20], 'Shift_JIS'];
protected const CP1250 = [[21], 'windows-1250'];
protected const CP1251 = [[22], 'windows-1251'];
protected const CP1252 = [[23], 'windows-1252'];
protected const CP1256 = [[24], 'windows-1256'];
protected const UNICODE_BIG_UNMARKED = [[25], 'UTF-16BE', 'UnicodeBig'];
protected const UTF8 = [[26], 'UTF-8'];
protected const ASCII = [[27, 170], 'US-ASCII'];
protected const BIG5 = [[28]];
protected const GB18030 = [[29], 'GB2312', 'EUC_CN', 'GBK'];
protected const EUC_KR = [[30], 'EUC-KR'];
/**
* @var string[]
*/
private array $otherEncodingNames;
/**
* @var array<int, self>|null
*/
private static ?array $valueToEci;
/**
* @var array<string, self>|null
*/
private static ?array $nameToEci = null;
/**
* @param int[] $values
*/
public function __construct(private readonly array $values, string ...$otherEncodingNames)
{
$this->otherEncodingNames = $otherEncodingNames;
}
/**
* Returns the primary value.
*/
public function getValue() : int
{
return $this->values[0];
}
/**
* Gets character set ECI by value.
*
* Returns the representing ECI of a given value, or null if it is legal but unsupported.
*
* @throws InvalidArgumentException if value is not between 0 and 900
*/
public static function getCharacterSetEciByValue(int $value) : ?self
{
if ($value < 0 || $value >= 900) {
throw new InvalidArgumentException('Value must be between 0 and 900');
}
$valueToEci = self::valueToEci();
if (! array_key_exists($value, $valueToEci)) {
return null;
}
return $valueToEci[$value];
}
/**
* Returns character set ECI by name.
*
* Returns the representing ECI of a given name, or null if it is legal but unsupported
*/
public static function getCharacterSetEciByName(string $name) : ?self
{
$nameToEci = self::nameToEci();
$name = strtolower($name);
if (! array_key_exists($name, $nameToEci)) {
return null;
}
return $nameToEci[$name];
}
private static function valueToEci() : array
{
if (null !== self::$valueToEci) {
return self::$valueToEci;
}
self::$valueToEci = [];
foreach (self::values() as $eci) {
foreach ($eci->values as $value) {
self::$valueToEci[$value] = $eci;
}
}
return self::$valueToEci;
}
private static function nameToEci() : array
{
if (null !== self::$nameToEci) {
return self::$nameToEci;
}
self::$nameToEci = [];
foreach (self::values() as $eci) {
self::$nameToEci[strtolower($eci->name())] = $eci;
foreach ($eci->otherEncodingNames as $name) {
self::$nameToEci[strtolower($name)] = $eci;
}
}
return self::$nameToEci;
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
/**
* Encapsulates the parameters for one error-correction block in one symbol version.
*
* This includes the number of data codewords, and the number of times a block with these parameters is used
* consecutively in the QR code version's format.
*/
final class EcBlock
{
public function __construct(private readonly int $count, private readonly int $dataCodewords)
{
}
/**
* Returns how many times the block is used.
*/
public function getCount() : int
{
return $this->count;
}
/**
* Returns the number of data codewords.
*/
public function getDataCodewords() : int
{
return $this->dataCodewords;
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
/**
* Encapsulates a set of error-correction blocks in one symbol version.
*
* Most versions will use blocks of differing sizes within one version, so, this encapsulates the parameters for each
* set of blocks. It also holds the number of error-correction codewords per block since it will be the same across all
* blocks within one version.
*/
final class EcBlocks
{
/**
* List of EC blocks.
*
* @var EcBlock[]
*/
private array $ecBlocks;
public function __construct(private readonly int $ecCodewordsPerBlock, EcBlock ...$ecBlocks)
{
$this->ecBlocks = $ecBlocks;
}
/**
* Returns the number of EC codewords per block.
*/
public function getEcCodewordsPerBlock() : int
{
return $this->ecCodewordsPerBlock;
}
/**
* Returns the total number of EC block appearances.
*/
public function getNumBlocks() : int
{
$total = 0;
foreach ($this->ecBlocks as $ecBlock) {
$total += $ecBlock->getCount();
}
return $total;
}
/**
* Returns the total count of EC codewords.
*/
public function getTotalEcCodewords() : int
{
return $this->ecCodewordsPerBlock * $this->getNumBlocks();
}
/**
* Returns the EC blocks included in this collection.
*
* @return EcBlock[]
*/
public function getEcBlocks() : array
{
return $this->ecBlocks;
}
}
@@ -0,0 +1,57 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
use BaconQrCode\Exception\OutOfBoundsException;
use DASPRiD\Enum\AbstractEnum;
/**
* Enum representing the four error correction levels.
*
* @method static self L() ~7% correction
* @method static self M() ~15% correction
* @method static self Q() ~25% correction
* @method static self H() ~30% correction
*/
final class ErrorCorrectionLevel extends AbstractEnum
{
protected const L = [0x01];
protected const M = [0x00];
protected const Q = [0x03];
protected const H = [0x02];
protected function __construct(private readonly int $bits)
{
}
/**
* @throws OutOfBoundsException if number of bits is invalid
*/
public static function forBits(int $bits) : self
{
switch ($bits) {
case 0:
return self::M();
case 1:
return self::L();
case 2:
return self::H();
case 3:
return self::Q();
}
throw new OutOfBoundsException('Invalid number of bits');
}
/**
* Returns the two bits used to encode this error correction level.
*/
public function getBits() : int
{
return $this->bits;
}
}
@@ -0,0 +1,196 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Common;
/**
* Encapsulates a QR Code's format information, including the data mask used and error correction level.
*/
class FormatInformation
{
/**
* Mask for format information.
*/
private const FORMAT_INFO_MASK_QR = 0x5412;
/**
* Lookup table for decoding format information.
*
* See ISO 18004:2006, Annex C, Table C.1
*/
private const FORMAT_INFO_DECODE_LOOKUP = [
[0x5412, 0x00],
[0x5125, 0x01],
[0x5e7c, 0x02],
[0x5b4b, 0x03],
[0x45f9, 0x04],
[0x40ce, 0x05],
[0x4f97, 0x06],
[0x4aa0, 0x07],
[0x77c4, 0x08],
[0x72f3, 0x09],
[0x7daa, 0x0a],
[0x789d, 0x0b],
[0x662f, 0x0c],
[0x6318, 0x0d],
[0x6c41, 0x0e],
[0x6976, 0x0f],
[0x1689, 0x10],
[0x13be, 0x11],
[0x1ce7, 0x12],
[0x19d0, 0x13],
[0x0762, 0x14],
[0x0255, 0x15],
[0x0d0c, 0x16],
[0x083b, 0x17],
[0x355f, 0x18],
[0x3068, 0x19],
[0x3f31, 0x1a],
[0x3a06, 0x1b],
[0x24b4, 0x1c],
[0x2183, 0x1d],
[0x2eda, 0x1e],
[0x2bed, 0x1f],
];
/**
* Offset i holds the number of 1 bits in the binary representation of i.
*
* @var int[]
*/
private const BITS_SET_IN_HALF_BYTE = [0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4];
/**
* Error correction level.
*/
private ErrorCorrectionLevel $ecLevel;
private int $dataMask;
protected function __construct(int $formatInfo)
{
$this->ecLevel = ErrorCorrectionLevel::forBits(($formatInfo >> 3) & 0x3);
$this->dataMask = $formatInfo & 0x7;
}
/**
* Checks how many bits are different between two integers.
*/
public static function numBitsDiffering(int $a, int $b) : int
{
$a ^= $b;
return (
self::BITS_SET_IN_HALF_BYTE[$a & 0xf]
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 4) & 0xf)]
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 8) & 0xf)]
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 12) & 0xf)]
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 16) & 0xf)]
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 20) & 0xf)]
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 24) & 0xf)]
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 28) & 0xf)]
);
}
/**
* Decodes format information.
*/
public static function decodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2) : ?self
{
$formatInfo = self::doDecodeFormatInformation($maskedFormatInfo1, $maskedFormatInfo2);
if (null !== $formatInfo) {
return $formatInfo;
}
// Should return null, but, some QR codes apparently do not mask this info. Try again by actually masking the
// pattern first.
return self::doDecodeFormatInformation(
$maskedFormatInfo1 ^ self::FORMAT_INFO_MASK_QR,
$maskedFormatInfo2 ^ self::FORMAT_INFO_MASK_QR
);
}
/**
* Internal method for decoding format information.
*/
private static function doDecodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2) : ?self
{
$bestDifference = PHP_INT_MAX;
$bestFormatInfo = 0;
foreach (self::FORMAT_INFO_DECODE_LOOKUP as $decodeInfo) {
$targetInfo = $decodeInfo[0];
if ($targetInfo === $maskedFormatInfo1 || $targetInfo === $maskedFormatInfo2) {
// Found an exact match
return new self($decodeInfo[1]);
}
$bitsDifference = self::numBitsDiffering($maskedFormatInfo1, $targetInfo);
if ($bitsDifference < $bestDifference) {
$bestFormatInfo = $decodeInfo[1];
$bestDifference = $bitsDifference;
}
if ($maskedFormatInfo1 !== $maskedFormatInfo2) {
// Also try the other option
$bitsDifference = self::numBitsDiffering($maskedFormatInfo2, $targetInfo);
if ($bitsDifference < $bestDifference) {
$bestFormatInfo = $decodeInfo[1];
$bestDifference = $bitsDifference;
}
}
}
// Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits differing means we found a match.
if ($bestDifference <= 3) {
return new self($bestFormatInfo);
}
return null;
}
/**
* Returns the error correction level.
*/
public function getErrorCorrectionLevel() : ErrorCorrectionLevel
{
return $this->ecLevel;
}
/**
* Returns the data mask.
*/
public function getDataMask() : int
{
return $this->dataMask;
}
/**
* Hashes the code of the EC level.
*/
public function hashCode() : int
{
return ($this->ecLevel->getBits() << 3) | $this->dataMask;
}
/**
* Verifies if this instance equals another one.
*/
public function equals(self $other) : bool
{
return (
$this->ecLevel === $other->ecLevel
&& $this->dataMask === $other->dataMask
);
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
use DASPRiD\Enum\AbstractEnum;
/**
* Enum representing various modes in which data can be encoded to bits.
*
* @method static self TERMINATOR()
* @method static self NUMERIC()
* @method static self ALPHANUMERIC()
* @method static self STRUCTURED_APPEND()
* @method static self BYTE()
* @method static self ECI()
* @method static self KANJI()
* @method static self FNC1_FIRST_POSITION()
* @method static self FNC1_SECOND_POSITION()
* @method static self HANZI()
*/
final class Mode extends AbstractEnum
{
protected const TERMINATOR = [[0, 0, 0], 0x00];
protected const NUMERIC = [[10, 12, 14], 0x01];
protected const ALPHANUMERIC = [[9, 11, 13], 0x02];
protected const STRUCTURED_APPEND = [[0, 0, 0], 0x03];
protected const BYTE = [[8, 16, 16], 0x04];
protected const ECI = [[0, 0, 0], 0x07];
protected const KANJI = [[8, 10, 12], 0x08];
protected const FNC1_FIRST_POSITION = [[0, 0, 0], 0x05];
protected const FNC1_SECOND_POSITION = [[0, 0, 0], 0x09];
protected const HANZI = [[8, 10, 12], 0x0d];
/**
* @param int[] $characterCountBitsForVersions
*/
protected function __construct(
private readonly array $characterCountBitsForVersions,
private readonly int $bits
) {
}
/**
* Returns the number of bits used in a specific QR code version.
*/
public function getCharacterCountBits(Version $version) : int
{
$number = $version->getVersionNumber();
if ($number <= 9) {
$offset = 0;
} elseif ($number <= 26) {
$offset = 1;
} else {
$offset = 2;
}
return $this->characterCountBitsForVersions[$offset];
}
/**
* Returns the four bits used to encode this mode.
*/
public function getBits() : int
{
return $this->bits;
}
}
@@ -0,0 +1,454 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
use BaconQrCode\Exception\InvalidArgumentException;
use BaconQrCode\Exception\RuntimeException;
use SplFixedArray;
/**
* Reed-Solomon codec for 8-bit characters.
*
* Based on libfec by Phil Karn, KA9Q.
*/
final class ReedSolomonCodec
{
/**
* Symbol size in bits.
*/
private int $symbolSize;
/**
* Block size in symbols.
*/
private int $blockSize;
/**
* First root of RS code generator polynomial, index form.
*/
private int $firstRoot;
/**
* Primitive element to generate polynomial roots, index form.
*/
private int $primitive;
/**
* Prim-th root of 1, index form.
*/
private int $iPrimitive;
/**
* RS code generator polynomial degree (number of roots).
*/
private int $numRoots;
/**
* Padding bytes at front of shortened block.
*/
private int $padding;
/**
* Log lookup table.
*
* @var SplFixedArray
*/
private SplFixedArray $alphaTo;
/**
* Anti-Log lookup table.
*
* @var SplFixedArray
*/
private SplFixedArray $indexOf;
/**
* Generator polynomial.
*
* @var SplFixedArray
*/
private SplFixedArray $generatorPoly;
/**
* @throws InvalidArgumentException if symbol size ist not between 0 and 8
* @throws InvalidArgumentException if first root is invalid
* @throws InvalidArgumentException if num roots is invalid
* @throws InvalidArgumentException if padding is invalid
* @throws RuntimeException if field generator polynomial is not primitive
*/
public function __construct(
int $symbolSize,
int $gfPoly,
int $firstRoot,
int $primitive,
int $numRoots,
int $padding
) {
if ($symbolSize < 0 || $symbolSize > 8) {
throw new InvalidArgumentException('Symbol size must be between 0 and 8');
}
if ($firstRoot < 0 || $firstRoot >= (1 << $symbolSize)) {
throw new InvalidArgumentException('First root must be between 0 and ' . (1 << $symbolSize));
}
if ($numRoots < 0 || $numRoots >= (1 << $symbolSize)) {
throw new InvalidArgumentException('Num roots must be between 0 and ' . (1 << $symbolSize));
}
if ($padding < 0 || $padding >= ((1 << $symbolSize) - 1 - $numRoots)) {
throw new InvalidArgumentException(
'Padding must be between 0 and ' . ((1 << $symbolSize) - 1 - $numRoots)
);
}
$this->symbolSize = $symbolSize;
$this->blockSize = (1 << $symbolSize) - 1;
$this->padding = $padding;
$this->alphaTo = SplFixedArray::fromArray(array_fill(0, $this->blockSize + 1, 0), false);
$this->indexOf = SplFixedArray::fromArray(array_fill(0, $this->blockSize + 1, 0), false);
// Generate galous field lookup table
$this->indexOf[0] = $this->blockSize;
$this->alphaTo[$this->blockSize] = 0;
$sr = 1;
for ($i = 0; $i < $this->blockSize; ++$i) {
$this->indexOf[$sr] = $i;
$this->alphaTo[$i] = $sr;
$sr <<= 1;
if ($sr & (1 << $symbolSize)) {
$sr ^= $gfPoly;
}
$sr &= $this->blockSize;
}
if (1 !== $sr) {
throw new RuntimeException('Field generator polynomial is not primitive');
}
// Form RS code generator polynomial from its roots
$this->generatorPoly = SplFixedArray::fromArray(array_fill(0, $numRoots + 1, 0), false);
$this->firstRoot = $firstRoot;
$this->primitive = $primitive;
$this->numRoots = $numRoots;
// Find prim-th root of 1, used in decoding
for ($iPrimitive = 1; ($iPrimitive % $primitive) !== 0; $iPrimitive += $this->blockSize) {
}
$this->iPrimitive = intdiv($iPrimitive, $primitive);
$this->generatorPoly[0] = 1;
for ($i = 0, $root = $firstRoot * $primitive; $i < $numRoots; ++$i, $root += $primitive) {
$this->generatorPoly[$i + 1] = 1;
for ($j = $i; $j > 0; $j--) {
if ($this->generatorPoly[$j] !== 0) {
$this->generatorPoly[$j] = $this->generatorPoly[$j - 1] ^ $this->alphaTo[
$this->modNn($this->indexOf[$this->generatorPoly[$j]] + $root)
];
} else {
$this->generatorPoly[$j] = $this->generatorPoly[$j - 1];
}
}
$this->generatorPoly[$j] = $this->alphaTo[$this->modNn($this->indexOf[$this->generatorPoly[0]] + $root)];
}
// Convert generator poly to index form for quicker encoding
for ($i = 0; $i <= $numRoots; ++$i) {
$this->generatorPoly[$i] = $this->indexOf[$this->generatorPoly[$i]];
}
}
/**
* Encodes data and writes result back into parity array.
*/
public function encode(SplFixedArray $data, SplFixedArray $parity) : void
{
for ($i = 0; $i < $this->numRoots; ++$i) {
$parity[$i] = 0;
}
$iterations = $this->blockSize - $this->numRoots - $this->padding;
for ($i = 0; $i < $iterations; ++$i) {
$feedback = $this->indexOf[$data[$i] ^ $parity[0]];
if ($feedback !== $this->blockSize) {
// Feedback term is non-zero
$feedback = $this->modNn($this->blockSize - $this->generatorPoly[$this->numRoots] + $feedback);
for ($j = 1; $j < $this->numRoots; ++$j) {
$parity[$j] = $parity[$j] ^ $this->alphaTo[
$this->modNn($feedback + $this->generatorPoly[$this->numRoots - $j])
];
}
}
for ($j = 0; $j < $this->numRoots - 1; ++$j) {
$parity[$j] = $parity[$j + 1];
}
if ($feedback !== $this->blockSize) {
$parity[$this->numRoots - 1] = $this->alphaTo[$this->modNn($feedback + $this->generatorPoly[0])];
} else {
$parity[$this->numRoots - 1] = 0;
}
}
}
/**
* Decodes received data.
*/
public function decode(SplFixedArray $data, ?SplFixedArray $erasures = null) : ?int
{
// This speeds up the initialization a bit.
$numRootsPlusOne = SplFixedArray::fromArray(array_fill(0, $this->numRoots + 1, 0), false);
$numRoots = SplFixedArray::fromArray(array_fill(0, $this->numRoots, 0), false);
$lambda = clone $numRootsPlusOne;
$b = clone $numRootsPlusOne;
$t = clone $numRootsPlusOne;
$omega = clone $numRootsPlusOne;
$root = clone $numRoots;
$loc = clone $numRoots;
$numErasures = (null !== $erasures ? count($erasures) : 0);
// Form the Syndromes; i.e., evaluate data(x) at roots of g(x)
$syndromes = SplFixedArray::fromArray(array_fill(0, $this->numRoots, $data[0]), false);
for ($i = 1; $i < $this->blockSize - $this->padding; ++$i) {
for ($j = 0; $j < $this->numRoots; ++$j) {
if ($syndromes[$j] === 0) {
$syndromes[$j] = $data[$i];
} else {
$syndromes[$j] = $data[$i] ^ $this->alphaTo[
$this->modNn($this->indexOf[$syndromes[$j]] + ($this->firstRoot + $j) * $this->primitive)
];
}
}
}
// Convert syndromes to index form, checking for nonzero conditions
$syndromeError = 0;
for ($i = 0; $i < $this->numRoots; ++$i) {
$syndromeError |= $syndromes[$i];
$syndromes[$i] = $this->indexOf[$syndromes[$i]];
}
if (! $syndromeError) {
// If syndrome is zero, data[] is a codeword and there are no errors to correct, so return data[]
// unmodified.
return 0;
}
$lambda[0] = 1;
if ($numErasures > 0) {
// Init lambda to be the erasure locator polynomial
$lambda[1] = $this->alphaTo[$this->modNn($this->primitive * ($this->blockSize - 1 - $erasures[0]))];
for ($i = 1; $i < $numErasures; ++$i) {
$u = $this->modNn($this->primitive * ($this->blockSize - 1 - $erasures[$i]));
for ($j = $i + 1; $j > 0; --$j) {
$tmp = $this->indexOf[$lambda[$j - 1]];
if ($tmp !== $this->blockSize) {
$lambda[$j] = $lambda[$j] ^ $this->alphaTo[$this->modNn($u + $tmp)];
}
}
}
}
for ($i = 0; $i <= $this->numRoots; ++$i) {
$b[$i] = $this->indexOf[$lambda[$i]];
}
// Begin Berlekamp-Massey algorithm to determine error+erasure locator polynomial
$r = $numErasures;
$el = $numErasures;
while (++$r <= $this->numRoots) {
// Compute discrepancy at the r-th step in poly form
$discrepancyR = 0;
for ($i = 0; $i < $r; ++$i) {
if ($lambda[$i] !== 0 && $syndromes[$r - $i - 1] !== $this->blockSize) {
$discrepancyR ^= $this->alphaTo[
$this->modNn($this->indexOf[$lambda[$i]] + $syndromes[$r - $i - 1])
];
}
}
$discrepancyR = $this->indexOf[$discrepancyR];
if ($discrepancyR === $this->blockSize) {
$tmp = $b->toArray();
array_unshift($tmp, $this->blockSize);
array_pop($tmp);
$b = SplFixedArray::fromArray($tmp, false);
continue;
}
$t[0] = $lambda[0];
for ($i = 0; $i < $this->numRoots; ++$i) {
if ($b[$i] !== $this->blockSize) {
$t[$i + 1] = $lambda[$i + 1] ^ $this->alphaTo[$this->modNn($discrepancyR + $b[$i])];
} else {
$t[$i + 1] = $lambda[$i + 1];
}
}
if (2 * $el <= $r + $numErasures - 1) {
$el = $r + $numErasures - $el;
for ($i = 0; $i <= $this->numRoots; ++$i) {
$b[$i] = (
$lambda[$i] === 0
? $this->blockSize
: $this->modNn($this->indexOf[$lambda[$i]] - $discrepancyR + $this->blockSize)
);
}
} else {
$tmp = $b->toArray();
array_unshift($tmp, $this->blockSize);
array_pop($tmp);
$b = SplFixedArray::fromArray($tmp, false);
}
$lambda = clone $t;
}
// Convert lambda to index form and compute deg(lambda(x))
$degLambda = 0;
for ($i = 0; $i <= $this->numRoots; ++$i) {
$lambda[$i] = $this->indexOf[$lambda[$i]];
if ($lambda[$i] !== $this->blockSize) {
$degLambda = $i;
}
}
// Find roots of the error+erasure locator polynomial by Chien search.
$reg = clone $lambda;
$reg[0] = 0;
$count = 0;
$i = 1;
for ($k = $this->iPrimitive - 1; $i <= $this->blockSize; ++$i, $k = $this->modNn($k + $this->iPrimitive)) {
$q = 1;
for ($j = $degLambda; $j > 0; $j--) {
if ($reg[$j] !== $this->blockSize) {
$reg[$j] = $this->modNn($reg[$j] + $j);
$q ^= $this->alphaTo[$reg[$j]];
}
}
if ($q !== 0) {
// Not a root
continue;
}
// Store root (index-form) and error location number
$root[$count] = $i;
$loc[$count] = $k;
if (++$count === $degLambda) {
break;
}
}
if ($degLambda !== $count) {
// deg(lambda) unequal to number of roots: uncorrectable error detected
return null;
}
// Compute err+eras evaluate poly omega(x) = s(x)*lambda(x) (modulo x**numRoots). In index form. Also find
// deg(omega).
$degOmega = $degLambda - 1;
for ($i = 0; $i <= $degOmega; ++$i) {
$tmp = 0;
for ($j = $i; $j >= 0; --$j) {
if ($syndromes[$i - $j] !== $this->blockSize && $lambda[$j] !== $this->blockSize) {
$tmp ^= $this->alphaTo[$this->modNn($syndromes[$i - $j] + $lambda[$j])];
}
}
$omega[$i] = $this->indexOf[$tmp];
}
// Compute error values in poly-form. num1 = omega(inv(X(l))), num2 = inv(X(l))**(firstRoot-1) and
// den = lambda_pr(inv(X(l))) all in poly form.
for ($j = $count - 1; $j >= 0; --$j) {
$num1 = 0;
for ($i = $degOmega; $i >= 0; $i--) {
if ($omega[$i] !== $this->blockSize) {
$num1 ^= $this->alphaTo[$this->modNn($omega[$i] + $i * $root[$j])];
}
}
$num2 = $this->alphaTo[$this->modNn($root[$j] * ($this->firstRoot - 1) + $this->blockSize)];
$den = 0;
// lambda[i+1] for i even is the formal derivativelambda_pr of lambda[i]
for ($i = min($degLambda, $this->numRoots - 1) & ~1; $i >= 0; $i -= 2) {
if ($lambda[$i + 1] !== $this->blockSize) {
$den ^= $this->alphaTo[$this->modNn($lambda[$i + 1] + $i * $root[$j])];
}
}
// Apply error to data
if ($num1 !== 0 && $loc[$j] >= $this->padding) {
$data[$loc[$j] - $this->padding] = $data[$loc[$j] - $this->padding] ^ (
$this->alphaTo[
$this->modNn(
$this->indexOf[$num1] + $this->indexOf[$num2] + $this->blockSize - $this->indexOf[$den]
)
]
);
}
}
if (null !== $erasures) {
if (count($erasures) < $count) {
$erasures->setSize($count);
}
for ($i = 0; $i < $count; $i++) {
$erasures[$i] = $loc[$i];
}
}
return $count;
}
/**
* Computes $x % GF_SIZE, where GF_SIZE is 2**GF_BITS - 1, without a slow divide.
*/
private function modNn(int $x) : int
{
while ($x >= $this->blockSize) {
$x -= $this->blockSize;
$x = ($x >> $this->symbolSize) + ($x & $this->blockSize);
}
return $x;
}
}
+592
View File
@@ -0,0 +1,592 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
use BaconQrCode\Exception\InvalidArgumentException;
use SplFixedArray;
/**
* Version representation.
*/
final class Version
{
private const VERSION_DECODE_INFO = [
0x07c94,
0x085bc,
0x09a99,
0x0a4d3,
0x0bbf6,
0x0c762,
0x0d847,
0x0e60d,
0x0f928,
0x10b78,
0x1145d,
0x12a17,
0x13532,
0x149a6,
0x15683,
0x168c9,
0x177ec,
0x18ec4,
0x191e1,
0x1afab,
0x1b08e,
0x1cc1a,
0x1d33f,
0x1ed75,
0x1f250,
0x209d5,
0x216f0,
0x228ba,
0x2379f,
0x24b0b,
0x2542e,
0x26a64,
0x27541,
0x28c69,
];
/**
* Version number of this version.
*/
private int $versionNumber;
/**
* Alignment pattern centers.
*
* @var SplFixedArray|array
*/
private SplFixedArray|array $alignmentPatternCenters;
/**
* Error correction blocks.
*
* @var EcBlocks[]
*/
private array $ecBlocks;
/**
* Total number of codewords.
*/
private null|int|float $totalCodewords;
/**
* Cached version instances.
*
* @var array<int, self>|null
*/
private static ?array $versions = null;
/**
* @param int[] $alignmentPatternCenters
*/
private function __construct(
int $versionNumber,
array $alignmentPatternCenters,
EcBlocks ...$ecBlocks
) {
$this->versionNumber = $versionNumber;
$this->alignmentPatternCenters = $alignmentPatternCenters;
$this->ecBlocks = $ecBlocks;
$totalCodewords = 0;
$ecCodewords = $ecBlocks[0]->getEcCodewordsPerBlock();
foreach ($ecBlocks[0]->getEcBlocks() as $ecBlock) {
$totalCodewords += $ecBlock->getCount() * ($ecBlock->getDataCodewords() + $ecCodewords);
}
$this->totalCodewords = $totalCodewords;
}
/**
* Returns the version number.
*/
public function getVersionNumber() : int
{
return $this->versionNumber;
}
/**
* Returns the alignment pattern centers.
*
* @return int[]
*/
public function getAlignmentPatternCenters() : array
{
return $this->alignmentPatternCenters;
}
/**
* Returns the total number of codewords.
*/
public function getTotalCodewords() : int
{
return $this->totalCodewords;
}
/**
* Calculates the dimension for the current version.
*/
public function getDimensionForVersion() : int
{
return 17 + 4 * $this->versionNumber;
}
/**
* Returns the number of EC blocks for a specific EC level.
*/
public function getEcBlocksForLevel(ErrorCorrectionLevel $ecLevel) : EcBlocks
{
return $this->ecBlocks[$ecLevel->ordinal()];
}
/**
* Gets a provisional version number for a specific dimension.
*
* @throws InvalidArgumentException if dimension is not 1 mod 4
*/
public static function getProvisionalVersionForDimension(int $dimension) : self
{
if (1 !== $dimension % 4) {
throw new InvalidArgumentException('Dimension is not 1 mod 4');
}
return self::getVersionForNumber(intdiv($dimension - 17, 4));
}
/**
* Gets a version instance for a specific version number.
*
* @throws InvalidArgumentException if version number is out of range
*/
public static function getVersionForNumber(int $versionNumber) : self
{
if ($versionNumber < 1 || $versionNumber > 40) {
throw new InvalidArgumentException('Version number must be between 1 and 40');
}
return self::versions()[$versionNumber - 1];
}
/**
* Decodes version information from an integer and returns the version.
*/
public static function decodeVersionInformation(int $versionBits) : ?self
{
$bestDifference = PHP_INT_MAX;
$bestVersion = 0;
foreach (self::VERSION_DECODE_INFO as $i => $targetVersion) {
if ($targetVersion === $versionBits) {
return self::getVersionForNumber($i + 7);
}
$bitsDifference = FormatInformation::numBitsDiffering($versionBits, $targetVersion);
if ($bitsDifference < $bestDifference) {
$bestVersion = $i + 7;
$bestDifference = $bitsDifference;
}
}
if ($bestDifference <= 3) {
return self::getVersionForNumber($bestVersion);
}
return null;
}
/**
* Builds the function pattern for the current version.
*/
public function buildFunctionPattern() : BitMatrix
{
$dimension = $this->getDimensionForVersion();
$bitMatrix = new BitMatrix($dimension);
// Top left finder pattern + separator + format
$bitMatrix->setRegion(0, 0, 9, 9);
// Top right finder pattern + separator + format
$bitMatrix->setRegion($dimension - 8, 0, 8, 9);
// Bottom left finder pattern + separator + format
$bitMatrix->setRegion(0, $dimension - 8, 9, 8);
// Alignment patterns
$max = count($this->alignmentPatternCenters);
for ($x = 0; $x < $max; ++$x) {
$i = $this->alignmentPatternCenters[$x] - 2;
for ($y = 0; $y < $max; ++$y) {
if (($x === 0 && ($y === 0 || $y === $max - 1)) || ($x === $max - 1 && $y === 0)) {
// No alignment patterns near the three finder paterns
continue;
}
$bitMatrix->setRegion($this->alignmentPatternCenters[$y] - 2, $i, 5, 5);
}
}
// Vertical timing pattern
$bitMatrix->setRegion(6, 9, 1, $dimension - 17);
// Horizontal timing pattern
$bitMatrix->setRegion(9, 6, $dimension - 17, 1);
if ($this->versionNumber > 6) {
// Version info, top right
$bitMatrix->setRegion($dimension - 11, 0, 3, 6);
// Version info, bottom left
$bitMatrix->setRegion(0, $dimension - 11, 6, 3);
}
return $bitMatrix;
}
/**
* Returns a string representation for the version.
*/
public function __toString() : string
{
return (string) $this->versionNumber;
}
/**
* Build and cache a specific version.
*
* See ISO 18004:2006 6.5.1 Table 9.
*
* @return array<int, self>
*/
private static function versions() : array
{
if (null !== self::$versions) {
return self::$versions;
}
return self::$versions = [
new self(
1,
[],
new EcBlocks(7, new EcBlock(1, 19)),
new EcBlocks(10, new EcBlock(1, 16)),
new EcBlocks(13, new EcBlock(1, 13)),
new EcBlocks(17, new EcBlock(1, 9))
),
new self(
2,
[6, 18],
new EcBlocks(10, new EcBlock(1, 34)),
new EcBlocks(16, new EcBlock(1, 28)),
new EcBlocks(22, new EcBlock(1, 22)),
new EcBlocks(28, new EcBlock(1, 16))
),
new self(
3,
[6, 22],
new EcBlocks(15, new EcBlock(1, 55)),
new EcBlocks(26, new EcBlock(1, 44)),
new EcBlocks(18, new EcBlock(2, 17)),
new EcBlocks(22, new EcBlock(2, 13))
),
new self(
4,
[6, 26],
new EcBlocks(20, new EcBlock(1, 80)),
new EcBlocks(18, new EcBlock(2, 32)),
new EcBlocks(26, new EcBlock(2, 24)),
new EcBlocks(16, new EcBlock(4, 9))
),
new self(
5,
[6, 30],
new EcBlocks(26, new EcBlock(1, 108)),
new EcBlocks(24, new EcBlock(2, 43)),
new EcBlocks(18, new EcBlock(2, 15), new EcBlock(2, 16)),
new EcBlocks(22, new EcBlock(2, 11), new EcBlock(2, 12))
),
new self(
6,
[6, 34],
new EcBlocks(18, new EcBlock(2, 68)),
new EcBlocks(16, new EcBlock(4, 27)),
new EcBlocks(24, new EcBlock(4, 19)),
new EcBlocks(28, new EcBlock(4, 15))
),
new self(
7,
[6, 22, 38],
new EcBlocks(20, new EcBlock(2, 78)),
new EcBlocks(18, new EcBlock(4, 31)),
new EcBlocks(18, new EcBlock(2, 14), new EcBlock(4, 15)),
new EcBlocks(26, new EcBlock(4, 13), new EcBlock(1, 14))
),
new self(
8,
[6, 24, 42],
new EcBlocks(24, new EcBlock(2, 97)),
new EcBlocks(22, new EcBlock(2, 38), new EcBlock(2, 39)),
new EcBlocks(22, new EcBlock(4, 18), new EcBlock(2, 19)),
new EcBlocks(26, new EcBlock(4, 14), new EcBlock(2, 15))
),
new self(
9,
[6, 26, 46],
new EcBlocks(30, new EcBlock(2, 116)),
new EcBlocks(22, new EcBlock(3, 36), new EcBlock(2, 37)),
new EcBlocks(20, new EcBlock(4, 16), new EcBlock(4, 17)),
new EcBlocks(24, new EcBlock(4, 12), new EcBlock(4, 13))
),
new self(
10,
[6, 28, 50],
new EcBlocks(18, new EcBlock(2, 68), new EcBlock(2, 69)),
new EcBlocks(26, new EcBlock(4, 43), new EcBlock(1, 44)),
new EcBlocks(24, new EcBlock(6, 19), new EcBlock(2, 20)),
new EcBlocks(28, new EcBlock(6, 15), new EcBlock(2, 16))
),
new self(
11,
[6, 30, 54],
new EcBlocks(20, new EcBlock(4, 81)),
new EcBlocks(30, new EcBlock(1, 50), new EcBlock(4, 51)),
new EcBlocks(28, new EcBlock(4, 22), new EcBlock(4, 23)),
new EcBlocks(24, new EcBlock(3, 12), new EcBlock(8, 13))
),
new self(
12,
[6, 32, 58],
new EcBlocks(24, new EcBlock(2, 92), new EcBlock(2, 93)),
new EcBlocks(22, new EcBlock(6, 36), new EcBlock(2, 37)),
new EcBlocks(26, new EcBlock(4, 20), new EcBlock(6, 21)),
new EcBlocks(28, new EcBlock(7, 14), new EcBlock(4, 15))
),
new self(
13,
[6, 34, 62],
new EcBlocks(26, new EcBlock(4, 107)),
new EcBlocks(22, new EcBlock(8, 37), new EcBlock(1, 38)),
new EcBlocks(24, new EcBlock(8, 20), new EcBlock(4, 21)),
new EcBlocks(22, new EcBlock(12, 11), new EcBlock(4, 12))
),
new self(
14,
[6, 26, 46, 66],
new EcBlocks(30, new EcBlock(3, 115), new EcBlock(1, 116)),
new EcBlocks(24, new EcBlock(4, 40), new EcBlock(5, 41)),
new EcBlocks(20, new EcBlock(11, 16), new EcBlock(5, 17)),
new EcBlocks(24, new EcBlock(11, 12), new EcBlock(5, 13))
),
new self(
15,
[6, 26, 48, 70],
new EcBlocks(22, new EcBlock(5, 87), new EcBlock(1, 88)),
new EcBlocks(24, new EcBlock(5, 41), new EcBlock(5, 42)),
new EcBlocks(30, new EcBlock(5, 24), new EcBlock(7, 25)),
new EcBlocks(24, new EcBlock(11, 12), new EcBlock(7, 13))
),
new self(
16,
[6, 26, 50, 74],
new EcBlocks(24, new EcBlock(5, 98), new EcBlock(1, 99)),
new EcBlocks(28, new EcBlock(7, 45), new EcBlock(3, 46)),
new EcBlocks(24, new EcBlock(15, 19), new EcBlock(2, 20)),
new EcBlocks(30, new EcBlock(3, 15), new EcBlock(13, 16))
),
new self(
17,
[6, 30, 54, 78],
new EcBlocks(28, new EcBlock(1, 107), new EcBlock(5, 108)),
new EcBlocks(28, new EcBlock(10, 46), new EcBlock(1, 47)),
new EcBlocks(28, new EcBlock(1, 22), new EcBlock(15, 23)),
new EcBlocks(28, new EcBlock(2, 14), new EcBlock(17, 15))
),
new self(
18,
[6, 30, 56, 82],
new EcBlocks(30, new EcBlock(5, 120), new EcBlock(1, 121)),
new EcBlocks(26, new EcBlock(9, 43), new EcBlock(4, 44)),
new EcBlocks(28, new EcBlock(17, 22), new EcBlock(1, 23)),
new EcBlocks(28, new EcBlock(2, 14), new EcBlock(19, 15))
),
new self(
19,
[6, 30, 58, 86],
new EcBlocks(28, new EcBlock(3, 113), new EcBlock(4, 114)),
new EcBlocks(26, new EcBlock(3, 44), new EcBlock(11, 45)),
new EcBlocks(26, new EcBlock(17, 21), new EcBlock(4, 22)),
new EcBlocks(26, new EcBlock(9, 13), new EcBlock(16, 14))
),
new self(
20,
[6, 34, 62, 90],
new EcBlocks(28, new EcBlock(3, 107), new EcBlock(5, 108)),
new EcBlocks(26, new EcBlock(3, 41), new EcBlock(13, 42)),
new EcBlocks(30, new EcBlock(15, 24), new EcBlock(5, 25)),
new EcBlocks(28, new EcBlock(15, 15), new EcBlock(10, 16))
),
new self(
21,
[6, 28, 50, 72, 94],
new EcBlocks(28, new EcBlock(4, 116), new EcBlock(4, 117)),
new EcBlocks(26, new EcBlock(17, 42)),
new EcBlocks(28, new EcBlock(17, 22), new EcBlock(6, 23)),
new EcBlocks(30, new EcBlock(19, 16), new EcBlock(6, 17))
),
new self(
22,
[6, 26, 50, 74, 98],
new EcBlocks(28, new EcBlock(2, 111), new EcBlock(7, 112)),
new EcBlocks(28, new EcBlock(17, 46)),
new EcBlocks(30, new EcBlock(7, 24), new EcBlock(16, 25)),
new EcBlocks(24, new EcBlock(34, 13))
),
new self(
23,
[6, 30, 54, 78, 102],
new EcBlocks(30, new EcBlock(4, 121), new EcBlock(5, 122)),
new EcBlocks(28, new EcBlock(4, 47), new EcBlock(14, 48)),
new EcBlocks(30, new EcBlock(11, 24), new EcBlock(14, 25)),
new EcBlocks(30, new EcBlock(16, 15), new EcBlock(14, 16))
),
new self(
24,
[6, 28, 54, 80, 106],
new EcBlocks(30, new EcBlock(6, 117), new EcBlock(4, 118)),
new EcBlocks(28, new EcBlock(6, 45), new EcBlock(14, 46)),
new EcBlocks(30, new EcBlock(11, 24), new EcBlock(16, 25)),
new EcBlocks(30, new EcBlock(30, 16), new EcBlock(2, 17))
),
new self(
25,
[6, 32, 58, 84, 110],
new EcBlocks(26, new EcBlock(8, 106), new EcBlock(4, 107)),
new EcBlocks(28, new EcBlock(8, 47), new EcBlock(13, 48)),
new EcBlocks(30, new EcBlock(7, 24), new EcBlock(22, 25)),
new EcBlocks(30, new EcBlock(22, 15), new EcBlock(13, 16))
),
new self(
26,
[6, 30, 58, 86, 114],
new EcBlocks(28, new EcBlock(10, 114), new EcBlock(2, 115)),
new EcBlocks(28, new EcBlock(19, 46), new EcBlock(4, 47)),
new EcBlocks(28, new EcBlock(28, 22), new EcBlock(6, 23)),
new EcBlocks(30, new EcBlock(33, 16), new EcBlock(4, 17))
),
new self(
27,
[6, 34, 62, 90, 118],
new EcBlocks(30, new EcBlock(8, 122), new EcBlock(4, 123)),
new EcBlocks(28, new EcBlock(22, 45), new EcBlock(3, 46)),
new EcBlocks(30, new EcBlock(8, 23), new EcBlock(26, 24)),
new EcBlocks(30, new EcBlock(12, 15), new EcBlock(28, 16))
),
new self(
28,
[6, 26, 50, 74, 98, 122],
new EcBlocks(30, new EcBlock(3, 117), new EcBlock(10, 118)),
new EcBlocks(28, new EcBlock(3, 45), new EcBlock(23, 46)),
new EcBlocks(30, new EcBlock(4, 24), new EcBlock(31, 25)),
new EcBlocks(30, new EcBlock(11, 15), new EcBlock(31, 16))
),
new self(
29,
[6, 30, 54, 78, 102, 126],
new EcBlocks(30, new EcBlock(7, 116), new EcBlock(7, 117)),
new EcBlocks(28, new EcBlock(21, 45), new EcBlock(7, 46)),
new EcBlocks(30, new EcBlock(1, 23), new EcBlock(37, 24)),
new EcBlocks(30, new EcBlock(19, 15), new EcBlock(26, 16))
),
new self(
30,
[6, 26, 52, 78, 104, 130],
new EcBlocks(30, new EcBlock(5, 115), new EcBlock(10, 116)),
new EcBlocks(28, new EcBlock(19, 47), new EcBlock(10, 48)),
new EcBlocks(30, new EcBlock(15, 24), new EcBlock(25, 25)),
new EcBlocks(30, new EcBlock(23, 15), new EcBlock(25, 16))
),
new self(
31,
[6, 30, 56, 82, 108, 134],
new EcBlocks(30, new EcBlock(13, 115), new EcBlock(3, 116)),
new EcBlocks(28, new EcBlock(2, 46), new EcBlock(29, 47)),
new EcBlocks(30, new EcBlock(42, 24), new EcBlock(1, 25)),
new EcBlocks(30, new EcBlock(23, 15), new EcBlock(28, 16))
),
new self(
32,
[6, 34, 60, 86, 112, 138],
new EcBlocks(30, new EcBlock(17, 115)),
new EcBlocks(28, new EcBlock(10, 46), new EcBlock(23, 47)),
new EcBlocks(30, new EcBlock(10, 24), new EcBlock(35, 25)),
new EcBlocks(30, new EcBlock(19, 15), new EcBlock(35, 16))
),
new self(
33,
[6, 30, 58, 86, 114, 142],
new EcBlocks(30, new EcBlock(17, 115), new EcBlock(1, 116)),
new EcBlocks(28, new EcBlock(14, 46), new EcBlock(21, 47)),
new EcBlocks(30, new EcBlock(29, 24), new EcBlock(19, 25)),
new EcBlocks(30, new EcBlock(11, 15), new EcBlock(46, 16))
),
new self(
34,
[6, 34, 62, 90, 118, 146],
new EcBlocks(30, new EcBlock(13, 115), new EcBlock(6, 116)),
new EcBlocks(28, new EcBlock(14, 46), new EcBlock(23, 47)),
new EcBlocks(30, new EcBlock(44, 24), new EcBlock(7, 25)),
new EcBlocks(30, new EcBlock(59, 16), new EcBlock(1, 17))
),
new self(
35,
[6, 30, 54, 78, 102, 126, 150],
new EcBlocks(30, new EcBlock(12, 121), new EcBlock(7, 122)),
new EcBlocks(28, new EcBlock(12, 47), new EcBlock(26, 48)),
new EcBlocks(30, new EcBlock(39, 24), new EcBlock(14, 25)),
new EcBlocks(30, new EcBlock(22, 15), new EcBlock(41, 16))
),
new self(
36,
[6, 24, 50, 76, 102, 128, 154],
new EcBlocks(30, new EcBlock(6, 121), new EcBlock(14, 122)),
new EcBlocks(28, new EcBlock(6, 47), new EcBlock(34, 48)),
new EcBlocks(30, new EcBlock(46, 24), new EcBlock(10, 25)),
new EcBlocks(30, new EcBlock(2, 15), new EcBlock(64, 16))
),
new self(
37,
[6, 28, 54, 80, 106, 132, 158],
new EcBlocks(30, new EcBlock(17, 122), new EcBlock(4, 123)),
new EcBlocks(28, new EcBlock(29, 46), new EcBlock(14, 47)),
new EcBlocks(30, new EcBlock(49, 24), new EcBlock(10, 25)),
new EcBlocks(30, new EcBlock(24, 15), new EcBlock(46, 16))
),
new self(
38,
[6, 32, 58, 84, 110, 136, 162],
new EcBlocks(30, new EcBlock(4, 122), new EcBlock(18, 123)),
new EcBlocks(28, new EcBlock(13, 46), new EcBlock(32, 47)),
new EcBlocks(30, new EcBlock(48, 24), new EcBlock(14, 25)),
new EcBlocks(30, new EcBlock(42, 15), new EcBlock(32, 16))
),
new self(
39,
[6, 26, 54, 82, 110, 138, 166],
new EcBlocks(30, new EcBlock(20, 117), new EcBlock(4, 118)),
new EcBlocks(28, new EcBlock(40, 47), new EcBlock(7, 48)),
new EcBlocks(30, new EcBlock(43, 24), new EcBlock(22, 25)),
new EcBlocks(30, new EcBlock(10, 15), new EcBlock(67, 16))
),
new self(
40,
[6, 30, 58, 86, 114, 142, 170],
new EcBlocks(30, new EcBlock(19, 118), new EcBlock(6, 119)),
new EcBlocks(28, new EcBlock(18, 47), new EcBlock(31, 48)),
new EcBlocks(30, new EcBlock(34, 24), new EcBlock(34, 25)),
new EcBlocks(30, new EcBlock(20, 15), new EcBlock(61, 16))
),
];
}
}
+44
View File
@@ -0,0 +1,44 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Encoder;
use SplFixedArray;
/**
* Block pair.
*/
final class BlockPair
{
/**
* Creates a new block pair.
*
* @param SplFixedArray<int> $dataBytes Data bytes in the block.
* @param SplFixedArray<int> $errorCorrectionBytes Error correction bytes in the block.
*/
public function __construct(
private readonly SplFixedArray $dataBytes,
private readonly SplFixedArray $errorCorrectionBytes
) {
}
/**
* Gets the data bytes.
*
* @return SplFixedArray<int>
*/
public function getDataBytes() : SplFixedArray
{
return $this->dataBytes;
}
/**
* Gets the error correction bytes.
*
* @return SplFixedArray<int>
*/
public function getErrorCorrectionBytes() : SplFixedArray
{
return $this->errorCorrectionBytes;
}
}
+134
View File
@@ -0,0 +1,134 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Encoder;
use SplFixedArray;
use Traversable;
/**
* Byte matrix.
*/
final class ByteMatrix
{
/**
* Bytes in the matrix, represented as array.
*
* @var SplFixedArray<SplFixedArray<int>>
*/
private SplFixedArray $bytes;
public function __construct(private readonly int $width, private readonly int $height)
{
$this->bytes = new SplFixedArray($height);
for ($y = 0; $y < $height; ++$y) {
$this->bytes[$y] = SplFixedArray::fromArray(array_fill(0, $width, 0));
}
}
/**
* Gets the width of the matrix.
*/
public function getWidth() : int
{
return $this->width;
}
/**
* Gets the height of the matrix.
*/
public function getHeight() : int
{
return $this->height;
}
/**
* Gets the internal representation of the matrix.
*
* @return SplFixedArray<SplFixedArray<int>>
*/
public function getArray() : SplFixedArray
{
return $this->bytes;
}
/**
* @return Traversable<int>
*/
public function getBytes() : Traversable
{
foreach ($this->bytes as $row) {
foreach ($row as $byte) {
yield $byte;
}
}
}
/**
* Gets the byte for a specific position.
*/
public function get(int $x, int $y) : int
{
return $this->bytes[$y][$x];
}
/**
* Sets the byte for a specific position.
*/
public function set(int $x, int $y, int $value) : void
{
$this->bytes[$y][$x] = $value;
}
/**
* Clears the matrix with a specific value.
*/
public function clear(int $value) : void
{
for ($y = 0; $y < $this->height; ++$y) {
for ($x = 0; $x < $this->width; ++$x) {
$this->bytes[$y][$x] = $value;
}
}
}
public function __clone()
{
$this->bytes = clone $this->bytes;
foreach ($this->bytes as $index => $row) {
$this->bytes[$index] = clone $row;
}
}
/**
* Returns a string representation of the matrix.
*/
public function __toString() : string
{
$result = '';
for ($y = 0; $y < $this->height; $y++) {
for ($x = 0; $x < $this->width; $x++) {
switch ($this->bytes[$y][$x]) {
case 0:
$result .= ' 0';
break;
case 1:
$result .= ' 1';
break;
default:
$result .= ' ';
break;
}
}
$result .= "\n";
}
return $result;
}
}
+666
View File
@@ -0,0 +1,666 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Encoder;
use BaconQrCode\Common\BitArray;
use BaconQrCode\Common\CharacterSetEci;
use BaconQrCode\Common\ErrorCorrectionLevel;
use BaconQrCode\Common\Mode;
use BaconQrCode\Common\ReedSolomonCodec;
use BaconQrCode\Common\Version;
use BaconQrCode\Exception\WriterException;
use SplFixedArray;
/**
* Encoder.
*/
final class Encoder
{
/**
* Default byte encoding.
*/
public const DEFAULT_BYTE_MODE_ENCODING = 'ISO-8859-1';
/** @deprecated use DEFAULT_BYTE_MODE_ENCODING */
public const DEFAULT_BYTE_MODE_ECODING = self::DEFAULT_BYTE_MODE_ENCODING;
/**
* Allowed characters for the Alphanumeric Mode.
*/
private const ALPHANUMERIC_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:';
/**
* The original table is defined in the table 5 of JISX0510:2004 (p.19).
*/
private const ALPHANUMERIC_TABLE = [
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x00-0x0f
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x10-0x1f
36, -1, -1, -1, 37, 38, -1, -1, -1, -1, 39, 40, -1, 41, 42, 43, // 0x20-0x2f
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 44, -1, -1, -1, -1, -1, // 0x30-0x3f
-1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, // 0x40-0x4f
25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1, // 0x50-0x5f
];
/**
* Codec cache.
*
* @var array<string,ReedSolomonCodec>
*/
private static array $codecs = [];
/**
* Encodes "content" with the error correction level "ecLevel".
*/
public static function encode(
string $content,
ErrorCorrectionLevel $ecLevel,
string $encoding = self::DEFAULT_BYTE_MODE_ENCODING,
?Version $forcedVersion = null,
// Barcode scanner might not be able to read the encoded message of the QR code with the prefix ECI of UTF-8
bool $prefixEci = true
) : QrCode {
// Pick an encoding mode appropriate for the content. Note that this
// will not attempt to use multiple modes / segments even if that were
// more efficient. Would be nice.
$mode = self::chooseMode($content, $encoding);
// This will store the header information, like mode and length, as well
// as "header" segments like an ECI segment.
$headerBits = new BitArray();
// Append ECI segment if applicable
if ($prefixEci && Mode::BYTE() === $mode && self::DEFAULT_BYTE_MODE_ENCODING !== $encoding) {
$eci = CharacterSetEci::getCharacterSetEciByName($encoding);
if (null !== $eci) {
self::appendEci($eci, $headerBits);
}
}
// (With ECI in place,) Write the mode marker
self::appendModeInfo($mode, $headerBits);
// Collect data within the main segment, separately, to count its size
// if needed. Don't add it to main payload yet.
$dataBits = new BitArray();
self::appendBytes($content, $mode, $dataBits, $encoding);
// Hard part: need to know version to know how many bits length takes.
// But need to know how many bits it takes to know version. First we
// take a guess at version by assuming version will be the minimum, 1:
$provisionalBitsNeeded = $headerBits->getSize()
+ $mode->getCharacterCountBits(Version::getVersionForNumber(1))
+ $dataBits->getSize();
$provisionalVersion = self::chooseVersion($provisionalBitsNeeded, $ecLevel);
// Use that guess to calculate the right version. I am still not sure
// this works in 100% of cases.
$bitsNeeded = $headerBits->getSize()
+ $mode->getCharacterCountBits($provisionalVersion)
+ $dataBits->getSize();
$version = self::chooseVersion($bitsNeeded, $ecLevel);
if (null !== $forcedVersion) {
// Forced version check
if ($version->getVersionNumber() <= $forcedVersion->getVersionNumber()) {
// Calculated minimum version is same or equal as forced version
$version = $forcedVersion;
} else {
throw new WriterException(
'Invalid version! Calculated version: '
. $version->getVersionNumber()
. ', requested version: '
. $forcedVersion->getVersionNumber()
);
}
}
$headerAndDataBits = new BitArray();
$headerAndDataBits->appendBitArray($headerBits);
// Find "length" of main segment and write it.
$numLetters = match ($mode) {
Mode::BYTE() => $dataBits->getSizeInBytes(),
Mode::NUMERIC(), Mode::ALPHANUMERIC() => strlen($content),
Mode::KANJI() => iconv_strlen($content, 'utf-8'),
};
self::appendLengthInfo($numLetters, $version, $mode, $headerAndDataBits);
// Put data together into the overall payload.
$headerAndDataBits->appendBitArray($dataBits);
$ecBlocks = $version->getEcBlocksForLevel($ecLevel);
$numDataBytes = $version->getTotalCodewords() - $ecBlocks->getTotalEcCodewords();
// Terminate the bits properly.
self::terminateBits($numDataBytes, $headerAndDataBits);
// Interleave data bits with error correction code.
$finalBits = self::interleaveWithEcBytes(
$headerAndDataBits,
$version->getTotalCodewords(),
$numDataBytes,
$ecBlocks->getNumBlocks()
);
// Choose the mask pattern.
$dimension = $version->getDimensionForVersion();
$matrix = new ByteMatrix($dimension, $dimension);
$maskPattern = self::chooseMaskPattern($finalBits, $ecLevel, $version, $matrix);
// Build the matrix.
MatrixUtil::buildMatrix($finalBits, $ecLevel, $version, $maskPattern, $matrix);
return new QrCode($mode, $ecLevel, $version, $maskPattern, $matrix);
}
/**
* Gets the alphanumeric code for a byte.
*/
private static function getAlphanumericCode(int $byte) : int
{
return self::ALPHANUMERIC_TABLE[$byte] ?? -1;
}
/**
* Chooses the best mode for a given content.
*/
private static function chooseMode(string $content, ?string $encoding = null) : Mode
{
if ('' === $content) {
return Mode::BYTE();
}
if (null !== $encoding && 0 === strcasecmp($encoding, 'SHIFT-JIS')) {
return self::isOnlyDoubleByteKanji($content) ? Mode::KANJI() : Mode::BYTE();
}
if (ctype_digit($content)) {
return Mode::NUMERIC();
}
if (self::isOnlyAlphanumeric($content)) {
return Mode::ALPHANUMERIC();
}
return Mode::BYTE();
}
/**
* Calculates the mask penalty for a matrix.
*/
private static function calculateMaskPenalty(ByteMatrix $matrix) : int
{
return (
MaskUtil::applyMaskPenaltyRule1($matrix)
+ MaskUtil::applyMaskPenaltyRule2($matrix)
+ MaskUtil::applyMaskPenaltyRule3($matrix)
+ MaskUtil::applyMaskPenaltyRule4($matrix)
);
}
/**
* Checks if content only consists of double-byte kanji characters (or is empty).
*/
private static function isOnlyDoubleByteKanji(string $content) : bool
{
$bytes = @iconv('utf-8', 'SHIFT-JIS', $content);
if (false === $bytes) {
return false;
}
$length = strlen($bytes);
if (0 !== $length % 2) {
return false;
}
for ($i = 0; $i < $length; $i += 2) {
$byte = ord($bytes[$i]);
if (($byte < 0x81 || $byte > 0x9f) && $byte < 0xe0 || $byte > 0xeb) {
return false;
}
}
return true;
}
/**
* Checks if content only consists of alphanumeric characters (or is empty).
*/
private static function isOnlyAlphanumeric(string $content) : bool
{
return strlen($content) === strspn($content, self::ALPHANUMERIC_CHARS);
}
/**
* Chooses the best mask pattern for a matrix.
*/
private static function chooseMaskPattern(
BitArray $bits,
ErrorCorrectionLevel $ecLevel,
Version $version,
ByteMatrix $matrix
) : int {
$minPenalty = PHP_INT_MAX;
$bestMaskPattern = -1;
for ($maskPattern = 0; $maskPattern < QrCode::NUM_MASK_PATTERNS; ++$maskPattern) {
MatrixUtil::buildMatrix($bits, $ecLevel, $version, $maskPattern, $matrix);
$penalty = self::calculateMaskPenalty($matrix);
if ($penalty < $minPenalty) {
$minPenalty = $penalty;
$bestMaskPattern = $maskPattern;
}
}
return $bestMaskPattern;
}
/**
* Chooses the best version for the input.
*
* @throws WriterException if data is too big
*/
private static function chooseVersion(int $numInputBits, ErrorCorrectionLevel $ecLevel) : Version
{
for ($versionNum = 1; $versionNum <= 40; ++$versionNum) {
$version = Version::getVersionForNumber($versionNum);
$numBytes = $version->getTotalCodewords();
$ecBlocks = $version->getEcBlocksForLevel($ecLevel);
$numEcBytes = $ecBlocks->getTotalEcCodewords();
$numDataBytes = $numBytes - $numEcBytes;
$totalInputBytes = intdiv($numInputBits + 8, 8);
if ($numDataBytes >= $totalInputBytes) {
return $version;
}
}
throw new WriterException('Data too big');
}
/**
* Terminates the bits in a bit array.
*
* @throws WriterException if data bits cannot fit in the QR code
* @throws WriterException if bits size does not equal the capacity
*/
private static function terminateBits(int $numDataBytes, BitArray $bits) : void
{
$capacity = $numDataBytes << 3;
if ($bits->getSize() > $capacity) {
throw new WriterException('Data bits cannot fit in the QR code');
}
for ($i = 0; $i < 4 && $bits->getSize() < $capacity; ++$i) {
$bits->appendBit(false);
}
$numBitsInLastByte = $bits->getSize() & 0x7;
if ($numBitsInLastByte > 0) {
for ($i = $numBitsInLastByte; $i < 8; ++$i) {
$bits->appendBit(false);
}
}
$numPaddingBytes = $numDataBytes - $bits->getSizeInBytes();
for ($i = 0; $i < $numPaddingBytes; ++$i) {
$bits->appendBits(0 === ($i & 0x1) ? 0xec : 0x11, 8);
}
if ($bits->getSize() !== $capacity) {
throw new WriterException('Bits size does not equal capacity');
}
}
/**
* Gets number of data- and EC bytes for a block ID.
*
* @return int[]
* @throws WriterException if block ID is too large
* @throws WriterException if EC bytes mismatch
* @throws WriterException if RS blocks mismatch
* @throws WriterException if total bytes mismatch
*/
private static function getNumDataBytesAndNumEcBytesForBlockId(
int $numTotalBytes,
int $numDataBytes,
int $numRsBlocks,
int $blockId
) : array {
if ($blockId >= $numRsBlocks) {
throw new WriterException('Block ID too large');
}
$numRsBlocksInGroup2 = $numTotalBytes % $numRsBlocks;
$numRsBlocksInGroup1 = $numRsBlocks - $numRsBlocksInGroup2;
$numTotalBytesInGroup1 = intdiv($numTotalBytes, $numRsBlocks);
$numTotalBytesInGroup2 = $numTotalBytesInGroup1 + 1;
$numDataBytesInGroup1 = intdiv($numDataBytes, $numRsBlocks);
$numDataBytesInGroup2 = $numDataBytesInGroup1 + 1;
$numEcBytesInGroup1 = $numTotalBytesInGroup1 - $numDataBytesInGroup1;
$numEcBytesInGroup2 = $numTotalBytesInGroup2 - $numDataBytesInGroup2;
if ($numEcBytesInGroup1 !== $numEcBytesInGroup2) {
throw new WriterException('EC bytes mismatch');
}
if ($numRsBlocks !== $numRsBlocksInGroup1 + $numRsBlocksInGroup2) {
throw new WriterException('RS blocks mismatch');
}
if ($numTotalBytes !==
(($numDataBytesInGroup1 + $numEcBytesInGroup1) * $numRsBlocksInGroup1)
+ (($numDataBytesInGroup2 + $numEcBytesInGroup2) * $numRsBlocksInGroup2)
) {
throw new WriterException('Total bytes mismatch');
}
if ($blockId < $numRsBlocksInGroup1) {
return [$numDataBytesInGroup1, $numEcBytesInGroup1];
} else {
return [$numDataBytesInGroup2, $numEcBytesInGroup2];
}
}
/**
* Interleaves data with EC bytes.
*
* @throws WriterException if number of bits and data bytes does not match
* @throws WriterException if data bytes does not match offset
* @throws WriterException if an interleaving error occurs
*/
private static function interleaveWithEcBytes(
BitArray $bits,
int $numTotalBytes,
int $numDataBytes,
int $numRsBlocks
) : BitArray {
if ($bits->getSizeInBytes() !== $numDataBytes) {
throw new WriterException('Number of bits and data bytes does not match');
}
$dataBytesOffset = 0;
$maxNumDataBytes = 0;
$maxNumEcBytes = 0;
$blocks = new SplFixedArray($numRsBlocks);
for ($i = 0; $i < $numRsBlocks; ++$i) {
list($numDataBytesInBlock, $numEcBytesInBlock) = self::getNumDataBytesAndNumEcBytesForBlockId(
$numTotalBytes,
$numDataBytes,
$numRsBlocks,
$i
);
$size = $numDataBytesInBlock;
$dataBytes = $bits->toBytes(8 * $dataBytesOffset, $size);
$ecBytes = self::generateEcBytes($dataBytes, $numEcBytesInBlock);
$blocks[$i] = new BlockPair($dataBytes, $ecBytes);
$maxNumDataBytes = max($maxNumDataBytes, $size);
$maxNumEcBytes = max($maxNumEcBytes, count($ecBytes));
$dataBytesOffset += $numDataBytesInBlock;
}
if ($numDataBytes !== $dataBytesOffset) {
throw new WriterException('Data bytes does not match offset');
}
$result = new BitArray();
for ($i = 0; $i < $maxNumDataBytes; ++$i) {
foreach ($blocks as $block) {
$dataBytes = $block->getDataBytes();
if ($i < count($dataBytes)) {
$result->appendBits($dataBytes[$i], 8);
}
}
}
for ($i = 0; $i < $maxNumEcBytes; ++$i) {
foreach ($blocks as $block) {
$ecBytes = $block->getErrorCorrectionBytes();
if ($i < count($ecBytes)) {
$result->appendBits($ecBytes[$i], 8);
}
}
}
if ($numTotalBytes !== $result->getSizeInBytes()) {
throw new WriterException(
'Interleaving error: ' . $numTotalBytes . ' and ' . $result->getSizeInBytes() . ' differ'
);
}
return $result;
}
/**
* Generates EC bytes for given data.
*
* @param SplFixedArray<int> $dataBytes
* @return SplFixedArray<int>
*/
private static function generateEcBytes(SplFixedArray $dataBytes, int $numEcBytesInBlock) : SplFixedArray
{
$numDataBytes = count($dataBytes);
$toEncode = new SplFixedArray($numDataBytes + $numEcBytesInBlock);
for ($i = 0; $i < $numDataBytes; $i++) {
$toEncode[$i] = $dataBytes[$i];
}
$ecBytes = new SplFixedArray($numEcBytesInBlock);
$codec = self::getCodec($numDataBytes, $numEcBytesInBlock);
$codec->encode($toEncode, $ecBytes);
return $ecBytes;
}
/**
* Gets an RS codec and caches it.
*/
private static function getCodec(int $numDataBytes, int $numEcBytesInBlock) : ReedSolomonCodec
{
$cacheId = $numDataBytes . '-' . $numEcBytesInBlock;
if (isset(self::$codecs[$cacheId])) {
return self::$codecs[$cacheId];
}
return self::$codecs[$cacheId] = new ReedSolomonCodec(
8,
0x11d,
0,
1,
$numEcBytesInBlock,
255 - $numDataBytes - $numEcBytesInBlock
);
}
/**
* Appends mode information to a bit array.
*/
private static function appendModeInfo(Mode $mode, BitArray $bits) : void
{
$bits->appendBits($mode->getBits(), 4);
}
/**
* Appends length information to a bit array.
*
* @throws WriterException if num letters is bigger than expected
*/
private static function appendLengthInfo(int $numLetters, Version $version, Mode $mode, BitArray $bits) : void
{
$numBits = $mode->getCharacterCountBits($version);
if ($numLetters >= (1 << $numBits)) {
throw new WriterException($numLetters . ' is bigger than ' . ((1 << $numBits) - 1));
}
$bits->appendBits($numLetters, $numBits);
}
/**
* Appends bytes to a bit array in a specific mode.
*/
private static function appendBytes(string $content, Mode $mode, BitArray $bits, string $encoding) : void
{
match ($mode) {
Mode::NUMERIC() => self::appendNumericBytes($content, $bits),
Mode::ALPHANUMERIC() => self::appendAlphanumericBytes($content, $bits),
Mode::BYTE() => self::append8BitBytes($content, $bits, $encoding),
Mode::KANJI() => self::appendKanjiBytes($content, $bits),
};
}
/**
* Appends numeric bytes to a bit array.
*/
private static function appendNumericBytes(string $content, BitArray $bits) : void
{
$length = strlen($content);
$i = 0;
while ($i < $length) {
$num1 = (int) $content[$i];
if ($i + 2 < $length) {
// Encode three numeric letters in ten bits.
$num2 = (int) $content[$i + 1];
$num3 = (int) $content[$i + 2];
$bits->appendBits($num1 * 100 + $num2 * 10 + $num3, 10);
$i += 3;
} elseif ($i + 1 < $length) {
// Encode two numeric letters in seven bits.
$num2 = (int) $content[$i + 1];
$bits->appendBits($num1 * 10 + $num2, 7);
$i += 2;
} else {
// Encode one numeric letter in four bits.
$bits->appendBits($num1, 4);
++$i;
}
}
}
/**
* Appends alpha-numeric bytes to a bit array.
*
* @throws WriterException if an invalid alphanumeric code was found
*/
private static function appendAlphanumericBytes(string $content, BitArray $bits) : void
{
$length = strlen($content);
$i = 0;
while ($i < $length) {
$code1 = self::getAlphanumericCode(ord($content[$i]));
if (-1 === $code1) {
throw new WriterException('Invalid alphanumeric code');
}
if ($i + 1 < $length) {
$code2 = self::getAlphanumericCode(ord($content[$i + 1]));
if (-1 === $code2) {
throw new WriterException('Invalid alphanumeric code');
}
// Encode two alphanumeric letters in 11 bits.
$bits->appendBits($code1 * 45 + $code2, 11);
$i += 2;
} else {
// Encode one alphanumeric letter in six bits.
$bits->appendBits($code1, 6);
++$i;
}
}
}
/**
* Appends regular 8-bit bytes to a bit array.
*
* @throws WriterException if content cannot be encoded to target encoding
*/
private static function append8BitBytes(string $content, BitArray $bits, string $encoding) : void
{
$bytes = @iconv('utf-8', $encoding, $content);
if (false === $bytes) {
throw new WriterException('Could not encode content to ' . $encoding);
}
$length = strlen($bytes);
for ($i = 0; $i < $length; $i++) {
$bits->appendBits(ord($bytes[$i]), 8);
}
}
/**
* Appends KANJI bytes to a bit array.
*
* @throws WriterException if content does not seem to be encoded in SHIFT-JIS
* @throws WriterException if an invalid byte sequence occurs
*/
private static function appendKanjiBytes(string $content, BitArray $bits) : void
{
$bytes = @iconv('utf-8', 'SHIFT-JIS', $content);
if (false === $bytes) {
throw new WriterException('Content could not be converted to SHIFT-JIS');
}
if (strlen($bytes) % 2 > 0) {
// We just do a simple length check here. The for loop will check
// individual characters.
throw new WriterException('Content does not seem to be encoded in SHIFT-JIS');
}
$length = strlen($bytes);
for ($i = 0; $i < $length; $i += 2) {
$byte1 = ord($bytes[$i]);
$byte2 = ord($bytes[$i + 1]);
$code = ($byte1 << 8) | $byte2;
if ($code >= 0x8140 && $code <= 0x9ffc) {
$subtracted = $code - 0x8140;
} elseif ($code >= 0xe040 && $code <= 0xebbf) {
$subtracted = $code - 0xc140;
} else {
throw new WriterException('Invalid byte sequence');
}
$encoded = (($subtracted >> 8) * 0xc0) + ($subtracted & 0xff);
$bits->appendBits($encoded, 13);
}
}
/**
* Appends ECI information to a bit array.
*/
private static function appendEci(CharacterSetEci $eci, BitArray $bits) : void
{
$mode = Mode::ECI();
$bits->appendBits($mode->getBits(), 4);
$bits->appendBits($eci->getValue(), 8);
}
}
+235
View File
@@ -0,0 +1,235 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Encoder;
/**
* Mask utility.
*/
final class MaskUtil
{
/**#@+
* Penalty weights from section 6.8.2.1
*/
public const N1 = 3;
public const N2 = 3;
public const N3 = 40;
public const N4 = 10;
/**#@-*/
private function __construct()
{
}
/**
* Applies mask penalty rule 1 and returns the penalty.
*
* Finds repetitive cells with the same color and gives penalty to them.
* Example: 00000 or 11111.
*/
public static function applyMaskPenaltyRule1(ByteMatrix $matrix) : int
{
return (
self::applyMaskPenaltyRule1Internal($matrix, true)
+ self::applyMaskPenaltyRule1Internal($matrix, false)
);
}
/**
* Applies mask penalty rule 2 and returns the penalty.
*
* Finds 2x2 blocks with the same color and gives penalty to them. This is
* actually equivalent to the spec's rule, which is to find MxN blocks and
* give a penalty proportional to (M-1)x(N-1), because this is the number of
* 2x2 blocks inside such a block.
*/
public static function applyMaskPenaltyRule2(ByteMatrix $matrix) : int
{
$penalty = 0;
$array = $matrix->getArray();
$width = $matrix->getWidth();
$height = $matrix->getHeight();
for ($y = 0; $y < $height - 1; ++$y) {
for ($x = 0; $x < $width - 1; ++$x) {
$value = $array[$y][$x];
if ($value === $array[$y][$x + 1]
&& $value === $array[$y + 1][$x]
&& $value === $array[$y + 1][$x + 1]
) {
++$penalty;
}
}
}
return self::N2 * $penalty;
}
/**
* Applies mask penalty rule 3 and returns the penalty.
*
* Finds consecutive cells of 00001011101 or 10111010000, and gives penalty
* to them. If we find patterns like 000010111010000, we give penalties
* twice (i.e. 40 * 2).
*/
public static function applyMaskPenaltyRule3(ByteMatrix $matrix) : int
{
$penalty = 0;
$array = $matrix->getArray();
$width = $matrix->getWidth();
$height = $matrix->getHeight();
for ($y = 0; $y < $height; ++$y) {
for ($x = 0; $x < $width; ++$x) {
if ($x + 6 < $width
&& 1 === $array[$y][$x]
&& 0 === $array[$y][$x + 1]
&& 1 === $array[$y][$x + 2]
&& 1 === $array[$y][$x + 3]
&& 1 === $array[$y][$x + 4]
&& 0 === $array[$y][$x + 5]
&& 1 === $array[$y][$x + 6]
&& (
(
$x + 10 < $width
&& 0 === $array[$y][$x + 7]
&& 0 === $array[$y][$x + 8]
&& 0 === $array[$y][$x + 9]
&& 0 === $array[$y][$x + 10]
)
|| (
$x - 4 >= 0
&& 0 === $array[$y][$x - 1]
&& 0 === $array[$y][$x - 2]
&& 0 === $array[$y][$x - 3]
&& 0 === $array[$y][$x - 4]
)
)
) {
$penalty += self::N3;
}
if ($y + 6 < $height
&& 1 === $array[$y][$x]
&& 0 === $array[$y + 1][$x]
&& 1 === $array[$y + 2][$x]
&& 1 === $array[$y + 3][$x]
&& 1 === $array[$y + 4][$x]
&& 0 === $array[$y + 5][$x]
&& 1 === $array[$y + 6][$x]
&& (
(
$y + 10 < $height
&& 0 === $array[$y + 7][$x]
&& 0 === $array[$y + 8][$x]
&& 0 === $array[$y + 9][$x]
&& 0 === $array[$y + 10][$x]
)
|| (
$y - 4 >= 0
&& 0 === $array[$y - 1][$x]
&& 0 === $array[$y - 2][$x]
&& 0 === $array[$y - 3][$x]
&& 0 === $array[$y - 4][$x]
)
)
) {
$penalty += self::N3;
}
}
}
return $penalty;
}
/**
* Applies mask penalty rule 4 and returns the penalty.
*
* Calculates the ratio of dark cells and gives penalty if the ratio is far
* from 50%. It gives 10 penalty for 5% distance.
*/
public static function applyMaskPenaltyRule4(ByteMatrix $matrix) : int
{
$numDarkCells = 0;
$array = $matrix->getArray();
$width = $matrix->getWidth();
$height = $matrix->getHeight();
for ($y = 0; $y < $height; ++$y) {
$arrayY = $array[$y];
for ($x = 0; $x < $width; ++$x) {
if (1 === $arrayY[$x]) {
++$numDarkCells;
}
}
}
$numTotalCells = $height * $width;
$darkRatio = $numDarkCells / $numTotalCells;
$fixedPercentVariances = (int) floor(abs($darkRatio - 0.5) * 20);
return $fixedPercentVariances * self::N4;
}
/**
* Returns the mask bit for "getMaskPattern" at "x" and "y".
*
* See 8.8 of JISX0510:2004 for mask pattern conditions.
*/
public static function getDataMaskBit(int $maskPattern, int $x, int $y) : bool
{
return 0 === match ($maskPattern) {
0 => ($x + $y) % 2,
1 => $y % 2,
2 => $x % 3,
3 => ($x + $y) % 3,
4 => (intdiv($y, 2) + intdiv($x, 3)) % 2,
5 => (($x * $y) % 2) + (($x * $y) % 3),
6 => ((($x * $y) % 2) + (($x * $y) % 3)) % 2,
7 => ((($x + $y) % 2) + (($x * $y) % 3)) % 2,
};
}
/**
* Helper function for applyMaskPenaltyRule1.
*
* We need this for doing this calculation in both vertical and horizontal
* orders respectively.
*/
private static function applyMaskPenaltyRule1Internal(ByteMatrix $matrix, bool $isHorizontal) : int
{
$penalty = 0;
$iLimit = $isHorizontal ? $matrix->getHeight() : $matrix->getWidth();
$jLimit = $isHorizontal ? $matrix->getWidth() : $matrix->getHeight();
$array = $matrix->getArray();
for ($i = 0; $i < $iLimit; ++$i) {
$numSameBitCells = 0;
$prevBit = -1;
for ($j = 0; $j < $jLimit; $j++) {
$bit = $isHorizontal ? $array[$i][$j] : $array[$j][$i];
if ($bit === $prevBit) {
++$numSameBitCells;
} else {
if ($numSameBitCells >= 5) {
$penalty += self::N1 + ($numSameBitCells - 5);
}
$numSameBitCells = 1;
$prevBit = $bit;
}
}
if ($numSameBitCells >= 5) {
$penalty += self::N1 + ($numSameBitCells - 5);
}
}
return $penalty;
}
}
+513
View File
@@ -0,0 +1,513 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Encoder;
use BaconQrCode\Common\BitArray;
use BaconQrCode\Common\ErrorCorrectionLevel;
use BaconQrCode\Common\Version;
use BaconQrCode\Exception\RuntimeException;
use BaconQrCode\Exception\WriterException;
/**
* Matrix utility.
*/
final class MatrixUtil
{
/**
* Position detection pattern.
*/
private const POSITION_DETECTION_PATTERN = [
[1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 1],
[1, 0, 1, 1, 1, 0, 1],
[1, 0, 1, 1, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1],
];
/**
* Position adjustment pattern.
*/
private const POSITION_ADJUSTMENT_PATTERN = [
[1, 1, 1, 1, 1],
[1, 0, 0, 0, 1],
[1, 0, 1, 0, 1],
[1, 0, 0, 0, 1],
[1, 1, 1, 1, 1],
];
/**
* Coordinates for position adjustment patterns for each version.
*/
private const POSITION_ADJUSTMENT_PATTERN_COORDINATE_TABLE = [
[null, null, null, null, null, null, null], // Version 1
[ 6, 18, null, null, null, null, null], // Version 2
[ 6, 22, null, null, null, null, null], // Version 3
[ 6, 26, null, null, null, null, null], // Version 4
[ 6, 30, null, null, null, null, null], // Version 5
[ 6, 34, null, null, null, null, null], // Version 6
[ 6, 22, 38, null, null, null, null], // Version 7
[ 6, 24, 42, null, null, null, null], // Version 8
[ 6, 26, 46, null, null, null, null], // Version 9
[ 6, 28, 50, null, null, null, null], // Version 10
[ 6, 30, 54, null, null, null, null], // Version 11
[ 6, 32, 58, null, null, null, null], // Version 12
[ 6, 34, 62, null, null, null, null], // Version 13
[ 6, 26, 46, 66, null, null, null], // Version 14
[ 6, 26, 48, 70, null, null, null], // Version 15
[ 6, 26, 50, 74, null, null, null], // Version 16
[ 6, 30, 54, 78, null, null, null], // Version 17
[ 6, 30, 56, 82, null, null, null], // Version 18
[ 6, 30, 58, 86, null, null, null], // Version 19
[ 6, 34, 62, 90, null, null, null], // Version 20
[ 6, 28, 50, 72, 94, null, null], // Version 21
[ 6, 26, 50, 74, 98, null, null], // Version 22
[ 6, 30, 54, 78, 102, null, null], // Version 23
[ 6, 28, 54, 80, 106, null, null], // Version 24
[ 6, 32, 58, 84, 110, null, null], // Version 25
[ 6, 30, 58, 86, 114, null, null], // Version 26
[ 6, 34, 62, 90, 118, null, null], // Version 27
[ 6, 26, 50, 74, 98, 122, null], // Version 28
[ 6, 30, 54, 78, 102, 126, null], // Version 29
[ 6, 26, 52, 78, 104, 130, null], // Version 30
[ 6, 30, 56, 82, 108, 134, null], // Version 31
[ 6, 34, 60, 86, 112, 138, null], // Version 32
[ 6, 30, 58, 86, 114, 142, null], // Version 33
[ 6, 34, 62, 90, 118, 146, null], // Version 34
[ 6, 30, 54, 78, 102, 126, 150], // Version 35
[ 6, 24, 50, 76, 102, 128, 154], // Version 36
[ 6, 28, 54, 80, 106, 132, 158], // Version 37
[ 6, 32, 58, 84, 110, 136, 162], // Version 38
[ 6, 26, 54, 82, 110, 138, 166], // Version 39
[ 6, 30, 58, 86, 114, 142, 170], // Version 40
];
/**
* Type information coordinates.
*/
private const TYPE_INFO_COORDINATES = [
[8, 0],
[8, 1],
[8, 2],
[8, 3],
[8, 4],
[8, 5],
[8, 7],
[8, 8],
[7, 8],
[5, 8],
[4, 8],
[3, 8],
[2, 8],
[1, 8],
[0, 8],
];
/**
* Version information polynomial.
*/
private const VERSION_INFO_POLY = 0x1f25;
/**
* Type information polynomial.
*/
private const TYPE_INFO_POLY = 0x537;
/**
* Type information mask pattern.
*/
private const TYPE_INFO_MASK_PATTERN = 0x5412;
/**
* Clears a given matrix.
*/
public static function clearMatrix(ByteMatrix $matrix) : void
{
$matrix->clear(-1);
}
/**
* Builds a complete matrix.
*/
public static function buildMatrix(
BitArray $dataBits,
ErrorCorrectionLevel $level,
Version $version,
int $maskPattern,
ByteMatrix $matrix
) : void {
self::clearMatrix($matrix);
self::embedBasicPatterns($version, $matrix);
self::embedTypeInfo($level, $maskPattern, $matrix);
self::maybeEmbedVersionInfo($version, $matrix);
self::embedDataBits($dataBits, $maskPattern, $matrix);
}
/**
* Removes the position detection patterns from a matrix.
*
* This can be useful if you need to render those patterns separately.
*/
public static function removePositionDetectionPatterns(ByteMatrix $matrix) : void
{
$pdpWidth = count(self::POSITION_DETECTION_PATTERN[0]);
self::removePositionDetectionPattern(0, 0, $matrix);
self::removePositionDetectionPattern($matrix->getWidth() - $pdpWidth, 0, $matrix);
self::removePositionDetectionPattern(0, $matrix->getWidth() - $pdpWidth, $matrix);
}
/**
* Embeds type information into a matrix.
*/
private static function embedTypeInfo(ErrorCorrectionLevel $level, int $maskPattern, ByteMatrix $matrix) : void
{
$typeInfoBits = new BitArray();
self::makeTypeInfoBits($level, $maskPattern, $typeInfoBits);
$typeInfoBitsSize = $typeInfoBits->getSize();
for ($i = 0; $i < $typeInfoBitsSize; ++$i) {
$bit = $typeInfoBits->get($typeInfoBitsSize - 1 - $i);
$x1 = self::TYPE_INFO_COORDINATES[$i][0];
$y1 = self::TYPE_INFO_COORDINATES[$i][1];
$matrix->set($x1, $y1, (int) $bit);
if ($i < 8) {
$x2 = $matrix->getWidth() - $i - 1;
$y2 = 8;
} else {
$x2 = 8;
$y2 = $matrix->getHeight() - 7 + ($i - 8);
}
$matrix->set($x2, $y2, (int) $bit);
}
}
/**
* Generates type information bits and appends them to a bit array.
*
* @throws RuntimeException if bit array resulted in invalid size
*/
private static function makeTypeInfoBits(ErrorCorrectionLevel $level, int $maskPattern, BitArray $bits) : void
{
$typeInfo = ($level->getBits() << 3) | $maskPattern;
$bits->appendBits($typeInfo, 5);
$bchCode = self::calculateBchCode($typeInfo, self::TYPE_INFO_POLY);
$bits->appendBits($bchCode, 10);
$maskBits = new BitArray();
$maskBits->appendBits(self::TYPE_INFO_MASK_PATTERN, 15);
$bits->xorBits($maskBits);
if (15 !== $bits->getSize()) {
throw new RuntimeException('Bit array resulted in invalid size: ' . $bits->getSize());
}
}
/**
* Embeds version information if required.
*/
private static function maybeEmbedVersionInfo(Version $version, ByteMatrix $matrix) : void
{
if ($version->getVersionNumber() < 7) {
return;
}
$versionInfoBits = new BitArray();
self::makeVersionInfoBits($version, $versionInfoBits);
$bitIndex = 6 * 3 - 1;
for ($i = 0; $i < 6; ++$i) {
for ($j = 0; $j < 3; ++$j) {
$bit = $versionInfoBits->get($bitIndex);
--$bitIndex;
$matrix->set($i, $matrix->getHeight() - 11 + $j, (int) $bit);
$matrix->set($matrix->getHeight() - 11 + $j, $i, (int) $bit);
}
}
}
/**
* Generates version information bits and appends them to a bit array.
*
* @throws RuntimeException if bit array resulted in invalid size
*/
private static function makeVersionInfoBits(Version $version, BitArray $bits) : void
{
$bits->appendBits($version->getVersionNumber(), 6);
$bchCode = self::calculateBchCode($version->getVersionNumber(), self::VERSION_INFO_POLY);
$bits->appendBits($bchCode, 12);
if (18 !== $bits->getSize()) {
throw new RuntimeException('Bit array resulted in invalid size: ' . $bits->getSize());
}
}
/**
* Calculates the BCH code for a value and a polynomial.
*/
private static function calculateBchCode(int $value, int $poly) : int
{
$msbSetInPoly = self::findMsbSet($poly);
$value <<= $msbSetInPoly - 1;
while (self::findMsbSet($value) >= $msbSetInPoly) {
$value ^= $poly << (self::findMsbSet($value) - $msbSetInPoly);
}
return $value;
}
/**
* Finds and MSB set.
*/
private static function findMsbSet(int $value) : int
{
$numDigits = 0;
while (0 !== $value) {
$value >>= 1;
++$numDigits;
}
return $numDigits;
}
/**
* Embeds basic patterns into a matrix.
*/
private static function embedBasicPatterns(Version $version, ByteMatrix $matrix) : void
{
self::embedPositionDetectionPatternsAndSeparators($matrix);
self::embedDarkDotAtLeftBottomCorner($matrix);
self::maybeEmbedPositionAdjustmentPatterns($version, $matrix);
self::embedTimingPatterns($matrix);
}
/**
* Embeds position detection patterns and separators into a byte matrix.
*/
private static function embedPositionDetectionPatternsAndSeparators(ByteMatrix $matrix) : void
{
$pdpWidth = count(self::POSITION_DETECTION_PATTERN[0]);
self::embedPositionDetectionPattern(0, 0, $matrix);
self::embedPositionDetectionPattern($matrix->getWidth() - $pdpWidth, 0, $matrix);
self::embedPositionDetectionPattern(0, $matrix->getWidth() - $pdpWidth, $matrix);
$hspWidth = 8;
self::embedHorizontalSeparationPattern(0, $hspWidth - 1, $matrix);
self::embedHorizontalSeparationPattern($matrix->getWidth() - $hspWidth, $hspWidth - 1, $matrix);
self::embedHorizontalSeparationPattern(0, $matrix->getWidth() - $hspWidth, $matrix);
$vspSize = 7;
self::embedVerticalSeparationPattern($vspSize, 0, $matrix);
self::embedVerticalSeparationPattern($matrix->getHeight() - $vspSize - 1, 0, $matrix);
self::embedVerticalSeparationPattern($vspSize, $matrix->getHeight() - $vspSize, $matrix);
}
/**
* Embeds a single position detection pattern into a byte matrix.
*/
private static function embedPositionDetectionPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
{
for ($y = 0; $y < 7; ++$y) {
for ($x = 0; $x < 7; ++$x) {
$matrix->set($xStart + $x, $yStart + $y, self::POSITION_DETECTION_PATTERN[$y][$x]);
}
}
}
private static function removePositionDetectionPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
{
for ($y = 0; $y < 7; ++$y) {
for ($x = 0; $x < 7; ++$x) {
$matrix->set($xStart + $x, $yStart + $y, 0);
}
}
}
/**
* Embeds a single horizontal separation pattern.
*
* @throws RuntimeException if a byte was already set
*/
private static function embedHorizontalSeparationPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
{
for ($x = 0; $x < 8; $x++) {
if (-1 !== $matrix->get($xStart + $x, $yStart)) {
throw new RuntimeException('Byte already set');
}
$matrix->set($xStart + $x, $yStart, 0);
}
}
/**
* Embeds a single vertical separation pattern.
*
* @throws RuntimeException if a byte was already set
*/
private static function embedVerticalSeparationPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
{
for ($y = 0; $y < 7; $y++) {
if (-1 !== $matrix->get($xStart, $yStart + $y)) {
throw new RuntimeException('Byte already set');
}
$matrix->set($xStart, $yStart + $y, 0);
}
}
/**
* Embeds a dot at the left bottom corner.
*
* @throws RuntimeException if a byte was already set to 0
*/
private static function embedDarkDotAtLeftBottomCorner(ByteMatrix $matrix) : void
{
if (0 === $matrix->get(8, $matrix->getHeight() - 8)) {
throw new RuntimeException('Byte already set to 0');
}
$matrix->set(8, $matrix->getHeight() - 8, 1);
}
/**
* Embeds position adjustment patterns if required.
*/
private static function maybeEmbedPositionAdjustmentPatterns(Version $version, ByteMatrix $matrix) : void
{
if ($version->getVersionNumber() < 2) {
return;
}
$index = $version->getVersionNumber() - 1;
$coordinates = self::POSITION_ADJUSTMENT_PATTERN_COORDINATE_TABLE[$index];
$numCoordinates = count($coordinates);
for ($i = 0; $i < $numCoordinates; ++$i) {
for ($j = 0; $j < $numCoordinates; ++$j) {
$y = $coordinates[$i];
$x = $coordinates[$j];
if (null === $x || null === $y) {
continue;
}
if (-1 === $matrix->get($x, $y)) {
self::embedPositionAdjustmentPattern($x - 2, $y - 2, $matrix);
}
}
}
}
/**
* Embeds a single position adjustment pattern.
*/
private static function embedPositionAdjustmentPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
{
for ($y = 0; $y < 5; $y++) {
for ($x = 0; $x < 5; $x++) {
$matrix->set($xStart + $x, $yStart + $y, self::POSITION_ADJUSTMENT_PATTERN[$y][$x]);
}
}
}
/**
* Embeds timing patterns into a matrix.
*/
private static function embedTimingPatterns(ByteMatrix $matrix) : void
{
$matrixWidth = $matrix->getWidth();
for ($i = 8; $i < $matrixWidth - 8; ++$i) {
$bit = ($i + 1) % 2;
if (-1 === $matrix->get($i, 6)) {
$matrix->set($i, 6, $bit);
}
if (-1 === $matrix->get(6, $i)) {
$matrix->set(6, $i, $bit);
}
}
}
/**
* Embeds "dataBits" using "getMaskPattern".
*
* For debugging purposes, it skips masking process if "getMaskPattern" is -1. See 8.7 of JISX0510:2004 (p.38) for
* how to embed data bits.
*
* @throws WriterException if not all bits could be consumed
*/
private static function embedDataBits(BitArray $dataBits, int $maskPattern, ByteMatrix $matrix) : void
{
$bitIndex = 0;
$direction = -1;
// Start from the right bottom cell.
$x = $matrix->getWidth() - 1;
$y = $matrix->getHeight() - 1;
while ($x > 0) {
// Skip vertical timing pattern.
if (6 === $x) {
--$x;
}
while ($y >= 0 && $y < $matrix->getHeight()) {
for ($i = 0; $i < 2; $i++) {
$xx = $x - $i;
// Skip the cell if it's not empty.
if (-1 !== $matrix->get($xx, $y)) {
continue;
}
if ($bitIndex < $dataBits->getSize()) {
$bit = $dataBits->get($bitIndex);
++$bitIndex;
} else {
// Padding bit. If there is no bit left, we'll fill the
// left cells with 0, as described in 8.4.9 of
// JISX0510:2004 (p. 24).
$bit = false;
}
// Skip masking if maskPattern is -1.
if (-1 !== $maskPattern && MaskUtil::getDataMaskBit($maskPattern, $xx, $y)) {
$bit = ! $bit;
}
$matrix->set($xx, $y, (int) $bit);
}
$y += $direction;
}
$direction = -$direction;
$y += $direction;
$x -= 2;
}
// All bits should be consumed
if ($dataBits->getSize() !== $bitIndex) {
throw new WriterException('Not all bits consumed (' . $bitIndex . ' out of ' . $dataBits->getSize() .')');
}
}
}
+108
View File
@@ -0,0 +1,108 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Encoder;
use BaconQrCode\Common\ErrorCorrectionLevel;
use BaconQrCode\Common\Mode;
use BaconQrCode\Common\Version;
/**
* QR code.
*/
final class QrCode
{
/**
* Number of possible mask patterns.
*/
public const NUM_MASK_PATTERNS = 8;
/**
* Mask pattern of the QR code.
*/
private int $maskPattern = -1;
/**
* Matrix of the QR code.
*/
private ByteMatrix $matrix;
public function __construct(
private readonly Mode $mode,
private readonly ErrorCorrectionLevel $errorCorrectionLevel,
private readonly Version $version,
int $maskPattern,
ByteMatrix $matrix
) {
$this->maskPattern = $maskPattern;
$this->matrix = $matrix;
}
/**
* Gets the mode.
*/
public function getMode() : Mode
{
return $this->mode;
}
/**
* Gets the EC level.
*/
public function getErrorCorrectionLevel() : ErrorCorrectionLevel
{
return $this->errorCorrectionLevel;
}
/**
* Gets the version.
*/
public function getVersion() : Version
{
return $this->version;
}
/**
* Gets the mask pattern.
*/
public function getMaskPattern() : int
{
return $this->maskPattern;
}
public function getMatrix(): ByteMatrix
{
return $this->matrix;
}
/**
* Validates whether a mask pattern is valid.
*/
public static function isValidMaskPattern(int $maskPattern) : bool
{
return $maskPattern > 0 && $maskPattern < self::NUM_MASK_PATTERNS;
}
/**
* Returns a string representation of the QR code.
*/
public function __toString() : string
{
$result = "<<\n"
. ' mode: ' . $this->mode . "\n"
. ' ecLevel: ' . $this->errorCorrectionLevel . "\n"
. ' version: ' . $this->version . "\n"
. ' maskPattern: ' . $this->maskPattern . "\n";
if ($this->matrix === null) {
$result .= " matrix: null\n";
} else {
$result .= " matrix:\n";
$result .= $this->matrix;
}
$result .= ">>\n";
return $result;
}
}
@@ -0,0 +1,10 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Exception;
use Throwable;
interface ExceptionInterface extends Throwable
{
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Exception;
final class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Exception;
final class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface
{
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Exception;
final class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Exception;
final class UnexpectedValueException extends \UnexpectedValueException implements ExceptionInterface
{
}
@@ -0,0 +1,8 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Exception;
final class WriterException extends \RuntimeException implements ExceptionInterface
{
}
+44
View File
@@ -0,0 +1,44 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Color;
use BaconQrCode\Exception;
final class Alpha implements ColorInterface
{
/**
* @param int $alpha the alpha value, 0 to 100
*/
public function __construct(private readonly int $alpha, private readonly ColorInterface $baseColor)
{
if ($alpha < 0 || $alpha > 100) {
throw new Exception\InvalidArgumentException('Alpha must be between 0 and 100');
}
}
public function getAlpha() : int
{
return $this->alpha;
}
public function getBaseColor() : ColorInterface
{
return $this->baseColor;
}
public function toRgb() : Rgb
{
return $this->baseColor->toRgb();
}
public function toCmyk() : Cmyk
{
return $this->baseColor->toCmyk();
}
public function toGray() : Gray
{
return $this->baseColor->toGray();
}
}
+82
View File
@@ -0,0 +1,82 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Color;
use BaconQrCode\Exception;
final class Cmyk implements ColorInterface
{
/**
* @param int $cyan the cyan amount, 0 to 100
* @param int $magenta the magenta amount, 0 to 100
* @param int $yellow the yellow amount, 0 to 100
* @param int $black the black amount, 0 to 100
*/
public function __construct(
private readonly int $cyan,
private readonly int $magenta,
private readonly int $yellow,
private readonly int $black
) {
if ($cyan < 0 || $cyan > 100) {
throw new Exception\InvalidArgumentException('Cyan must be between 0 and 100');
}
if ($magenta < 0 || $magenta > 100) {
throw new Exception\InvalidArgumentException('Magenta must be between 0 and 100');
}
if ($yellow < 0 || $yellow > 100) {
throw new Exception\InvalidArgumentException('Yellow must be between 0 and 100');
}
if ($black < 0 || $black > 100) {
throw new Exception\InvalidArgumentException('Black must be between 0 and 100');
}
}
public function getCyan() : int
{
return $this->cyan;
}
public function getMagenta() : int
{
return $this->magenta;
}
public function getYellow() : int
{
return $this->yellow;
}
public function getBlack() : int
{
return $this->black;
}
public function toRgb() : Rgb
{
$c = $this->cyan / 100;
$m = $this->magenta / 100;
$y = $this->yellow / 100;
$k = $this->black / 100;
return new Rgb(
(int) round(255 * (1 - $c) * (1 - $k)),
(int) round(255 * (1 - $m) * (1 - $k)),
(int) round(255 * (1 - $y) * (1 - $k))
);
}
public function toCmyk() : Cmyk
{
return $this;
}
public function toGray() : Gray
{
return $this->toRgb()->toGray();
}
}
@@ -0,0 +1,22 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Color;
interface ColorInterface
{
/**
* Converts the color to RGB.
*/
public function toRgb() : Rgb;
/**
* Converts the color to CMYK.
*/
public function toCmyk() : Cmyk;
/**
* Converts the color to gray.
*/
public function toGray() : Gray;
}
+42
View File
@@ -0,0 +1,42 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Color;
use BaconQrCode\Exception;
final class Gray implements ColorInterface
{
/**
* @param int $gray the gray value between 0 (black) and 100 (white)
*/
public function __construct(private readonly int $gray)
{
if ($gray < 0 || $gray > 100) {
throw new Exception\InvalidArgumentException('Gray must be between 0 and 100');
}
}
public function getGray() : int
{
return $this->gray;
}
public function toRgb() : Rgb
{
// use 255/100 instead of 2.55 to avoid floating-point precision loss (100 * 2.55 = 254.999...)
$value = (int) round($this->gray * 255 / 100);
return new Rgb($value, $value, $value);
}
public function toCmyk() : Cmyk
{
return new Cmyk(0, 0, 0, 100 - $this->gray);
}
public function toGray() : Gray
{
return $this;
}
}
+75
View File
@@ -0,0 +1,75 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Color;
use BaconQrCode\Exception;
final class Rgb implements ColorInterface
{
/**
* @param int $red the red amount of the color, 0 to 255
* @param int $green the green amount of the color, 0 to 255
* @param int $blue the blue amount of the color, 0 to 255
*/
public function __construct(private readonly int $red, private readonly int $green, private readonly int $blue)
{
if ($red < 0 || $red > 255) {
throw new Exception\InvalidArgumentException('Red must be between 0 and 255');
}
if ($green < 0 || $green > 255) {
throw new Exception\InvalidArgumentException('Green must be between 0 and 255');
}
if ($blue < 0 || $blue > 255) {
throw new Exception\InvalidArgumentException('Blue must be between 0 and 255');
}
}
public function getRed() : int
{
return $this->red;
}
public function getGreen() : int
{
return $this->green;
}
public function getBlue() : int
{
return $this->blue;
}
public function toRgb() : Rgb
{
return $this;
}
public function toCmyk() : Cmyk
{
// avoid division by zero with input rgb(0,0,0), by handling it as a specific case
if (0 === $this->red && 0 === $this->green && 0 === $this->blue) {
return new Cmyk(0, 0, 0, 100);
}
$c = 1 - ($this->red / 255);
$m = 1 - ($this->green / 255);
$y = 1 - ($this->blue / 255);
$k = min($c, $m, $y);
return new Cmyk(
(int) round(100 * ($c - $k) / (1 - $k)),
(int) round(100 * ($m - $k) / (1 - $k)),
(int) round(100 * ($y - $k) / (1 - $k)),
(int) round(100 * $k)
);
}
public function toGray() : Gray
{
// use integer-based calculation to avoid floating-point precision loss
return new Gray((int) round(($this->red * 2126 + $this->green * 7152 + $this->blue * 722) / 25500));
}
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Eye;
use BaconQrCode\Renderer\Path\Path;
/**
* Combines the style of two different eyes.
*/
final class CompositeEye implements EyeInterface
{
public function __construct(private readonly EyeInterface $externalEye, private readonly EyeInterface $internalEye)
{
}
public function getExternalPath() : Path
{
return $this->externalEye->getExternalPath();
}
public function getInternalPath() : Path
{
return $this->internalEye->getInternalPath();
}
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Eye;
use BaconQrCode\Renderer\Path\Path;
/**
* Interface for describing the look of an eye.
*/
interface EyeInterface
{
/**
* Returns the path of the external eye element.
*
* The path origin point (0, 0) must be anchored at the middle of the path.
*/
public function getExternalPath() : Path;
/**
* Returns the path of the internal eye element.
*
* The path origin point (0, 0) must be anchored at the middle of the path.
*/
public function getInternalPath() : Path;
}
@@ -0,0 +1,48 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Eye;
use BaconQrCode\Encoder\ByteMatrix;
use BaconQrCode\Renderer\Module\ModuleInterface;
use BaconQrCode\Renderer\Path\Path;
/**
* Renders an eye based on a module renderer.
*/
final class ModuleEye implements EyeInterface
{
public function __construct(private readonly ModuleInterface $module)
{
}
public function getExternalPath() : Path
{
$matrix = new ByteMatrix(7, 7);
for ($x = 0; $x < 7; ++$x) {
$matrix->set($x, 0, 1);
$matrix->set($x, 6, 1);
}
for ($y = 1; $y < 6; ++$y) {
$matrix->set(0, $y, 1);
$matrix->set(6, $y, 1);
}
return $this->module->createPath($matrix)->translate(-3.5, -3.5);
}
public function getInternalPath() : Path
{
$matrix = new ByteMatrix(3, 3);
for ($x = 0; $x < 3; ++$x) {
for ($y = 0; $y < 3; ++$y) {
$matrix->set($x, $y, 1);
}
}
return $this->module->createPath($matrix)->translate(-1.5, -1.5);
}
}
@@ -0,0 +1,56 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Eye;
use BaconQrCode\Renderer\Path\Path;
/**
* Renders the outer eye as solid with a curved corner and inner eye as a circle.
*/
final class PointyEye implements EyeInterface
{
/**
* @var self|null
*/
private static $instance;
private function __construct()
{
}
public static function instance() : self
{
return self::$instance ?: self::$instance = new self();
}
public function getExternalPath() : Path
{
return (new Path())
->move(-3.5, 3.5)
->line(-3.5, 0)
->ellipticArc(3.5, 3.5, 0, false, true, 0, -3.5)
->line(3.5, -3.5)
->line(3.5, 3.5)
->close()
->move(2.5, 0)
->ellipticArc(2.5, 2.5, 0, false, true, 0, 2.5)
->ellipticArc(2.5, 2.5, 0, false, true, -2.5, 0)
->ellipticArc(2.5, 2.5, 0, false, true, 0, -2.5)
->ellipticArc(2.5, 2.5, 0, false, true, 2.5, 0)
->close()
;
}
public function getInternalPath() : Path
{
return (new Path())
->move(1.5, 0)
->ellipticArc(1.5, 1.5, 0., false, true, 0., 1.5)
->ellipticArc(1.5, 1.5, 0., false, true, -1.5, 0.)
->ellipticArc(1.5, 1.5, 0., false, true, 0., -1.5)
->ellipticArc(1.5, 1.5, 0., false, true, 1.5, 0.)
->close()
;
}
}
@@ -0,0 +1,51 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Eye;
use BaconQrCode\Renderer\Path\Path;
/**
* Renders the inner eye as a circle.
*/
final class SimpleCircleEye implements EyeInterface
{
private static ?SimpleCircleEye $instance = null;
private function __construct()
{
}
public static function instance() : self
{
return self::$instance ?: self::$instance = new self();
}
public function getExternalPath() : Path
{
return (new Path())
->move(-3.5, -3.5)
->line(3.5, -3.5)
->line(3.5, 3.5)
->line(-3.5, 3.5)
->close()
->move(-2.5, -2.5)
->line(-2.5, 2.5)
->line(2.5, 2.5)
->line(2.5, -2.5)
->close()
;
}
public function getInternalPath() : Path
{
return (new Path())
->move(1.5, 0)
->ellipticArc(1.5, 1.5, 0., false, true, 0., 1.5)
->ellipticArc(1.5, 1.5, 0., false, true, -1.5, 0.)
->ellipticArc(1.5, 1.5, 0., false, true, 0., -1.5)
->ellipticArc(1.5, 1.5, 0., false, true, 1.5, 0.)
->close()
;
}
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Eye;
use BaconQrCode\Renderer\Path\Path;
/**
* Renders the eyes in their default square shape.
*/
final class SquareEye implements EyeInterface
{
private static ?SquareEye $instance = null;
private function __construct()
{
}
public static function instance() : self
{
return self::$instance ?: self::$instance = new self();
}
public function getExternalPath() : Path
{
return (new Path())
->move(-3.5, -3.5)
->line(3.5, -3.5)
->line(3.5, 3.5)
->line(-3.5, 3.5)
->close()
->move(-2.5, -2.5)
->line(-2.5, 2.5)
->line(2.5, 2.5)
->line(2.5, -2.5)
->close()
;
}
public function getInternalPath() : Path
{
return (new Path())
->move(-1.5, -1.5)
->line(1.5, -1.5)
->line(1.5, 1.5)
->line(-1.5, 1.5)
->close()
;
}
}
@@ -0,0 +1,237 @@
<?php
declare(strict_types=1);
namespace BaconQrCode\Renderer;
use BaconQrCode\Encoder\ByteMatrix;
use BaconQrCode\Encoder\MatrixUtil;
use BaconQrCode\Encoder\QrCode;
use BaconQrCode\Exception\InvalidArgumentException;
use BaconQrCode\Exception\RuntimeException;
use BaconQrCode\Renderer\Color\Alpha;
use BaconQrCode\Renderer\Color\ColorInterface;
use BaconQrCode\Renderer\RendererStyle\EyeFill;
use BaconQrCode\Renderer\RendererStyle\Fill;
use GdImage;
final class GDLibRenderer implements RendererInterface
{
private ?GdImage $image;
/**
* @var array<string, int>
*/
private array $colors;
public function __construct(
private int $size,
private int $margin = 4,
private string $imageFormat = 'png',
private int $compressionQuality = 9,
private ?Fill $fill = null
) {
if (! extension_loaded('gd') || ! function_exists('gd_info')) {
throw new RuntimeException('You need to install the GD extension to use this back end');
}
if ($this->fill === null) {
$this->fill = Fill::default();
}
if ($this->fill->hasGradientFill()) {
throw new InvalidArgumentException('GDLibRenderer does not support gradients');
}
}
/**
* @throws InvalidArgumentException if matrix width doesn't match height
*/
public function render(QrCode $qrCode): string
{
$matrix = $qrCode->getMatrix();
$matrixSize = $matrix->getWidth();
if ($matrixSize !== $matrix->getHeight()) {
throw new InvalidArgumentException('Matrix must have the same width and height');
}
MatrixUtil::removePositionDetectionPatterns($matrix);
$this->newImage();
$this->draw($matrix);
return $this->renderImage();
}
private function newImage(): void
{
$img = imagecreatetruecolor($this->size, $this->size);
if ($img === false) {
throw new RuntimeException('Failed to create image of that size');
}
$this->image = $img;
imagealphablending($this->image, false);
imagesavealpha($this->image, true);
$bg = $this->getColor($this->fill->getBackgroundColor());
imagefilledrectangle($this->image, 0, 0, $this->size, $this->size, $bg);
imagealphablending($this->image, true);
}
private function draw(ByteMatrix $matrix): void
{
$matrixSize = $matrix->getWidth();
$pointsOnSide = $matrix->getWidth() + $this->margin * 2;
$pointInPx = $this->size / $pointsOnSide;
$this->drawEye(0, 0, $pointInPx, $this->fill->getTopLeftEyeFill());
$this->drawEye($matrixSize - 7, 0, $pointInPx, $this->fill->getTopRightEyeFill());
$this->drawEye(0, $matrixSize - 7, $pointInPx, $this->fill->getBottomLeftEyeFill());
$rows = $matrix->getArray()->toArray();
$color = $this->getColor($this->fill->getForegroundColor());
for ($y = 0; $y < $matrixSize; $y += 1) {
for ($x = 0; $x < $matrixSize; $x += 1) {
if (! $rows[$y][$x]) {
continue;
}
$points = $this->normalizePoints([
($this->margin + $x) * $pointInPx, ($this->margin + $y) * $pointInPx,
($this->margin + $x + 1) * $pointInPx, ($this->margin + $y) * $pointInPx,
($this->margin + $x + 1) * $pointInPx, ($this->margin + $y + 1) * $pointInPx,
($this->margin + $x) * $pointInPx, ($this->margin + $y + 1) * $pointInPx,
]);
imagefilledpolygon($this->image, $points, $color);
}
}
}
private function drawEye(int $xOffset, int $yOffset, float $pointInPx, EyeFill $eyeFill): void
{
$internalColor = $this->getColor($eyeFill->inheritsInternalColor()
? $this->fill->getForegroundColor()
: $eyeFill->getInternalColor());
$externalColor = $this->getColor($eyeFill->inheritsExternalColor()
? $this->fill->getForegroundColor()
: $eyeFill->getExternalColor());
for ($y = 0; $y < 7; $y += 1) {
for ($x = 0; $x < 7; $x += 1) {
if ((($y === 1 || $y === 5) && $x > 0 && $x < 6) || (($x === 1 || $x === 5) && $y > 0 && $y < 6)) {
continue;
}
$points = $this->normalizePoints([
($this->margin + $x + $xOffset) * $pointInPx, ($this->margin + $y + $yOffset) * $pointInPx,
($this->margin + $x + $xOffset + 1) * $pointInPx, ($this->margin + $y + $yOffset) * $pointInPx,
($this->margin + $x + $xOffset + 1) * $pointInPx, ($this->margin + $y + $yOffset + 1) * $pointInPx,
($this->margin + $x + $xOffset) * $pointInPx, ($this->margin + $y + $yOffset + 1) * $pointInPx,
]);
if ($y > 1 && $y < 5 && $x > 1 && $x < 5) {
imagefilledpolygon($this->image, $points, $internalColor);
} else {
imagefilledpolygon($this->image, $points, $externalColor);
}
}
}
}
/**
* Normalize points will trim right and bottom line by 1 pixel.
* Otherwise pixels of neighbors are overlapping which leads to issue with transparency and small QR codes.
*/
private function normalizePoints(array $points): array
{
$maxX = $maxY = 0;
for ($i = 0; $i < count($points); $i += 2) {
// Do manual round as GD just removes decimal part
$points[$i] = $newX = round($points[$i]);
$points[$i + 1] = $newY = round($points[$i + 1]);
$maxX = max($maxX, $newX);
$maxY = max($maxY, $newY);
}
// Do trimming only if there are 4 points (8 coordinates), assumes this is square.
for ($i = 0; $i < count($points); $i += 2) {
$points[$i] = min($points[$i], $maxX - 1);
$points[$i + 1] = min($points[$i + 1], $maxY - 1);
}
return $points;
}
private function renderImage(): string
{
ob_start();
$quality = $this->compressionQuality;
switch ($this->imageFormat) {
case 'png':
if ($quality > 9 || $quality < 0) {
$quality = 9;
}
imagepng($this->image, null, $quality);
break;
case 'gif':
imagegif($this->image, null);
break;
case 'jpeg':
case 'jpg':
if ($quality > 100 || $quality < 0) {
$quality = 85;
}
imagejpeg($this->image, null, $quality);
break;
default:
ob_end_clean();
throw new InvalidArgumentException(
'Supported image formats are jpeg, png and gif, got: ' . $this->imageFormat
);
}
$this->colors = [];
$this->image = null;
return ob_get_clean();
}
private function getColor(ColorInterface $color): int
{
$alpha = 100;
if ($color instanceof Alpha) {
$alpha = $color->getAlpha();
$color = $color->getBaseColor();
}
$rgb = $color->toRgb();
$colorKey = sprintf('%02X%02X%02X%02X', $rgb->getRed(), $rgb->getGreen(), $rgb->getBlue(), $alpha);
if (! isset($this->colors[$colorKey])) {
$colorId = imagecolorallocatealpha(
$this->image,
$rgb->getRed(),
$rgb->getGreen(),
$rgb->getBlue(),
(int)((100 - $alpha) / 100 * 127) // Alpha for GD is in range 0 (opaque) - 127 (transparent)
);
if ($colorId === false) {
throw new RuntimeException('Failed to create color: #' . $colorKey);
}
$this->colors[$colorKey] = $colorId;
}
return $this->colors[$colorKey];
}
}
@@ -0,0 +1,373 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Image;
use BaconQrCode\Exception\RuntimeException;
use BaconQrCode\Renderer\Color\Alpha;
use BaconQrCode\Renderer\Color\Cmyk;
use BaconQrCode\Renderer\Color\ColorInterface;
use BaconQrCode\Renderer\Color\Gray;
use BaconQrCode\Renderer\Color\Rgb;
use BaconQrCode\Renderer\Path\Close;
use BaconQrCode\Renderer\Path\Curve;
use BaconQrCode\Renderer\Path\EllipticArc;
use BaconQrCode\Renderer\Path\Line;
use BaconQrCode\Renderer\Path\Move;
use BaconQrCode\Renderer\Path\Path;
use BaconQrCode\Renderer\RendererStyle\Gradient;
use BaconQrCode\Renderer\RendererStyle\GradientType;
final class EpsImageBackEnd implements ImageBackEndInterface
{
private const PRECISION = 3;
private ?string $eps;
public function new(int $size, ColorInterface $backgroundColor) : void
{
$this->eps = "%!PS-Adobe-3.0 EPSF-3.0\n"
. "%%Creator: BaconQrCode\n"
. sprintf("%%%%BoundingBox: 0 0 %d %d \n", $size, $size)
. "%%BeginProlog\n"
. "save\n"
. "50 dict begin\n"
. "/q { gsave } bind def\n"
. "/Q { grestore } bind def\n"
. "/s { scale } bind def\n"
. "/t { translate } bind def\n"
. "/r { rotate } bind def\n"
. "/n { newpath } bind def\n"
. "/m { moveto } bind def\n"
. "/l { lineto } bind def\n"
. "/c { curveto } bind def\n"
. "/z { closepath } bind def\n"
. "/f { eofill } bind def\n"
. "/rgb { setrgbcolor } bind def\n"
. "/cmyk { setcmykcolor } bind def\n"
. "/gray { setgray } bind def\n"
. "%%EndProlog\n"
. "1 -1 s\n"
. sprintf("0 -%d t\n", $size);
if ($backgroundColor instanceof Alpha && 0 === $backgroundColor->getAlpha()) {
return;
}
$this->eps .= wordwrap(
'0 0 m'
. sprintf(' %s 0 l', (string) $size)
. sprintf(' %s %s l', (string) $size, (string) $size)
. sprintf(' 0 %s l', (string) $size)
. ' z'
. ' ' .$this->getColorSetString($backgroundColor) . " f\n",
75,
"\n "
);
}
public function scale(float $size) : void
{
if (null === $this->eps) {
throw new RuntimeException('No image has been started');
}
$this->eps .= sprintf("%1\$s %1\$s s\n", round($size, self::PRECISION));
}
public function translate(float $x, float $y) : void
{
if (null === $this->eps) {
throw new RuntimeException('No image has been started');
}
$this->eps .= sprintf("%s %s t\n", round($x, self::PRECISION), round($y, self::PRECISION));
}
public function rotate(int $degrees) : void
{
if (null === $this->eps) {
throw new RuntimeException('No image has been started');
}
$this->eps .= sprintf("%d r\n", $degrees);
}
public function push() : void
{
if (null === $this->eps) {
throw new RuntimeException('No image has been started');
}
$this->eps .= "q\n";
}
public function pop() : void
{
if (null === $this->eps) {
throw new RuntimeException('No image has been started');
}
$this->eps .= "Q\n";
}
public function drawPathWithColor(Path $path, ColorInterface $color) : void
{
if (null === $this->eps) {
throw new RuntimeException('No image has been started');
}
$fromX = 0;
$fromY = 0;
$this->eps .= wordwrap(
'n '
. $this->drawPathOperations($path, $fromX, $fromY)
. ' ' . $this->getColorSetString($color) . " f\n",
75,
"\n "
);
}
public function drawPathWithGradient(
Path $path,
Gradient $gradient,
float $x,
float $y,
float $width,
float $height
) : void {
if (null === $this->eps) {
throw new RuntimeException('No image has been started');
}
$fromX = 0;
$fromY = 0;
$this->eps .= wordwrap(
'q n ' . $this->drawPathOperations($path, $fromX, $fromY) . "\n",
75,
"\n "
);
$this->createGradientFill($gradient, $x, $y, $width, $height);
}
public function done() : string
{
if (null === $this->eps) {
throw new RuntimeException('No image has been started');
}
$this->eps .= "%%TRAILER\nend restore\n%%EOF";
$blob = $this->eps;
$this->eps = null;
return $blob;
}
private function drawPathOperations(Iterable $ops, &$fromX, &$fromY) : string
{
$pathData = [];
foreach ($ops as $op) {
switch (true) {
case $op instanceof Move:
$fromX = $toX = round($op->getX(), self::PRECISION);
$fromY = $toY = round($op->getY(), self::PRECISION);
$pathData[] = sprintf('%s %s m', $toX, $toY);
break;
case $op instanceof Line:
$fromX = $toX = round($op->getX(), self::PRECISION);
$fromY = $toY = round($op->getY(), self::PRECISION);
$pathData[] = sprintf('%s %s l', $toX, $toY);
break;
case $op instanceof EllipticArc:
$pathData[] = $this->drawPathOperations($op->toCurves($fromX, $fromY), $fromX, $fromY);
break;
case $op instanceof Curve:
$x1 = round($op->getX1(), self::PRECISION);
$y1 = round($op->getY1(), self::PRECISION);
$x2 = round($op->getX2(), self::PRECISION);
$y2 = round($op->getY2(), self::PRECISION);
$fromX = $x3 = round($op->getX3(), self::PRECISION);
$fromY = $y3 = round($op->getY3(), self::PRECISION);
$pathData[] = sprintf('%s %s %s %s %s %s c', $x1, $y1, $x2, $y2, $x3, $y3);
break;
case $op instanceof Close:
$pathData[] = 'z';
break;
default:
throw new RuntimeException('Unexpected draw operation: ' . get_class($op));
}
}
return implode(' ', $pathData);
}
private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : void
{
$startColor = $gradient->getStartColor();
$endColor = $gradient->getEndColor();
if ($startColor instanceof Alpha) {
$startColor = $startColor->getBaseColor();
}
$startColorType = get_class($startColor);
if (! in_array($startColorType, [Rgb::class, Cmyk::class, Gray::class])) {
$startColorType = Cmyk::class;
$startColor = $startColor->toCmyk();
}
if (get_class($endColor) !== $startColorType) {
switch ($startColorType) {
case Cmyk::class:
$endColor = $endColor->toCmyk();
break;
case Rgb::class:
$endColor = $endColor->toRgb();
break;
case Gray::class:
$endColor = $endColor->toGray();
break;
}
}
$this->eps .= "eoclip\n<<\n";
if ($gradient->getType() === GradientType::RADIAL()) {
$this->eps .= " /ShadingType 3\n";
} else {
$this->eps .= " /ShadingType 2\n";
}
$this->eps .= " /Extend [ true true ]\n"
. " /AntiAlias true\n";
switch ($startColorType) {
case Cmyk::class:
$this->eps .= " /ColorSpace /DeviceCMYK\n";
break;
case Rgb::class:
$this->eps .= " /ColorSpace /DeviceRGB\n";
break;
case Gray::class:
$this->eps .= " /ColorSpace /DeviceGray\n";
break;
}
switch ($gradient->getType()) {
case GradientType::HORIZONTAL():
$this->eps .= sprintf(
" /Coords [ %s %s %s %s ]\n",
round($x, self::PRECISION),
round($y, self::PRECISION),
round($x + $width, self::PRECISION),
round($y, self::PRECISION)
);
break;
case GradientType::VERTICAL():
$this->eps .= sprintf(
" /Coords [ %s %s %s %s ]\n",
round($x, self::PRECISION),
round($y, self::PRECISION),
round($x, self::PRECISION),
round($y + $height, self::PRECISION)
);
break;
case GradientType::DIAGONAL():
$this->eps .= sprintf(
" /Coords [ %s %s %s %s ]\n",
round($x, self::PRECISION),
round($y, self::PRECISION),
round($x + $width, self::PRECISION),
round($y + $height, self::PRECISION)
);
break;
case GradientType::INVERSE_DIAGONAL():
$this->eps .= sprintf(
" /Coords [ %s %s %s %s ]\n",
round($x, self::PRECISION),
round($y + $height, self::PRECISION),
round($x + $width, self::PRECISION),
round($y, self::PRECISION)
);
break;
case GradientType::RADIAL():
$centerX = ($x + $width) / 2;
$centerY = ($y + $height) / 2;
$this->eps .= sprintf(
" /Coords [ %s %s 0 %s %s %s ]\n",
round($centerX, self::PRECISION),
round($centerY, self::PRECISION),
round($centerX, self::PRECISION),
round($centerY, self::PRECISION),
round(max($width, $height) / 2, self::PRECISION)
);
break;
}
$this->eps .= " /Function\n"
. " <<\n"
. " /FunctionType 2\n"
. " /Domain [ 0 1 ]\n"
. sprintf(" /C0 [ %s ]\n", $this->getColorString($startColor))
. sprintf(" /C1 [ %s ]\n", $this->getColorString($endColor))
. " /N 1\n"
. " >>\n>>\nshfill\nQ\n";
}
private function getColorSetString(ColorInterface $color) : string
{
if ($color instanceof Rgb) {
return $this->getColorString($color) . ' rgb';
}
if ($color instanceof Cmyk) {
return $this->getColorString($color) . ' cmyk';
}
if ($color instanceof Gray) {
return $this->getColorString($color) . ' gray';
}
return $this->getColorSetString($color->toCmyk());
}
private function getColorString(ColorInterface $color) : string
{
if ($color instanceof Rgb) {
return sprintf('%s %s %s', $color->getRed() / 255, $color->getGreen() / 255, $color->getBlue() / 255);
}
if ($color instanceof Cmyk) {
return sprintf(
'%s %s %s %s',
$color->getCyan() / 100,
$color->getMagenta() / 100,
$color->getYellow() / 100,
$color->getBlack() / 100
);
}
if ($color instanceof Gray) {
return sprintf('%s', $color->getGray() / 100);
}
return $this->getColorString($color->toCmyk());
}
}
@@ -0,0 +1,87 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Image;
use BaconQrCode\Exception\RuntimeException;
use BaconQrCode\Renderer\Color\ColorInterface;
use BaconQrCode\Renderer\Path\Path;
use BaconQrCode\Renderer\RendererStyle\Gradient;
/**
* Interface for back ends able to to produce path based images.
*/
interface ImageBackEndInterface
{
/**
* Starts a new image.
*
* If a previous image was already started, previous data get erased.
*/
public function new(int $size, ColorInterface $backgroundColor) : void;
/**
* Transforms all following drawing operation coordinates by scaling them by a given factor.
*
* @throws RuntimeException if no image was started yet.
*/
public function scale(float $size) : void;
/**
* Transforms all following drawing operation coordinates by translating them by a given amount.
*
* @throws RuntimeException if no image was started yet.
*/
public function translate(float $x, float $y) : void;
/**
* Transforms all following drawing operation coordinates by rotating them by a given amount.
*
* @throws RuntimeException if no image was started yet.
*/
public function rotate(int $degrees) : void;
/**
* Pushes the current coordinate transformation onto a stack.
*
* @throws RuntimeException if no image was started yet.
*/
public function push() : void;
/**
* Pops the last coordinate transformation from a stack.
*
* @throws RuntimeException if no image was started yet.
*/
public function pop() : void;
/**
* Draws a path with a given color.
*
* @throws RuntimeException if no image was started yet.
*/
public function drawPathWithColor(Path $path, ColorInterface $color) : void;
/**
* Draws a path with a given gradient which spans the box described by the position and size.
*
* @throws RuntimeException if no image was started yet.
*/
public function drawPathWithGradient(
Path $path,
Gradient $gradient,
float $x,
float $y,
float $width,
float $height
) : void;
/**
* Ends the image drawing operation and returns the resulting blob.
*
* This should reset the state of the back end and thus this method should only be callable once per image.
*
* @throws RuntimeException if no image was started yet.
*/
public function done() : string;
}
@@ -0,0 +1,327 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Image;
use BaconQrCode\Exception\RuntimeException;
use BaconQrCode\Renderer\Color\Alpha;
use BaconQrCode\Renderer\Color\Cmyk;
use BaconQrCode\Renderer\Color\ColorInterface;
use BaconQrCode\Renderer\Color\Gray;
use BaconQrCode\Renderer\Color\Rgb;
use BaconQrCode\Renderer\Path\Close;
use BaconQrCode\Renderer\Path\Curve;
use BaconQrCode\Renderer\Path\EllipticArc;
use BaconQrCode\Renderer\Path\Line;
use BaconQrCode\Renderer\Path\Move;
use BaconQrCode\Renderer\Path\Path;
use BaconQrCode\Renderer\RendererStyle\Gradient;
use BaconQrCode\Renderer\RendererStyle\GradientType;
use Imagick;
use ImagickDraw;
use ImagickPixel;
final class ImagickImageBackEnd implements ImageBackEndInterface
{
private string $imageFormat;
private int $compressionQuality;
private ?Imagick $image;
private ?ImagickDraw $draw;
private ?int $gradientCount;
/**
* @var TransformationMatrix[]|null
*/
private ?array $matrices;
private ?int $matrixIndex;
private bool $antialias;
public function __construct(string $imageFormat = 'png', int $compressionQuality = 100, bool $antialias = true)
{
if (! class_exists(Imagick::class)) {
throw new RuntimeException('You need to install the imagick extension to use this back end');
}
$this->imageFormat = $imageFormat;
$this->compressionQuality = $compressionQuality;
$this->antialias = $antialias;
}
public function new(int $size, ColorInterface $backgroundColor) : void
{
$this->image = new Imagick();
$this->image->newImage($size, $size, $this->getColorPixel($backgroundColor));
$this->image->setImageFormat($this->imageFormat);
$this->image->setCompressionQuality($this->compressionQuality);
$this->draw = new ImagickDraw();
if (! $this->antialias) {
$this->image->setAntiAlias(false);
$this->draw->setStrokeAntialias(false);
}
$this->gradientCount = 0;
$this->matrices = [new TransformationMatrix()];
$this->matrixIndex = 0;
}
public function scale(float $size) : void
{
if (null === $this->draw) {
throw new RuntimeException('No image has been started');
}
$this->draw->scale($size, $size);
$this->matrices[$this->matrixIndex] = $this->matrices[$this->matrixIndex]
->multiply(TransformationMatrix::scale($size));
}
public function translate(float $x, float $y) : void
{
if (null === $this->draw) {
throw new RuntimeException('No image has been started');
}
$this->draw->translate($x, $y);
$this->matrices[$this->matrixIndex] = $this->matrices[$this->matrixIndex]
->multiply(TransformationMatrix::translate($x, $y));
}
public function rotate(int $degrees) : void
{
if (null === $this->draw) {
throw new RuntimeException('No image has been started');
}
$this->draw->rotate($degrees);
$this->matrices[$this->matrixIndex] = $this->matrices[$this->matrixIndex]
->multiply(TransformationMatrix::rotate($degrees));
}
public function push() : void
{
if (null === $this->draw) {
throw new RuntimeException('No image has been started');
}
$this->draw->push();
$this->matrices[++$this->matrixIndex] = $this->matrices[$this->matrixIndex - 1];
}
public function pop() : void
{
if (null === $this->draw) {
throw new RuntimeException('No image has been started');
}
$this->draw->pop();
unset($this->matrices[$this->matrixIndex--]);
}
public function drawPathWithColor(Path $path, ColorInterface $color) : void
{
if (null === $this->draw) {
throw new RuntimeException('No image has been started');
}
$this->draw->setFillColor($this->getColorPixel($color));
$this->drawPath($path);
}
public function drawPathWithGradient(
Path $path,
Gradient $gradient,
float $x,
float $y,
float $width,
float $height
) : void {
if (null === $this->draw) {
throw new RuntimeException('No image has been started');
}
$this->draw->setFillPatternURL('#' . $this->createGradientFill($gradient, $x, $y, $width, $height));
$this->drawPath($path);
}
public function done() : string
{
if (null === $this->draw) {
throw new RuntimeException('No image has been started');
}
$this->image->drawImage($this->draw);
$blob = $this->image->getImageBlob();
$this->draw->clear();
$this->image->clear();
$this->draw = null;
$this->image = null;
$this->gradientCount = null;
return $blob;
}
private function drawPath(Path $path) : void
{
$this->draw->pathStart();
foreach ($path as $op) {
switch (true) {
case $op instanceof Move:
$this->draw->pathMoveToAbsolute($op->getX(), $op->getY());
break;
case $op instanceof Line:
$this->draw->pathLineToAbsolute($op->getX(), $op->getY());
break;
case $op instanceof EllipticArc:
$this->draw->pathEllipticArcAbsolute(
$op->getXRadius(),
$op->getYRadius(),
$op->getXAxisAngle(),
$op->isLargeArc(),
$op->isSweep(),
$op->getX(),
$op->getY()
);
break;
case $op instanceof Curve:
$this->draw->pathCurveToAbsolute(
$op->getX1(),
$op->getY1(),
$op->getX2(),
$op->getY2(),
$op->getX3(),
$op->getY3()
);
break;
case $op instanceof Close:
$this->draw->pathClose();
break;
default:
throw new RuntimeException('Unexpected draw operation: ' . get_class($op));
}
}
$this->draw->pathFinish();
}
private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : string
{
list($width, $height) = $this->matrices[$this->matrixIndex]->apply($width, $height);
$startColor = $this->getColorPixel($gradient->getStartColor())->getColorAsString();
$endColor = $this->getColorPixel($gradient->getEndColor())->getColorAsString();
$gradientImage = new Imagick();
switch ($gradient->getType()) {
case GradientType::HORIZONTAL():
$gradientImage->newPseudoImage((int) $height, (int) $width, sprintf(
'gradient:%s-%s',
$startColor,
$endColor
));
$gradientImage->rotateImage('transparent', -90);
break;
case GradientType::VERTICAL():
$gradientImage->newPseudoImage((int) $width, (int) $height, sprintf(
'gradient:%s-%s',
$startColor,
$endColor
));
break;
case GradientType::DIAGONAL():
case GradientType::INVERSE_DIAGONAL():
$gradientImage->newPseudoImage((int) ($width * sqrt(2)), (int) ($height * sqrt(2)), sprintf(
'gradient:%s-%s',
$startColor,
$endColor
));
if (GradientType::DIAGONAL() === $gradient->getType()) {
$gradientImage->rotateImage('transparent', -45);
} else {
$gradientImage->rotateImage('transparent', -135);
}
$rotatedWidth = $gradientImage->getImageWidth();
$rotatedHeight = $gradientImage->getImageHeight();
$gradientImage->setImagePage($rotatedWidth, $rotatedHeight, 0, 0);
$gradientImage->cropImage(
intdiv($rotatedWidth, 2) - 2,
intdiv($rotatedHeight, 2) - 2,
intdiv($rotatedWidth, 4) + 1,
intdiv($rotatedWidth, 4) + 1
);
break;
case GradientType::RADIAL():
$gradientImage->newPseudoImage((int) $width, (int) $height, sprintf(
'radial-gradient:%s-%s',
$startColor,
$endColor
));
break;
}
$id = sprintf('g%d', ++$this->gradientCount);
$this->draw->pushPattern($id, 0, 0, $width, $height);
$this->draw->composite(Imagick::COMPOSITE_COPY, 0, 0, $width, $height, $gradientImage);
$this->draw->popPattern();
return $id;
}
private function getColorPixel(ColorInterface $color) : ImagickPixel
{
$alpha = 100;
if ($color instanceof Alpha) {
$alpha = $color->getAlpha();
$color = $color->getBaseColor();
}
if ($color instanceof Rgb) {
return new ImagickPixel(sprintf(
'rgba(%d, %d, %d, %F)',
$color->getRed(),
$color->getGreen(),
$color->getBlue(),
$alpha / 100
));
}
if ($color instanceof Cmyk) {
return new ImagickPixel(sprintf(
'cmyka(%d, %d, %d, %d, %F)',
$color->getCyan(),
$color->getMagenta(),
$color->getYellow(),
$color->getBlack(),
$alpha / 100
));
}
if ($color instanceof Gray) {
return new ImagickPixel(sprintf(
'graya(%d%%, %F)',
$color->getGray(),
$alpha / 100
));
}
return $this->getColorPixel(new Alpha($alpha, $color->toRgb()));
}
}
@@ -0,0 +1,363 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Image;
use BaconQrCode\Exception\RuntimeException;
use BaconQrCode\Renderer\Color\Alpha;
use BaconQrCode\Renderer\Color\ColorInterface;
use BaconQrCode\Renderer\Path\Close;
use BaconQrCode\Renderer\Path\Curve;
use BaconQrCode\Renderer\Path\EllipticArc;
use BaconQrCode\Renderer\Path\Line;
use BaconQrCode\Renderer\Path\Move;
use BaconQrCode\Renderer\Path\Path;
use BaconQrCode\Renderer\RendererStyle\Gradient;
use BaconQrCode\Renderer\RendererStyle\GradientType;
use XMLWriter;
final class SvgImageBackEnd implements ImageBackEndInterface
{
private const PRECISION = 3;
private ?XMLWriter $xmlWriter;
private ?array $stack;
private ?int $currentStack;
private ?int $gradientCount;
public function __construct()
{
if (! class_exists(XMLWriter::class)) {
throw new RuntimeException(
'You need to install the libxml extension and enable the xmlwriter extension to use this back end'
);
}
}
public function new(int $size, ColorInterface $backgroundColor) : void
{
$this->xmlWriter = new XMLWriter();
$this->xmlWriter->openMemory();
$this->xmlWriter->startDocument('1.0', 'UTF-8');
$this->xmlWriter->startElement('svg');
$this->xmlWriter->writeAttribute('xmlns', 'http://www.w3.org/2000/svg');
$this->xmlWriter->writeAttribute('version', '1.1');
$this->xmlWriter->writeAttribute('width', (string) $size);
$this->xmlWriter->writeAttribute('height', (string) $size);
$this->xmlWriter->writeAttribute('viewBox', '0 0 '. $size . ' ' . $size);
$this->gradientCount = 0;
$this->currentStack = 0;
$this->stack[0] = 0;
$alpha = 1;
if ($backgroundColor instanceof Alpha) {
$alpha = $backgroundColor->getAlpha() / 100;
}
if (0 === $alpha) {
return;
}
$this->xmlWriter->startElement('rect');
$this->xmlWriter->writeAttribute('x', '0');
$this->xmlWriter->writeAttribute('y', '0');
$this->xmlWriter->writeAttribute('width', (string) $size);
$this->xmlWriter->writeAttribute('height', (string) $size);
$this->xmlWriter->writeAttribute('fill', $this->getColorString($backgroundColor));
if ($alpha < 1) {
$this->xmlWriter->writeAttribute('fill-opacity', (string) $alpha);
}
$this->xmlWriter->endElement();
}
public function scale(float $size) : void
{
if (null === $this->xmlWriter) {
throw new RuntimeException('No image has been started');
}
$this->xmlWriter->startElement('g');
$this->xmlWriter->writeAttribute(
'transform',
sprintf('scale(%s)', round($size, self::PRECISION))
);
++$this->stack[$this->currentStack];
}
public function translate(float $x, float $y) : void
{
if (null === $this->xmlWriter) {
throw new RuntimeException('No image has been started');
}
$this->xmlWriter->startElement('g');
$this->xmlWriter->writeAttribute(
'transform',
sprintf('translate(%s,%s)', round($x, self::PRECISION), round($y, self::PRECISION))
);
++$this->stack[$this->currentStack];
}
public function rotate(int $degrees) : void
{
if (null === $this->xmlWriter) {
throw new RuntimeException('No image has been started');
}
$this->xmlWriter->startElement('g');
$this->xmlWriter->writeAttribute('transform', sprintf('rotate(%d)', $degrees));
++$this->stack[$this->currentStack];
}
public function push() : void
{
if (null === $this->xmlWriter) {
throw new RuntimeException('No image has been started');
}
$this->xmlWriter->startElement('g');
$this->stack[] = 1;
++$this->currentStack;
}
public function pop() : void
{
if (null === $this->xmlWriter) {
throw new RuntimeException('No image has been started');
}
for ($i = 0; $i < $this->stack[$this->currentStack]; ++$i) {
$this->xmlWriter->endElement();
}
array_pop($this->stack);
--$this->currentStack;
}
public function drawPathWithColor(Path $path, ColorInterface $color) : void
{
if (null === $this->xmlWriter) {
throw new RuntimeException('No image has been started');
}
$alpha = 1;
if ($color instanceof Alpha) {
$alpha = $color->getAlpha() / 100;
}
$this->startPathElement($path);
$this->xmlWriter->writeAttribute('fill', $this->getColorString($color));
if ($alpha < 1) {
$this->xmlWriter->writeAttribute('fill-opacity', (string) $alpha);
}
$this->xmlWriter->endElement();
}
public function drawPathWithGradient(
Path $path,
Gradient $gradient,
float $x,
float $y,
float $width,
float $height
) : void {
if (null === $this->xmlWriter) {
throw new RuntimeException('No image has been started');
}
$gradientId = $this->createGradientFill($gradient, $x, $y, $width, $height);
$this->startPathElement($path);
$this->xmlWriter->writeAttribute('fill', 'url(#' . $gradientId . ')');
$this->xmlWriter->endElement();
}
public function done() : string
{
if (null === $this->xmlWriter) {
throw new RuntimeException('No image has been started');
}
foreach ($this->stack as $openElements) {
for ($i = $openElements; $i > 0; --$i) {
$this->xmlWriter->endElement();
}
}
$this->xmlWriter->endDocument();
$blob = $this->xmlWriter->outputMemory(true);
$this->xmlWriter = null;
$this->stack = null;
$this->currentStack = null;
$this->gradientCount = null;
return $blob;
}
private function startPathElement(Path $path) : void
{
$pathData = [];
foreach ($path as $op) {
switch (true) {
case $op instanceof Move:
$pathData[] = sprintf(
'M%s %s',
round($op->getX(), self::PRECISION),
round($op->getY(), self::PRECISION)
);
break;
case $op instanceof Line:
$pathData[] = sprintf(
'L%s %s',
round($op->getX(), self::PRECISION),
round($op->getY(), self::PRECISION)
);
break;
case $op instanceof EllipticArc:
$pathData[] = sprintf(
'A%s %s %s %u %u %s %s',
round($op->getXRadius(), self::PRECISION),
round($op->getYRadius(), self::PRECISION),
round($op->getXAxisAngle(), self::PRECISION),
$op->isLargeArc(),
$op->isSweep(),
round($op->getX(), self::PRECISION),
round($op->getY(), self::PRECISION)
);
break;
case $op instanceof Curve:
$pathData[] = sprintf(
'C%s %s %s %s %s %s',
round($op->getX1(), self::PRECISION),
round($op->getY1(), self::PRECISION),
round($op->getX2(), self::PRECISION),
round($op->getY2(), self::PRECISION),
round($op->getX3(), self::PRECISION),
round($op->getY3(), self::PRECISION)
);
break;
case $op instanceof Close:
$pathData[] = 'Z';
break;
default:
throw new RuntimeException('Unexpected draw operation: ' . get_class($op));
}
}
$this->xmlWriter->startElement('path');
$this->xmlWriter->writeAttribute('fill-rule', 'evenodd');
$this->xmlWriter->writeAttribute('d', implode('', $pathData));
}
private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : string
{
$this->xmlWriter->startElement('defs');
$startColor = $gradient->getStartColor();
$endColor = $gradient->getEndColor();
if ($gradient->getType() === GradientType::RADIAL()) {
$this->xmlWriter->startElement('radialGradient');
} else {
$this->xmlWriter->startElement('linearGradient');
}
$this->xmlWriter->writeAttribute('gradientUnits', 'userSpaceOnUse');
switch ($gradient->getType()) {
case GradientType::HORIZONTAL():
$this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
$this->xmlWriter->writeAttribute('y1', (string) round($y, self::PRECISION));
$this->xmlWriter->writeAttribute('x2', (string) round($x + $width, self::PRECISION));
$this->xmlWriter->writeAttribute('y2', (string) round($y, self::PRECISION));
break;
case GradientType::VERTICAL():
$this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
$this->xmlWriter->writeAttribute('y1', (string) round($y, self::PRECISION));
$this->xmlWriter->writeAttribute('x2', (string) round($x, self::PRECISION));
$this->xmlWriter->writeAttribute('y2', (string) round($y + $height, self::PRECISION));
break;
case GradientType::DIAGONAL():
$this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
$this->xmlWriter->writeAttribute('y1', (string) round($y, self::PRECISION));
$this->xmlWriter->writeAttribute('x2', (string) round($x + $width, self::PRECISION));
$this->xmlWriter->writeAttribute('y2', (string) round($y + $height, self::PRECISION));
break;
case GradientType::INVERSE_DIAGONAL():
$this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
$this->xmlWriter->writeAttribute('y1', (string) round($y + $height, self::PRECISION));
$this->xmlWriter->writeAttribute('x2', (string) round($x + $width, self::PRECISION));
$this->xmlWriter->writeAttribute('y2', (string) round($y, self::PRECISION));
break;
case GradientType::RADIAL():
$this->xmlWriter->writeAttribute('cx', (string) round(($x + $width) / 2, self::PRECISION));
$this->xmlWriter->writeAttribute('cy', (string) round(($y + $height) / 2, self::PRECISION));
$this->xmlWriter->writeAttribute('r', (string) round(max($width, $height) / 2, self::PRECISION));
break;
}
$toBeHashed = $this->getColorString($startColor) . $this->getColorString($endColor) . $gradient->getType();
if ($startColor instanceof Alpha) {
$toBeHashed .= (string) $startColor->getAlpha();
}
$id = sprintf('g%d-%s', ++$this->gradientCount, hash('xxh3', $toBeHashed));
$this->xmlWriter->writeAttribute('id', $id);
$this->xmlWriter->startElement('stop');
$this->xmlWriter->writeAttribute('offset', '0%');
$this->xmlWriter->writeAttribute('stop-color', $this->getColorString($startColor));
if ($startColor instanceof Alpha) {
$this->xmlWriter->writeAttribute('stop-opacity', (string) $startColor->getAlpha());
}
$this->xmlWriter->endElement();
$this->xmlWriter->startElement('stop');
$this->xmlWriter->writeAttribute('offset', '100%');
$this->xmlWriter->writeAttribute('stop-color', $this->getColorString($endColor));
if ($endColor instanceof Alpha) {
$this->xmlWriter->writeAttribute('stop-opacity', (string) $endColor->getAlpha());
}
$this->xmlWriter->endElement();
$this->xmlWriter->endElement();
$this->xmlWriter->endElement();
return $id;
}
private function getColorString(ColorInterface $color) : string
{
$color = $color->toRgb();
return sprintf(
'#%02x%02x%02x',
$color->getRed(),
$color->getGreen(),
$color->getBlue()
);
}
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Image;
final class TransformationMatrix
{
/**
* @var float[]
*/
private array $values;
public function __construct()
{
$this->values = [1, 0, 0, 1, 0, 0];
}
public function multiply(self $other) : self
{
$matrix = new self();
$matrix->values[0] = $this->values[0] * $other->values[0] + $this->values[2] * $other->values[1];
$matrix->values[1] = $this->values[1] * $other->values[0] + $this->values[3] * $other->values[1];
$matrix->values[2] = $this->values[0] * $other->values[2] + $this->values[2] * $other->values[3];
$matrix->values[3] = $this->values[1] * $other->values[2] + $this->values[3] * $other->values[3];
$matrix->values[4] = $this->values[0] * $other->values[4] + $this->values[2] * $other->values[5]
+ $this->values[4];
$matrix->values[5] = $this->values[1] * $other->values[4] + $this->values[3] * $other->values[5]
+ $this->values[5];
return $matrix;
}
public static function scale(float $size) : self
{
$matrix = new self();
$matrix->values = [$size, 0, 0, $size, 0, 0];
return $matrix;
}
public static function translate(float $x, float $y) : self
{
$matrix = new self();
$matrix->values = [1, 0, 0, 1, $x, $y];
return $matrix;
}
public static function rotate(int $degrees) : self
{
$matrix = new self();
$rad = deg2rad($degrees);
$matrix->values = [cos($rad), sin($rad), -sin($rad), cos($rad), 0, 0];
return $matrix;
}
/**
* Applies this matrix onto a point and returns the resulting viewport point.
*
* @return float[]
*/
public function apply(float $x, float $y) : array
{
return [
$x * $this->values[0] + $y * $this->values[2] + $this->values[4],
$x * $this->values[1] + $y * $this->values[3] + $this->values[5],
];
}
}
@@ -0,0 +1,150 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer;
use BaconQrCode\Encoder\MatrixUtil;
use BaconQrCode\Encoder\QrCode;
use BaconQrCode\Exception\InvalidArgumentException;
use BaconQrCode\Renderer\Image\ImageBackEndInterface;
use BaconQrCode\Renderer\Path\Path;
use BaconQrCode\Renderer\RendererStyle\EyeFill;
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
final class ImageRenderer implements RendererInterface
{
public function __construct(
private readonly RendererStyle $rendererStyle,
private readonly ImageBackEndInterface $imageBackEnd
) {
}
/**
* @throws InvalidArgumentException if matrix width doesn't match height
*/
public function render(QrCode $qrCode) : string
{
$size = $this->rendererStyle->getSize();
$margin = $this->rendererStyle->getMargin();
$matrix = $qrCode->getMatrix();
$matrixSize = $matrix->getWidth();
if ($matrixSize !== $matrix->getHeight()) {
throw new InvalidArgumentException('Matrix must have the same width and height');
}
$totalSize = $matrixSize + ($margin * 2);
$moduleSize = $size / $totalSize;
$fill = $this->rendererStyle->getFill();
$this->imageBackEnd->new($size, $fill->getBackgroundColor());
$this->imageBackEnd->scale((float) $moduleSize);
$this->imageBackEnd->translate((float) $margin, (float) $margin);
$module = $this->rendererStyle->getModule();
$moduleMatrix = clone $matrix;
MatrixUtil::removePositionDetectionPatterns($moduleMatrix);
$modulePath = $this->drawEyes($matrixSize, $module->createPath($moduleMatrix));
if ($fill->hasGradientFill()) {
$this->imageBackEnd->drawPathWithGradient(
$modulePath,
$fill->getForegroundGradient(),
0,
0,
$matrixSize,
$matrixSize
);
} else {
$this->imageBackEnd->drawPathWithColor($modulePath, $fill->getForegroundColor());
}
return $this->imageBackEnd->done();
}
private function drawEyes(int $matrixSize, Path $modulePath) : Path
{
$fill = $this->rendererStyle->getFill();
$eye = $this->rendererStyle->getEye();
$externalPath = $eye->getExternalPath();
$internalPath = $eye->getInternalPath();
$modulePath = $this->drawEye(
$externalPath,
$internalPath,
$fill->getTopLeftEyeFill(),
3.5,
3.5,
0,
$modulePath
);
$modulePath = $this->drawEye(
$externalPath,
$internalPath,
$fill->getTopRightEyeFill(),
$matrixSize - 3.5,
3.5,
90,
$modulePath
);
$modulePath = $this->drawEye(
$externalPath,
$internalPath,
$fill->getBottomLeftEyeFill(),
3.5,
$matrixSize - 3.5,
-90,
$modulePath
);
return $modulePath;
}
private function drawEye(
Path $externalPath,
Path $internalPath,
EyeFill $fill,
float $xTranslation,
float $yTranslation,
int $rotation,
Path $modulePath
) : Path {
if ($fill->inheritsBothColors()) {
return $modulePath
->append(
$externalPath->rotate($rotation)->translate($xTranslation, $yTranslation)
)
->append(
$internalPath->rotate($rotation)->translate($xTranslation, $yTranslation)
);
}
$this->imageBackEnd->push();
$this->imageBackEnd->translate($xTranslation, $yTranslation);
if (0 !== $rotation) {
$this->imageBackEnd->rotate($rotation);
}
if ($fill->inheritsExternalColor()) {
$modulePath = $modulePath->append(
$externalPath->rotate($rotation)->translate($xTranslation, $yTranslation)
);
} else {
$this->imageBackEnd->drawPathWithColor($externalPath, $fill->getExternalColor());
}
if ($fill->inheritsInternalColor()) {
$modulePath = $modulePath->append(
$internalPath->rotate($rotation)->translate($xTranslation, $yTranslation)
);
} else {
$this->imageBackEnd->drawPathWithColor($internalPath, $fill->getInternalColor());
}
$this->imageBackEnd->pop();
return $modulePath;
}
}
@@ -0,0 +1,56 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Module;
use BaconQrCode\Encoder\ByteMatrix;
use BaconQrCode\Exception\InvalidArgumentException;
use BaconQrCode\Renderer\Path\Path;
/**
* Renders individual modules as dots.
*/
final class DotsModule implements ModuleInterface
{
public const LARGE = 1;
public const MEDIUM = .8;
public const SMALL = .6;
public function __construct(private readonly float $size)
{
if ($size <= 0 || $size > 1) {
throw new InvalidArgumentException('Size must between 0 (exclusive) and 1 (inclusive)');
}
}
public function createPath(ByteMatrix $matrix) : Path
{
$width = $matrix->getWidth();
$height = $matrix->getHeight();
$path = new Path();
$halfSize = $this->size / 2;
$margin = (1 - $this->size) / 2;
for ($y = 0; $y < $height; ++$y) {
for ($x = 0; $x < $width; ++$x) {
if (! $matrix->get($x, $y)) {
continue;
}
$pathX = $x + $margin;
$pathY = $y + $margin;
$path = $path
->move($pathX + $this->size, $pathY + $halfSize)
->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX + $halfSize, $pathY + $this->size)
->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX, $pathY + $halfSize)
->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX + $halfSize, $pathY)
->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX + $this->size, $pathY + $halfSize)
->close()
;
}
}
return $path;
}
}
@@ -0,0 +1,82 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Module\EdgeIterator;
final class Edge
{
/**
* @var array<int[]>
*/
private array $points = [];
/**
* @var array<int[]>|null
*/
private ?array $simplifiedPoints = null;
private int $minX = PHP_INT_MAX;
private int $minY = PHP_INT_MAX;
private int $maxX = -1;
private int $maxY = -1;
public function __construct(private readonly bool $positive)
{
}
public function addPoint(int $x, int $y) : void
{
$this->points[] = [$x, $y];
$this->minX = min($this->minX, $x);
$this->minY = min($this->minY, $y);
$this->maxX = max($this->maxX, $x);
$this->maxY = max($this->maxY, $y);
}
public function isPositive() : bool
{
return $this->positive;
}
/**
* @return array<int[]>
*/
public function getPoints() : array
{
return $this->points;
}
public function getMaxX() : int
{
return $this->maxX;
}
public function getSimplifiedPoints() : array
{
if (null !== $this->simplifiedPoints) {
return $this->simplifiedPoints;
}
$points = [];
$length = count($this->points);
for ($i = 0; $i < $length; ++$i) {
$previousPoint = $this->points[(0 === $i ? $length : $i) - 1];
$nextPoint = $this->points[($length - 1 === $i ? -1 : $i) + 1];
$currentPoint = $this->points[$i];
if (($previousPoint[0] === $currentPoint[0] && $currentPoint[0] === $nextPoint[0])
|| ($previousPoint[1] === $currentPoint[1] && $currentPoint[1] === $nextPoint[1])
) {
continue;
}
$points[] = $currentPoint;
}
return $this->simplifiedPoints = $points;
}
}
@@ -0,0 +1,160 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Module\EdgeIterator;
use BaconQrCode\Encoder\ByteMatrix;
use IteratorAggregate;
use Traversable;
/**
* Edge iterator based on potrace.
*/
final class EdgeIterator implements IteratorAggregate
{
/**
* @var int[]
*/
private array $bytes = [];
private ?int $size;
private int $width;
private int $height;
public function __construct(ByteMatrix $matrix)
{
$this->bytes = iterator_to_array($matrix->getBytes());
$this->size = count($this->bytes);
$this->width = $matrix->getWidth();
$this->height = $matrix->getHeight();
}
/**
* @return Traversable<Edge>
*/
public function getIterator() : Traversable
{
$originalBytes = $this->bytes;
$point = $this->findNext(0, 0);
while (null !== $point) {
$edge = $this->findEdge($point[0], $point[1]);
$this->xorEdge($edge);
yield $edge;
$point = $this->findNext($point[0], $point[1]);
}
$this->bytes = $originalBytes;
}
/**
* @return int[]|null
*/
private function findNext(int $x, int $y) : ?array
{
$i = $this->width * $y + $x;
while ($i < $this->size && 1 !== $this->bytes[$i]) {
++$i;
}
if ($i < $this->size) {
return $this->pointOf($i);
}
return null;
}
private function findEdge(int $x, int $y) : Edge
{
$edge = new Edge($this->isSet($x, $y));
$startX = $x;
$startY = $y;
$dirX = 0;
$dirY = 1;
while (true) {
$edge->addPoint($x, $y);
$x += $dirX;
$y += $dirY;
if ($x === $startX && $y === $startY) {
break;
}
$left = $this->isSet($x + ($dirX + $dirY - 1 ) / 2, $y + ($dirY - $dirX - 1) / 2);
$right = $this->isSet($x + ($dirX - $dirY - 1) / 2, $y + ($dirY + $dirX - 1) / 2);
if ($right && ! $left) {
$tmp = $dirX;
$dirX = -$dirY;
$dirY = $tmp;
} elseif ($right) {
$tmp = $dirX;
$dirX = -$dirY;
$dirY = $tmp;
} elseif (! $left) {
$tmp = $dirX;
$dirX = $dirY;
$dirY = -$tmp;
}
}
return $edge;
}
private function xorEdge(Edge $path) : void
{
$points = $path->getPoints();
$y1 = $points[0][1];
$length = count($points);
$maxX = $path->getMaxX();
for ($i = 1; $i < $length; ++$i) {
$y = $points[$i][1];
if ($y === $y1) {
continue;
}
$x = $points[$i][0];
$minY = min($y1, $y);
for ($j = $x; $j < $maxX; ++$j) {
$this->flip($j, $minY);
}
$y1 = $y;
}
}
private function isSet(int $x, int $y) : bool
{
return (
$x >= 0
&& $x < $this->width
&& $y >= 0
&& $y < $this->height
) && 1 === $this->bytes[$this->width * $y + $x];
}
/**
* @return int[]
*/
private function pointOf(int $i) : array
{
$y = intdiv($i, $this->width);
return [$i - $y * $this->width, $y];
}
private function flip(int $x, int $y) : void
{
$this->bytes[$this->width * $y + $x] = (
$this->isSet($x, $y) ? 0 : 1
);
}
}

Some files were not shown because too many files have changed in this diff Show More