env and vendor added temporary

This commit is contained in:
Claudio 2025-04-09 16:39:53 +02:00
parent 35e90c283e
commit 43b1cb2202
10304 changed files with 1205060 additions and 254 deletions

40
.env Normal file
View File

@ -0,0 +1,40 @@
APP_ENV=production
APP_DEBUG=false
APP_KEY=base64:RWTN8ZDkeItU6xmobXjl6sRn8ph0XoAHgDpX4+wCdlE=
APP_URL=http://vanguard.test
LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST="localhost"
DB_DATABASE="yogiboook"
DB_USERNAME="solocla"
DB_PASSWORD="!Massarosa2"
DB_PREFIX="auth_"
BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_DRIVER=sync
SESSION_DRIVER=database
SESSION_LIFETIME=120
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=mail
MAIL_FROM_NAME=Vanguard
MAIL_FROM_ADDRESS=vanguard@test.dev
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

3
.gitignore vendored
View File

@ -3,7 +3,6 @@
/public/hot
/public/storage
/storage/*.key
/vendor
/.idea
/.fleet
/.vscode
@ -12,7 +11,6 @@ Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
.env
.phpunit.result.cache
.php_cs.cache
/documentation
@ -21,4 +19,3 @@ yarn-error.log
.env.backup
.env.production
auth.json
.env

View File

@ -0,0 +1,41 @@
<?php
include('include/headscript.php');
$dbHandler = DBHandlerSelect::getInstance();
$pdo = $dbHandler->getConnection();
// Verifica che iduserlogin sia definito
if (!isset($iduserlogin)) {
die("Errore: ID utente non definito.");
}
// Recupera i dati della scuola in base all'utente loggato
$stmt = $pdo->prepare("
SELECT id, name, website, email, phone, description, address_street, address_city, address_postal_code, address_province, address_country, logo, status
FROM schools
WHERE owner_id = ?
");
$stmt->execute([$iduserlogin]);
$school = $stmt->fetch();
if (!$school) {
die("Errore: Nessuna scuola trovata per l'utente loggato.");
}
$school_id = $school['id'];
$product_id = $_GET['product_id'] ?? 0;
// Verifica che il prodotto appartenga alla scuola
$stmt = $pdo->prepare("SELECT id FROM products WHERE id = ? AND school_id = ?");
$stmt->execute([$product_id, $school_id]);
if (!$stmt->fetch()) {
die("Errore: Prodotto non trovato o non autorizzato.");
}
$stmt = $pdo->prepare("SELECT class_type_id FROM product_class_types WHERE product_id = ?");
$stmt->execute([$product_id]);
$class_types = $stmt->fetchAll(PDO::FETCH_COLUMN);
header('Content-Type: application/json');
echo json_encode($class_types);

View File

@ -0,0 +1,696 @@
<?php
include('include/headscript.php');
// Connessione al database
$dbHandler = DBHandlerSelect::getInstance();
$pdo = $dbHandler->getConnection();
// Verifica che iduserlogin sia definito
if (!isset($iduserlogin)) {
die("Errore: ID utente non definito.");
}
// Recupera i dati della scuola in base all'utente loggato
$stmt = $pdo->prepare("
SELECT id, name, website, email, phone, description, address_street, address_city, address_postal_code, address_province, address_country, logo, status
FROM schools
WHERE owner_id = ?
");
$stmt->execute([$iduserlogin]);
$school = $stmt->fetch();
if (!$school) {
die("Errore: Nessuna scuola trovata per l'utente loggato.");
}
$school_id = $school['id'];
$school_name = $school['name'];
// Recupera i prodotti della scuola con classi e variazioni associate
$stmt = $pdo->prepare("
SELECT p.*,
GROUP_CONCAT(DISTINCT c.name) AS class_names,
GROUP_CONCAT(DISTINCT CONCAT(ct.level, ' (', ct.day_of_week, ')')) AS variation_names
FROM products p
LEFT JOIN product_class_types pct ON p.id = pct.product_id
LEFT JOIN class_types ct ON pct.class_type_id = ct.id
LEFT JOIN classes c ON ct.class_id = c.id
WHERE p.school_id = ?
GROUP BY p.id
ORDER BY p.created_at DESC
");
$stmt->execute([$school_id]);
$products = $stmt->fetchAll();
// Recupera tutte le classi e le loro variazioni per il modale di aggiunta/modifica prodotti
$stmt = $pdo->prepare("
SELECT c.id AS class_id, c.name AS class_name, ct.id AS class_type_id, ct.level, ct.day_of_week
FROM classes c
LEFT JOIN class_types ct ON c.id = ct.class_id
WHERE c.school_id = ?
ORDER BY c.name, ct.level, ct.day_of_week
");
$stmt->execute([$school_id]);
$class_data = $stmt->fetchAll();
// Organizza i dati in una struttura gerarchica: classi -> variazioni
$all_classes = [];
foreach ($class_data as $row) {
$class_id = $row['class_id'];
if (!isset($all_classes[$class_id])) {
$all_classes[$class_id] = [
'name' => $row['class_name'],
'variations' => []
];
}
if ($row['class_type_id']) {
$all_classes[$class_id]['variations'][] = [
'id' => $row['class_type_id'],
'name' => ucfirst($row['level']) . ' (' . ucfirst($row['day_of_week']) . ')'
];
}
}
// Gestione delle azioni (aggiunta, modifica, eliminazione)
$success_message = '';
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
// Aggiungi un prodotto
if ($action === 'add_product') {
$name = $_POST['name'] ?? '';
$type = $_POST['type'] ?? '';
$price = $_POST['price'] ?? 0;
$duration_days = $_POST['duration_days'] ?: null;
$max_entries = $_POST['max_entries'] ?: null;
$weekly_limit = $_POST['weekly_limit'] ?: null;
$max_recoveries = $_POST['max_recoveries'] ?: null;
$recovery_validity_days = $_POST['recovery_validity_days'] ?: null;
$allow_freeze = $_POST['allow_freeze'] ?? 0;
$freeze_max_days = $_POST['freeze_max_days'] ?: null;
$max_inventory = $_POST['max_inventory'] ?: null;
$class_types = $_POST['class_types'] ?? [];
if (empty($name) || empty($type) || $price <= 0) {
$error = "Nome, tipo e prezzo sono obbligatori.";
} else {
$current_inventory = $max_inventory; // All'inizio, l'inventario corrente è uguale al massimo
$stmt = $pdo->prepare("
INSERT INTO products (school_id, name, type, price, duration_days, max_entries, weekly_limit, max_recoveries, recovery_validity_days, allow_freeze, freeze_max_days, max_inventory, current_inventory)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$school_id,
$name,
$type,
$price,
$duration_days,
$max_entries,
$weekly_limit,
$max_recoveries,
$recovery_validity_days,
$allow_freeze,
$freeze_max_days,
$max_inventory,
$current_inventory
]);
$product_id = $pdo->lastInsertId();
// Associa le classi selezionate
if (!empty($class_types)) {
$stmt = $pdo->prepare("INSERT INTO product_class_types (product_id, class_type_id, entry_cost) VALUES (?, ?, 1)");
foreach ($class_types as $class_type_id) {
$stmt->execute([$product_id, $class_type_id]);
}
}
$success_message = "Prodotto aggiunto con successo!";
header("Location: products.php");
exit;
}
}
// Modifica un prodotto
if ($action === 'edit_product') {
$id = $_POST['id'] ?? 0;
$name = $_POST['name'] ?? '';
$type = $_POST['type'] ?? '';
$price = $_POST['price'] ?? 0;
$duration_days = $_POST['duration_days'] ?: null;
$max_entries = $_POST['max_entries'] ?: null;
$weekly_limit = $_POST['weekly_limit'] ?: null;
$max_recoveries = $_POST['max_recoveries'] ?: null;
$recovery_validity_days = $_POST['recovery_validity_days'] ?: null;
$allow_freeze = $_POST['allow_freeze'] ?? 0;
$freeze_max_days = $_POST['freeze_max_days'] ?: null;
$max_inventory = $_POST['max_inventory'] ?: null;
$current_inventory = $_POST['current_inventory'] ?: null;
$status = $_POST['status'] ?? 'active';
$class_types = $_POST['class_types'] ?? [];
if ($id <= 0 || empty($name) || empty($type) || $price <= 0) {
$error = "ID, nome, tipo e prezzo sono obbligatori.";
} else {
$stmt = $pdo->prepare("
UPDATE products
SET name = ?, type = ?, price = ?, duration_days = ?, max_entries = ?, weekly_limit = ?, max_recoveries = ?, recovery_validity_days = ?, allow_freeze = ?, freeze_max_days = ?, max_inventory = ?, current_inventory = ?, status = ?
WHERE id = ? AND school_id = ?
");
$stmt->execute([
$name,
$type,
$price,
$duration_days,
$max_entries,
$weekly_limit,
$max_recoveries,
$recovery_validity_days,
$allow_freeze,
$freeze_max_days,
$max_inventory,
$current_inventory,
$status,
$id,
$school_id
]);
// Aggiorna le classi associate
$stmt = $pdo->prepare("DELETE FROM product_class_types WHERE product_id = ?");
$stmt->execute([$id]);
if (!empty($class_types)) {
$stmt = $pdo->prepare("INSERT INTO product_class_types (product_id, class_type_id, entry_cost) VALUES (?, ?, 1)");
foreach ($class_types as $class_type_id) {
$stmt->execute([$id, $class_type_id]);
}
}
$success_message = "Prodotto modificato con successo!";
header("Location: products.php");
exit;
}
}
// Elimina un prodotto
if ($action === 'delete_product') {
$id = $_POST['id'] ?? 0;
if ($id <= 0) {
$error = "ID non valido.";
} else {
$stmt = $pdo->prepare("DELETE FROM products WHERE id = ? AND school_id = ?");
$stmt->execute([$id, $school_id]);
$success_message = "Prodotto eliminato con successo!";
header("Location: products.php");
exit;
}
}
}
?>
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--favicon-->
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<?php include('siteinfo.php'); ?>
</head>
<body>
<!--wrapper-->
<div class="wrapper">
<!--sidebar wrapper -->
<?php include('include/navbar.php'); ?>
<!--end sidebar wrapper -->
<!--start header -->
<?php include('include/topbar.php'); ?>
<!--end header -->
<!--start page wrapper -->
<div class="page-wrapper">
<div class="page-content">
<!-- Messaggi di successo o errore -->
<?php if ($success_message): ?>
<div class="alert alert-success" role="alert">
<?php echo htmlspecialchars($success_message); ?>
</div>
<?php endif; ?>
<?php if ($error): ?>
<div class="alert alert-danger" role="alert">
<?php echo htmlspecialchars($error); ?>
</div>
<?php endif; ?>
<div class="card radius-10">
<div class="card-header">
<div class="d-flex align-items-center">
<div>
<h6 class="mb-0">Prodotti</h6>
</div>
<div class="ms-auto">
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addProductModal">
<i class="bx bx-plus"></i> Aggiungi Prodotto
</button>
</div>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table id="productsTable" class="table table-striped table-bordered">
<thead>
<tr>
<th>Nome</th>
<th>Tipo</th>
<th>Prezzo</th>
<th>Durata (giorni)</th>
<th>Ingressi</th>
<th>Inventario</th>
<th>Classi Associate</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<?php if (empty($products)): ?>
<tr>
<td colspan="8" class="text-center">Nessun prodotto trovato.</td>
</tr>
<?php else: ?>
<?php foreach ($products as $product): ?>
<tr>
<td><?php echo htmlspecialchars($product['name']); ?></td>
<td>
<?php
$type_labels = [
'carnet' => 'Carnet',
'subscription' => 'Abbonamento',
'drop_in' => 'Lezione Singola'
];
echo $type_labels[$product['type']] ?? $product['type'];
?>
</td>
<td><?php echo number_format($product['price'], 2); ?> €</td>
<td><?php echo $product['duration_days'] ?? 'N/A'; ?></td>
<td><?php echo $product['max_entries'] ?? 'Illimitati'; ?></td>
<td>
<?php
if ($product['max_inventory'] !== null) {
echo htmlspecialchars($product['current_inventory']) . ' / ' . htmlspecialchars($product['max_inventory']);
} else {
echo 'Illimitato';
}
?>
</td>
<td>
<?php
$class_names = $product['class_names'] ?? '';
$variation_names = $product['variation_names'] ?? '';
if ($class_names || $variation_names) {
echo htmlspecialchars($class_names);
if ($variation_names) {
echo '<br><small>' . htmlspecialchars($variation_names) . '</small>';
}
} else {
echo 'Nessuna classe associata';
}
?>
</td>
<td>
<button type="button" class="btn btn-sm btn-warning" data-bs-toggle="modal" data-bs-target="#editProductModal"
onclick='fillEditProductModal(<?php echo json_encode([
"id" => $product['id'],
"name" => htmlspecialchars($product['name'], ENT_QUOTES),
"type" => $product['type'],
"price" => $product['price'],
"duration_days" => $product['duration_days'],
"max_entries" => $product['max_entries'],
"weekly_limit" => $product['weekly_limit'],
"max_recoveries" => $product['max_recoveries'],
"recovery_validity_days" => $product['recovery_validity_days'],
"allow_freeze" => $product['allow_freeze'],
"freeze_max_days" => $product['freeze_max_days'],
"max_inventory" => $product['max_inventory'],
"current_inventory" => $product['current_inventory'],
"status" => $product['status']
]); ?>)'
data-bs-toggle="tooltip" data-bs-placement="top" title="Modifica">
<i class="bx bx-edit"></i>
</button>
<form action="" method="POST" style="display:inline;" onsubmit="return confirm('Sei sicuro di voler eliminare questo prodotto?');">
<input type="hidden" name="action" value="delete_product">
<input type="hidden" name="id" value="<?php echo $product['id']; ?>">
<button type="submit" class="btn btn-sm btn-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="Elimina">
<i class="bx bx-trash"></i>
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Modale per aggiungere un prodotto -->
<div class="modal fade" id="addProductModal" tabindex="-1" aria-labelledby="addProductModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addProductModalLabel">Aggiungi Prodotto</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="" method="POST">
<div class="modal-body">
<input type="hidden" name="action" value="add_product">
<div class="row">
<div class="col-md-6 mb-3">
<label for="add_product_name" class="form-label">Nome</label>
<input type="text" class="form-control" id="add_product_name" name="name" required>
</div>
<div class="col-md-6 mb-3">
<label for="add_product_type" class="form-label">Tipo</label>
<select class="form-control" id="add_product_type" name="type" required>
<option value="carnet">Carnet</option>
<option value="subscription">Abbonamento</option>
<option value="drop_in">Lezione Singola</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label for="add_product_price" class="form-label">Prezzo ()</label>
<input type="number" step="0.01" class="form-control" id="add_product_price" name="price" required>
</div>
<div class="col-md-6 mb-3">
<label for="add_product_duration_days" class="form-label">Durata (giorni)</label>
<input type="number" class="form-control" id="add_product_duration_days" name="duration_days">
<small class="form-text text-muted">Lascia vuoto per lezioni singole.</small>
</div>
<div class="col-md-6 mb-3">
<label for="add_product_max_entries" class="form-label">Numero Massimo di Ingressi</label>
<input type="number" class="form-control" id="add_product_max_entries" name="max_entries">
<small class="form-text text-muted">Lascia vuoto per abbonamenti illimitati.</small>
</div>
<div class="col-md-6 mb-3">
<label for="add_product_weekly_limit" class="form-label">Limite Settimanale</label>
<input type="number" class="form-control" id="add_product_weekly_limit" name="weekly_limit">
<small class="form-text text-muted">Lascia vuoto se non applicabile.</small>
</div>
<div class="col-md-6 mb-3">
<label for="add_product_max_recoveries" class="form-label">Numero Massimo di Recuperi</label>
<input type="number" class="form-control" id="add_product_max_recoveries" name="max_recoveries">
<small class="form-text text-muted">Lascia vuoto se non applicabile.</small>
</div>
<div class="col-md-6 mb-3">
<label for="add_product_recovery_validity_days" class="form-label">Validità Recuperi (giorni)</label>
<input type="number" class="form-control" id="add_product_recovery_validity_days" name="recovery_validity_days">
<small class="form-text text-muted">Lascia vuoto se non applicabile.</small>
</div>
<div class="col-md-6 mb-3">
<label for="add_product_max_inventory" class="form-label">Inventario Massimo</label>
<input type="number" class="form-control" id="add_product_max_inventory" name="max_inventory">
<small class="form-text text-muted">Lascia vuoto per illimitato.</small>
</div>
<div class="col-md-6 mb-3">
<label for="add_product_allow_freeze" class="form-label">Permetti Congelamento</label>
<select class="form-control" id="add_product_allow_freeze" name="allow_freeze">
<option value="0">No</option>
<option value="1"></option>
</select>
</div>
<div class="col-md-6 mb-3">
<label for="add_product_freeze_max_days" class="form-label">Giorni Massimi di Congelamento</label>
<input type="number" class="form-control" id="add_product_freeze_max_days" name="freeze_max_days">
<small class="form-text text-muted">Lascia vuoto se non applicabile.</small>
</div>
<div class="col-md-12 mb-3">
<label class="form-label">Classi e Variazioni Associate</label>
<?php foreach ($all_classes as $class_id => $class): ?>
<div class="form-check">
<input class="form-check-input class-checkbox" type="checkbox" id="add_class_<?php echo $class_id; ?>" data-class-id="<?php echo $class_id; ?>">
<label class="form-check-label" for="add_class_<?php echo $class_id; ?>">
<?php echo htmlspecialchars($class['name']); ?>
</label>
<?php if (!empty($class['variations'])): ?>
<div class="ms-4">
<?php foreach ($class['variations'] as $variation): ?>
<div class="form-check">
<input class="form-check-input variation-checkbox" type="checkbox" name="class_types[]" value="<?php echo $variation['id']; ?>" id="add_variation_<?php echo $variation['id']; ?>" data-class-id="<?php echo $class_id; ?>">
<label class="form-check-label" for="add_variation_<?php echo $variation['id']; ?>">
<?php echo htmlspecialchars($variation['name']); ?>
</label>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Chiudi</button>
<button type="submit" class="btn btn-primary">Aggiungi</button>
</div>
</form>
</div>
</div>
</div>
<!-- Modale per modificare un prodotto -->
<div class="modal fade" id="editProductModal" tabindex="-1" aria-labelledby="editProductModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editProductModalLabel">Modifica Prodotto</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="" method="POST">
<div class="modal-body">
<input type="hidden" name="action" value="edit_product">
<input type="hidden" name="id" id="edit_product_id">
<div class="row">
<div class="col-md-6 mb-3">
<label for="edit_product_name" class="form-label">Nome</label>
<input type="text" class="form-control" id="edit_product_name" name="name" required>
</div>
<div class="col-md-6 mb-3">
<label for="edit_product_type" class="form-label">Tipo</label>
<select class="form-control" id="edit_product_type" name="type" required>
<option value="carnet">Carnet</option>
<option value="subscription">Abbonamento</option>
<option value="drop_in">Lezione Singola</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label for="edit_product_price" class="form-label">Prezzo ()</label>
<input type="number" step="0.01" class="form-control" id="edit_product_price" name="price" required>
</div>
<div class="col-md-6 mb-3">
<label for="edit_product_duration_days" class="form-label">Durata (giorni)</label>
<input type="number" class="form-control" id="edit_product_duration_days" name="duration_days">
</div>
<div class="col-md-6 mb-3">
<label for="edit_product_max_entries" class="form-label">Numero Massimo di Ingressi</label>
<input type="number" class="form-control" id="edit_product_max_entries" name="max_entries">
</div>
<div class="col-md-6 mb-3">
<label for="edit_product_weekly_limit" class="form-label">Limite Settimanale</label>
<input type="number" class="form-control" id="edit_product_weekly_limit" name="weekly_limit">
</div>
<div class="col-md-6 mb-3">
<label for="edit_product_max_recoveries" class="form-label">Numero Massimo di Recuperi</label>
<input type="number" class="form-control" id="edit_product_max_recoveries" name="max_recoveries">
</div>
<div class="col-md-6 mb-3">
<label for="edit_product_recovery_validity_days" class="form-label">Validità Recuperi (giorni)</label>
<input type="number" class="form-control" id="edit_product_recovery_validity_days" name="recovery_validity_days">
</div>
<div class="col-md-6 mb-3">
<label for="edit_product_max_inventory" class="form-label">Inventario Massimo</label>
<input type="number" class="form-control" id="edit_product_max_inventory" name="max_inventory">
</div>
<div class="col-md-6 mb-3">
<label for="edit_product_current_inventory" class="form-label">Inventario Corrente</label>
<input type="number" class="form-control" id="edit_product_current_inventory" name="current_inventory">
</div>
<div class="col-md-6 mb-3">
<label for="edit_product_allow_freeze" class="form-label">Permetti Congelamento</label>
<select class="form-control" id="edit_product_allow_freeze" name="allow_freeze">
<option value="0">No</option>
<option value="1"></option>
</select>
</div>
<div class="col-md-6 mb-3">
<label for="edit_product_freeze_max_days" class="form-label">Giorni Massimi di Congelamento</label>
<input type="number" class="form-control" id="edit_product_freeze_max_days" name="freeze_max_days">
</div>
<div class="col-md-6 mb-3">
<label for="edit_product_status" class="form-label">Stato</label>
<select class="form-control" id="edit_product_status" name="status">
<option value="active">Attivo</option>
<option value="inactive">Inattivo</option>
</select>
</div>
<div class="col-md-12 mb-3">
<label class="form-label">Classi e Variazioni Associate</label>
<?php foreach ($all_classes as $class_id => $class): ?>
<div class="form-check">
<input class="form-check-input class-checkbox" type="checkbox" id="edit_class_<?php echo $class_id; ?>" data-class-id="<?php echo $class_id; ?>">
<label class="form-check-label" for="edit_class_<?php echo $class_id; ?>">
<?php echo htmlspecialchars($class['name']); ?>
</label>
<?php if (!empty($class['variations'])): ?>
<div class="ms-4">
<?php foreach ($class['variations'] as $variation): ?>
<div class="form-check">
<input class="form-check-input variation-checkbox" type="checkbox" name="class_types[]" value="<?php echo $variation['id']; ?>" id="edit_variation_<?php echo $variation['id']; ?>" data-class-id="<?php echo $class_id; ?>">
<label class="form-check-label" for="edit_variation_<?php echo $variation['id']; ?>">
<?php echo htmlspecialchars($variation['name']); ?>
</label>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Chiudi</button>
<button type="submit" class="btn btn-primary">Salva</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<!--end page wrapper -->
<!--start overlay-->
<div class="overlay toggle-icon"></div>
<!--end overlay-->
<!--Start Back To Top Button-->
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<!--End Back To Top Button-->
<?php include('include/footer.php'); ?>
</div>
<!--end wrapper-->
<!-- search modal -->
<?php //include('include/searchmodal.php');
?>
<!-- end search modal -->
<!--start switcher-->
<?php //include('include/themeswitcher.php');
?>
<!--end switcher-->
<?php include('jsinclude.php'); ?>
<script>
$(document).ready(function() {
$('#productsTable').DataTable({
"language": {
"url": "//cdn.datatables.net/plug-ins/1.10.25/i18n/Italian.json"
}
});
// Inizializza i tooltip
$('[data-bs-toggle="tooltip"]').tooltip();
// Gestione selezione/deselezione delle classi e variazioni
$('.class-checkbox').on('change', function() {
const classId = $(this).data('class-id');
const isChecked = $(this).is(':checked');
// Seleziona/deseleziona tutte le variazioni della classe
$(`.variation-checkbox[data-class-id="${classId}"]`).prop('checked', isChecked);
});
$('.variation-checkbox').on('change', function() {
const classId = $(this).data('class-id');
const classCheckbox = $(`#add_class_${classId}, #edit_class_${classId}`);
const variations = $(`.variation-checkbox[data-class-id="${classId}"]`);
const allChecked = variations.length === variations.filter(':checked').length;
const someChecked = variations.filter(':checked').length > 0;
// Aggiorna lo stato della checkbox della classe
if (allChecked) {
classCheckbox.prop('checked', true).prop('indeterminate', false);
} else if (someChecked) {
classCheckbox.prop('checked', false).prop('indeterminate', true);
} else {
classCheckbox.prop('checked', false).prop('indeterminate', false);
}
});
});
function fillEditProductModal(data) {
document.getElementById('edit_product_id').value = data.id;
document.getElementById('edit_product_name').value = data.name;
document.getElementById('edit_product_type').value = data.type;
document.getElementById('edit_product_price').value = data.price;
document.getElementById('edit_product_duration_days').value = data.duration_days || '';
document.getElementById('edit_product_max_entries').value = data.max_entries || '';
document.getElementById('edit_product_weekly_limit').value = data.weekly_limit || '';
document.getElementById('edit_product_max_recoveries').value = data.max_recoveries || '';
document.getElementById('edit_product_recovery_validity_days').value = data.recovery_validity_days || '';
document.getElementById('edit_product_allow_freeze').value = data.allow_freeze;
document.getElementById('edit_product_freeze_max_days').value = data.freeze_max_days || '';
document.getElementById('edit_product_max_inventory').value = data.max_inventory || '';
document.getElementById('edit_product_current_inventory').value = data.current_inventory || '';
document.getElementById('edit_product_status').value = data.status;
// Recupera le classi associate al prodotto
fetch('get_product_classes.php?product_id=' + data.id)
.then(response => response.json())
.then(class_types => {
// Resetta tutte le checkbox
document.querySelectorAll('.variation-checkbox').forEach(checkbox => {
checkbox.checked = false;
});
document.querySelectorAll('.class-checkbox').forEach(checkbox => {
checkbox.checked = false;
checkbox.indeterminate = false;
});
// Seleziona le variazioni associate
document.querySelectorAll('.variation-checkbox').forEach(checkbox => {
if (class_types.includes(parseInt(checkbox.value))) {
checkbox.checked = true;
}
});
// Aggiorna lo stato delle checkbox delle classi
document.querySelectorAll('.class-checkbox').forEach(classCheckbox => {
const classId = classCheckbox.getAttribute('data-class-id');
const variations = document.querySelectorAll(`.variation-checkbox[data-class-id="${classId}"]`);
const checkedVariations = document.querySelectorAll(`.variation-checkbox[data-class-id="${classId}"]:checked`);
if (variations.length === checkedVariations.length && variations.length > 0) {
classCheckbox.checked = true;
classCheckbox.indeterminate = false;
} else if (checkedVariations.length > 0) {
classCheckbox.checked = false;
classCheckbox.indeterminate = true;
} else {
classCheckbox.checked = false;
classCheckbox.indeterminate = false;
}
});
});
}
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
; This file is for unifying the coding style for different editors and IDEs.
; More information at https://editorconfig.org
root = true
[*]
charset = utf-8
indent_size = 4
indent_style = space
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.yml]
indent_size = 2

View File

@ -0,0 +1,25 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text eol=lf
# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout.
*.c text
*.h text
# Declare files that will always have CRLF line endings on checkout.
*.sln text eol=crlf
# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary
*.otf binary
*.eot binary
*.svg binary
*.ttf binary
*.woff binary
*.woff2 binary
*.css linguist-vendored
*.scss linguist-vendored
*.js linguist-vendored
CHANGELOG.md export-ignore

View File

@ -0,0 +1,37 @@
name: Tests
on: [push, pull_request]
jobs:
tests:
name: PHP ${{ matrix.php }}
runs-on: ubuntu-latest
strategy:
matrix:
php: ['7.3', '7.4', '8.0', '8.1']
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Cache composer
uses: actions/cache@v1
with:
path: ~/.composer/cache/files
key: php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extension-csv: bcmath, ctype, dom, fileinfo, intl, gd, json, mbstring, pdo, pdo_sqlite, openssl, sqlite, xml, zip
coverage: none
- name: Install composer
run: composer install --no-interaction --no-scripts --no-suggest --prefer-source
- name: Execute tests
run: vendor/bin/phpunit

View File

@ -0,0 +1,9 @@
/.idea
/.history
/.vscode
/tests/databases
/vendor
.DS_Store
.phpunit.result.cache
composer.phar
composer.lock

View File

@ -0,0 +1,4 @@
preset: psr2
enabled:
- concat_with_spaces

View File

@ -0,0 +1,23 @@
The MIT License (MIT)
Copyright (c) 2015 Andreas Lutro
Copyright (c) 2017 Akaunting
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,186 @@
# Persistent settings package for Laravel
[![Downloads](https://poser.pugx.org/akaunting/laravel-setting/d/total.svg)](https://github.com/akaunting/laravel-setting)
[![StyleCI](https://styleci.io/repos/101231817/shield?style=flat&branch=master)](https://styleci.io/repos/101231817)
[![License](https://poser.pugx.org/akaunting/laravel-setting/license.svg)](LICENSE.md)
This package allows you to save settings in a more persistent way. You can use the database and/or json file to save your settings. You can also override the Laravel config.
* Driver support
* Helper function
* Blade directive
* Override config values
* Encryption
* Custom file, table and columns
* Auto save
* Extra columns
* Cache support
## Getting Started
### 1. Install
Run the following command:
```bash
composer require akaunting/laravel-setting
```
### 2. Register (for Laravel < 5.5)
Register the service provider in `config/app.php`
```php
Akaunting\Setting\Provider::class,
```
Add alias if you want to use the facade.
```php
'Setting' => Akaunting\Setting\Facade::class,
```
### 3. Publish
Publish config file.
```bash
php artisan vendor:publish --tag=setting
```
### 4. Database
Create table for database driver
```bash
php artisan migrate
```
### 5. Configure
You can change the options of your app from `config/setting.php` file
## Usage
You can either use the helper method like `setting('foo')` or the facade `Setting::get('foo')`
### Facade
```php
Setting::get('foo', 'default');
Setting::get('nested.element');
Setting::set('foo', 'bar');
Setting::forget('foo');
$settings = Setting::all();
```
### Helper
```php
setting('foo', 'default');
setting('nested.element');
setting(['foo' => 'bar']);
setting()->forget('foo');
$settings = setting()->all();
```
You can call the `save()` method to save the changes.
### Auto Save
If you enable the `auto_save` option in the config file, settings will be saved automatically every time the application shuts down if anything has been changed.
### Blade Directive
You can get the settings directly in your blade templates using the helper method or the blade directive like `@setting('foo')`
### Override Config Values
You can easily override default config values by adding them to the `override` option in `config/setting.php`, thereby eliminating the need to modify the default config files and also allowing you to change said values during production. Ex:
```php
'override' => [
"app.name" => "app_name",
"app.env" => "app_env",
"mail.driver" => "app_mail_driver",
"mail.host" => "app_mail_host",
],
```
The values on the left corresponds to the respective config value (Ex: config('app.name')) and the value on the right is the name of the `key` in your settings table/json file.
### Encryption
If you like to encrypt the values for a given key, you can pass the key to the `encrypted_keys` option in `config/setting.php` and the rest is automatically handled by using Laravel's built-in encryption facilities. Ex:
```php
'encrypted_keys' => [
"payment.key",
],
```
### JSON Storage
You can modify the path used on run-time using `setting()->setPath($path)`.
### Database Storage
If you want to use the database as settings storage then you should run the `php artisan migrate`. You can modify the table fields from the `create_settings_table` file in the migrations directory.
#### Extra Columns
If you want to store settings for multiple users/clients in the same database you can do so by specifying extra columns:
```php
setting()->setExtraColumns(['user_id' => Auth::user()->id]);
```
where `user_id = x` will now be added to the database query when settings are retrieved, and when new settings are saved, the `user_id` will be populated.
If you need more fine-tuned control over which data gets queried, you can use the `setConstraint` method which takes a closure with two arguments:
- `$query` is the query builder instance
- `$insert` is a boolean telling you whether the query is an insert or not. If it is an insert, you usually don't need to do anything to `$query`.
```php
setting()->setConstraint(function($query, $insert) {
if ($insert) return;
$query->where(/* ... */);
});
```
### Custom Drivers
This package uses the Laravel `Manager` class under the hood, so it's easy to add your own storage driver. All you need to do is extend the abstract `Driver` class, implement the abstract methods and call `setting()->extend`.
```php
class MyDriver extends Akaunting\Setting\Contracts\Driver
{
// ...
}
app('setting.manager')->extend('mydriver', function($app) {
return $app->make('MyDriver');
});
```
## Changelog
Please see [Releases](../../releases) for more information what has changed recently.
## Contributing
Pull requests are more than welcome. You must follow the PSR coding standards.
## Security
If you discover any security related issues, please email security@akaunting.com instead of using the issue tracker.
## Credits
- [Denis Duliçi](https://github.com/denisdulici)
- [All Contributors](../../contributors)
## License
The MIT License (MIT). Please see [LICENSE](LICENSE.md) for more information.

View File

@ -0,0 +1,48 @@
{
"name": "akaunting/laravel-setting",
"description": "Persistent settings package for Laravel",
"keywords": [
"laravel",
"persistent",
"settings",
"config"
],
"license": "MIT",
"authors": [
{
"name": "Denis Duliçi",
"email": "info@akaunting.com",
"homepage": "https://akaunting.com",
"role": "Developer"
}
],
"require": {
"php": ">=5.5.9",
"laravel/framework": ">=5.3"
},
"require-dev": {
"phpunit/phpunit": ">=4.8",
"mockery/mockery": "0.9.*",
"laravel/framework": ">=5.3"
},
"autoload": {
"psr-4": {
"Akaunting\\Setting\\": "./src"
},
"files": [
"src/helpers.php"
]
},
"extra": {
"laravel": {
"providers": [
"Akaunting\\Setting\\Provider"
],
"aliases": {
"Setting": "Akaunting\\Setting\\Facade"
}
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
>
<testsuites>
<testsuite name="Package Test Suite">
<directory suffix=".php">./tests/</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@ -0,0 +1,132 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Enable / Disable auto save
|--------------------------------------------------------------------------
|
| Auto-save every time the application shuts down
|
*/
'auto_save' => false,
/*
|--------------------------------------------------------------------------
| Cache
|--------------------------------------------------------------------------
|
| Options for caching. Set whether to enable cache, its key, time to live
| in seconds and whether to auto clear after save.
|
*/
'cache' => [
'enabled' => false,
'key' => 'setting',
'ttl' => 3600,
'auto_clear' => true,
],
/*
|--------------------------------------------------------------------------
| Setting driver
|--------------------------------------------------------------------------
|
| Select where to store the settings.
|
| Supported: "database", "json", "memory"
|
*/
'driver' => 'database',
/*
|--------------------------------------------------------------------------
| Database driver
|--------------------------------------------------------------------------
|
| Options for database driver. Enter which connection to use, null means
| the default connection. Set the table and column names.
|
*/
'database' => [
'connection' => null,
'table' => 'settings',
'key' => 'key',
'value' => 'value',
],
/*
|--------------------------------------------------------------------------
| JSON driver
|--------------------------------------------------------------------------
|
| Options for json driver. Enter the full path to the .json file.
|
*/
'json' => [
'path' => storage_path() . '/settings.json',
],
/*
|--------------------------------------------------------------------------
| Override application config values
|--------------------------------------------------------------------------
|
| If defined, settings package will override these config values.
|
| Sample:
| "app.locale" => "settings.locale",
|
*/
'override' => [
],
/*
|--------------------------------------------------------------------------
| Fallback
|--------------------------------------------------------------------------
|
| Define fallback settings to be used in case the default is null
|
| Sample:
| "currency" => "USD",
|
*/
'fallback' => [
],
/*
|--------------------------------------------------------------------------
| Required Extra Columns
|--------------------------------------------------------------------------
|
| The list of columns required to be set up
|
| Sample:
| "user_id",
| "tenant_id",
|
*/
'required_extra_columns' => [
],
/*
|--------------------------------------------------------------------------
| Encryption
|--------------------------------------------------------------------------
|
| Define the keys which should be crypt automatically.
|
| Sample:
| "payment.key"
|
*/
'encrypted_keys' => [
],
];

View File

@ -0,0 +1,321 @@
<?php
namespace Akaunting\Setting\Contracts;
use Akaunting\Setting\Support\Arr;
use Illuminate\Support\Facades\Cache;
abstract class Driver
{
/**
* The settings data.
*
* @var array
*/
protected $data = [];
/**
* Whether the store has changed since it was last loaded.
*
* @var bool
*/
protected $unsaved = false;
/**
* Whether the settings data are loaded.
*
* @var bool
*/
protected $loaded = false;
/**
* Include and merge with fallbacks
*
* @var bool
*/
protected $with_fallback = true;
/**
* Excludes fallback data
*/
public function withoutFallback()
{
$this->with_fallback = false;
return $this;
}
/**
* Get a specific key from the settings data.
*
* @param string|array $key
* @param mixed $default Optional default value.
*
* @return mixed
*/
public function get($key, $default = null)
{
if (!$this->checkExtraColumns()) {
return false;
}
$this->load();
return Arr::get($this->data, $key, $default);
}
/**
* Get the fallback value if default is null.
*
* @param string|array $key
* @param mixed $default
*
* @return mixed
*/
public function getFallback($key, $default = null)
{
if (($default !== null) || is_array($key)) {
return $default;
}
return Arr::get((array) config('setting.fallback'), $key);
}
/**
* Check if the given value is same as fallback.
*
* @param string $key
* @param string $value
*
* @return bool
*/
public function isEqualToFallback($key, $value)
{
return (string) $this->getFallback($key) == (string) $value;
}
/**
* Determine if a key exists in the settings data.
*
* @param string $key
*
* @return bool
*/
public function has($key)
{
if (!$this->checkExtraColumns()) {
return false;
}
$this->load();
return Arr::has($this->data, $key);
}
/**
* Set a specific key to a value in the settings data.
*
* @param string|array $key Key string or associative array of key => value
* @param mixed $value Optional only if the first argument is an array
*/
public function set($key, $value = null)
{
if (!$this->checkExtraColumns()) {
return;
}
$this->load();
$this->unsaved = true;
if (is_array($key)) {
foreach ($key as $k => $v) {
Arr::set($this->data, $k, $v);
}
} else {
Arr::set($this->data, $key, $value);
}
}
/**
* Unset a key in the settings data.
*
* @param string $key
*/
public function forget($key)
{
if (!$this->checkExtraColumns()) {
return;
}
$this->unsaved = true;
if ($this->has($key)) {
Arr::forget($this->data, $key);
}
}
/**
* Unset all keys in the settings data.
*
* @return void
*/
public function forgetAll()
{
if (!$this->checkExtraColumns()) {
return;
}
if (config('setting.cache.enabled')) {
Cache::forget($this->getCacheKey());
}
$this->unsaved = true;
$this->data = [];
}
/**
* Get all settings data.
*
* @return array|bool
*/
public function all()
{
if (!$this->checkExtraColumns()) {
return [];
}
$this->load();
return $this->data;
}
/**
* Save any changes done to the settings data.
*
* @return void
*/
public function save()
{
if (!$this->checkExtraColumns()) {
return;
}
if (!$this->unsaved) {
// either nothing has been changed, or data has not been loaded, so
// do nothing by returning early
return;
}
if (config('setting.cache.enabled') && config('setting.cache.auto_clear')) {
Cache::forget($this->getCacheKey());
}
$this->write($this->data);
$this->unsaved = false;
}
/**
* Make sure data is loaded.
*
* @param $force Force a reload of data. Default false.
*/
public function load($force = false)
{
if (!$this->checkExtraColumns()) {
return;
}
if ($this->loaded && !$force) {
return;
}
$fallback_data = $this->with_fallback ? config('setting.fallback') : [];
$driver_data = $this->readData();
$this->data = Arr::merge((array) $fallback_data, (array) $driver_data);
$this->loaded = true;
}
/**
* Read data from driver or cache
*
* @return array
*/
public function readData()
{
if (config('setting.cache.enabled')) {
return $this->readDataFromCache();
}
return $this->read();
}
/**
* Read data from cache
*
* @return array
*/
public function readDataFromCache()
{
return Cache::remember($this->getCacheKey(), config('setting.cache.ttl'), function () {
return $this->read();
});
}
/**
* Check if extra columns are set up.
*
* @return boolean
*/
public function checkExtraColumns()
{
if (!$required_extra_columns = config('setting.required_extra_columns')) {
return true;
}
if (array_keys_exists($required_extra_columns, $this->getExtraColumns())) {
return true;
}
return false;
}
/**
* Get cache key based on extra columns.
*
* @return string
*/
public function getCacheKey()
{
$key = config('setting.cache.key');
foreach ($this->getExtraColumns() as $name => $value) {
$key .= '_' . $name . '_' . $value;
}
return $key;
}
/**
* Get extra columns added to the rows.
*
* @return array
*/
abstract protected function getExtraColumns();
/**
* Read data from driver.
*
* @return array
*/
abstract protected function read();
/**
* Write data to driver.
*
* @param array $data
*
* @return void
*/
abstract protected function write(array $data);
}

View File

@ -0,0 +1,372 @@
<?php
namespace Akaunting\Setting\Drivers;
use Akaunting\Setting\Contracts\Driver;
use Akaunting\Setting\Support\Arr;
use Closure;
use Illuminate\Database\Connection;
use Illuminate\Support\Arr as LaravelArr;
use Illuminate\Support\Facades\Crypt;
class Database extends Driver
{
/**
* The database connection instance.
*
* @var \Illuminate\Database\Connection
*/
protected $connection;
/**
* The table to query from.
*
* @var string
*/
protected $table;
/**
* The key column name to query from.
*
* @var string
*/
protected $key;
/**
* The value column name to query from.
*
* @var string
*/
protected $value;
/**
* Keys which should be encrypt automatically.
*
* @var array
*/
protected $encrypted_keys;
/**
* Any query constraints that should be applied.
*
* @var Closure|null
*/
protected $query_constraint;
/**
* Any extra columns that should be added to the rows.
*
* @var array
*/
protected $extra_columns = [];
/**
* @param \Illuminate\Database\Connection $connection
* @param string $table
*/
public function __construct(Connection $connection, $table = null, $key = null, $value = null, array $encrypted_keys = [])
{
$this->connection = $connection;
$this->table = $table ?: 'settings';
$this->key = $key ?: 'key';
$this->value = $value ?: 'value';
$this->encrypted_keys = $encrypted_keys;
}
/**
* Set the table to query from.
*
* @param string $table
*/
public function setTable($table)
{
$this->table = $table;
}
/**
* Set the key column name to query from.
*
* @param string $key
*/
public function setKey($key)
{
$this->key = $key;
}
/**
* Set the value column name to query from.
*
* @param string $value
*/
public function setValue($value)
{
$this->value = $value;
}
/**
* Set the query constraint.
*
* @param Closure $callback
*/
public function setConstraint(Closure $callback)
{
$this->data = [];
$this->loaded = false;
$this->query_constraint = $callback;
}
/**
* Set extra columns to be added to the rows.
*
* @param array $columns
*/
public function setExtraColumns(array $columns)
{
$this->extra_columns = $columns;
}
/**
* Get extra columns added to the rows.
*
* @return array
*/
public function getExtraColumns()
{
return $this->extra_columns;
}
/**
* {@inheritdoc}
*/
public function forget($key)
{
parent::forget($key);
// because the database driver cannot store empty arrays, remove empty
// arrays to keep data consistent before and after saving
$segments = explode('.', $key);
array_pop($segments);
while ($segments) {
$segment = implode('.', $segments);
// non-empty array - exit out of the loop
if ($this->get($segment)) {
break;
}
// remove the empty array and move on to the next segment
$this->forget($segment);
array_pop($segments);
}
}
/**
* {@inheritdoc}
*/
protected function write(array $data)
{
// Get current data
$db_data = $this->newQuery()->get([$this->key, $this->value])->toArray();
$insert_data = LaravelArr::dot($data);
$update_data = [];
$delete_keys = [];
foreach ($db_data as $db_row) {
$key = $db_row->{$this->key};
$value = $db_row->{$this->value};
$is_in_insert = $is_different_in_db = $is_same_as_fallback = false;
if (isset($insert_data[$key])) {
$is_in_insert = true;
$is_different_in_db = (string) $insert_data[$key] != (string) $value;
$is_same_as_fallback = $this->isEqualToFallback($key, $insert_data[$key]);
}
if ($is_in_insert) {
if ($is_same_as_fallback) {
// Delete if new data is same as fallback
$delete_keys[] = $key;
} elseif ($is_different_in_db) {
// Update if new data is different from db
$update_data[$key] = $insert_data[$key];
}
} else {
// Delete if current db not available in new data
$delete_keys[] = $key;
}
unset($insert_data[$key]);
}
foreach ($update_data as $key => $value) {
$value = $this->prepareValue($key, $value);
$this->newQuery()
->where($this->key, '=', $key)
->update([$this->value => $value]);
}
if ($insert_data) {
$this->newQuery(true)
->insert($this->prepareInsertData($insert_data));
}
if ($delete_keys) {
$this->newQuery()
->whereIn($this->key, $delete_keys)
->delete();
}
}
/**
* Transforms settings data into an array ready to be insterted into the
* database. Call array_dot on a multidimensional array before passing it
* into this method!
*
* @param array $data Call array_dot on a multidimensional array before passing it into this method!
*
* @return array
*/
protected function prepareInsertData(array $data)
{
$db_data = [];
if ($this->getExtraColumns()) {
foreach ($data as $key => $value) {
$value = $this->prepareValue($key, $value);
// Don't insert if same as fallback
if ($this->isEqualToFallback($key, $value)) {
continue;
}
$db_data[] = array_merge(
$this->getExtraColumns(),
[$this->key => $key, $this->value => $value]
);
}
} else {
foreach ($data as $key => $value) {
$value = $this->prepareValue($key, $value);
// Don't insert if same as fallback
if ($this->isEqualToFallback($key, $value)) {
continue;
}
$db_data[] = [$this->key => $key, $this->value => $value];
}
}
return $db_data;
}
/**
* Checks if the provided key should be encrypted or not.
* Also type casts the given value to a string so errors with booleans or integers are handeled.
* Otherwise it returns the original value.
*
* @param string $key Key to check if it's inside the encryptedValues variable.
* @param mixed $value Info: Encryption only supports strings.
*
* @return string
*/
protected function prepareValue(string $key, $value)
{
// Check if key should be encrypted
if (in_array($key, $this->encrypted_keys)) {
// Cast to string to avoid error when a user passes a boolean value
return Crypt::encryptString((string) $value);
}
return $value;
}
/**
* Checks if the provided key should be decrypted or not.
* Otherwise it returns the original value.
*
* @param string $key Key to check if it's inside the encryptedValues variable.
* @param mixed $value Info: Encryption only supports strings.
*
* @return string
*/
protected function unpackValue(string $key, $value)
{
// Check if key should be encrypted
if (in_array($key, $this->encrypted_keys)) {
// Cast to string to avoid error when a user passes a boolean value
return Crypt::decryptString((string) $value);
}
return $value;
}
/**
* {@inheritdoc}
*/
protected function read()
{
return $this->parseReadData($this->newQuery()->get());
}
/**
* Parse data coming from the database.
*
* @param array $data
*
* @return array
*/
public function parseReadData($data)
{
$results = [];
foreach ($data as $row) {
if (is_array($row)) {
$key = $row[$this->key];
$value = $row[$this->value];
} elseif (is_object($row)) {
$key = $row->{$this->key};
$value = $row->{$this->value};
} else {
$msg = 'Expected array or object, got ' . gettype($row);
throw new \UnexpectedValueException($msg);
}
// Encryption
$value = $this->unpackValue($key, $value);
Arr::set($results, $key, $value);
}
return $results;
}
/**
* Create a new query builder instance.
*
* @param bool $insert
*
* @return \Illuminate\Database\Query\Builder
*/
protected function newQuery($insert = false)
{
$query = $this->connection->table($this->table);
if (!$insert) {
foreach ($this->getExtraColumns() as $key => $value) {
$query->where($key, '=', $value);
}
}
if ($this->query_constraint !== null) {
$callback = $this->query_constraint;
$callback($query, $insert);
}
return $query;
}
}

View File

@ -0,0 +1,80 @@
<?php
namespace Akaunting\Setting\Drivers;
use Akaunting\Setting\Contracts\Driver;
use Illuminate\Filesystem\Filesystem;
class Json extends Driver
{
/**
* @param \Illuminate\Filesystem\Filesystem $files
* @param string $path
*/
public function __construct(Filesystem $files, $path = null)
{
$this->files = $files;
$this->setPath($path ?: storage_path() . '/settings.json');
}
/**
* Set the path for the JSON file.
*
* @param string $path
*/
public function setPath($path)
{
// If the file does not already exist, we will attempt to create it.
if (!$this->files->exists($path)) {
$result = $this->files->put($path, '{}');
if ($result === false) {
throw new \InvalidArgumentException("Could not write to $path.");
}
}
if (!$this->files->isWritable($path)) {
throw new \InvalidArgumentException("$path is not writable.");
}
$this->path = $path;
}
/**
* {@inheritdoc}
*/
protected function getExtraColumns()
{
return [];
}
/**
* {@inheritdoc}
*/
protected function read()
{
$contents = $this->files->get($this->path);
$data = json_decode($contents, true);
if ($data === null) {
throw new \RuntimeException("Invalid JSON in {$this->path}");
}
return $data;
}
/**
* {@inheritdoc}
*/
protected function write(array $data)
{
if ($data) {
$contents = json_encode($data);
} else {
$contents = '{}';
}
$this->files->put($this->path, $contents);
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace Akaunting\Setting\Drivers;
use Akaunting\Setting\Contracts\Driver;
class Memory extends Driver
{
/**
* @param array $data
*/
public function __construct(array $data = null)
{
if ($data) {
$this->data = $data;
}
}
/**
* {@inheritdoc}
*/
protected function getExtraColumns()
{
return [];
}
/**
* {@inheritdoc}
*/
protected function read()
{
return $this->data;
}
/**
* {@inheritdoc}
*/
protected function write(array $data)
{
// do nothing
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Akaunting\Setting;
use Illuminate\Support\Facades\Facade as BaseFacade;
class Facade extends BaseFacade
{
/**
* Get the registered name of the component.
*/
public static function getFacadeAccessor()
{
return 'setting';
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace Akaunting\Setting;
use Akaunting\Setting\Drivers\Database;
use Akaunting\Setting\Drivers\Json;
use Akaunting\Setting\Drivers\Memory;
use Illuminate\Support\Manager as BaseManager;
class Manager extends BaseManager
{
/**
* The container instance.
*
* @var \Illuminate\Contracts\Container\Container
*/
protected $container;
/**
* The application instance.
*
* @param \Illuminate\Contracts\Foundation\Application $app
*/
public function __construct($app = null)
{
$this->container = $app ?? app();
parent::__construct($this->container);
}
public function getDefaultDriver()
{
return config('setting.driver');
}
public function createJsonDriver()
{
$path = config('setting.json.path');
return new Json($this->container['files'], $path);
}
public function createDatabaseDriver()
{
$connection = $this->container['db']->connection(config('setting.database.connection'));
$table = config('setting.database.table');
$key = config('setting.database.key');
$value = config('setting.database.value');
$encryptedKeys = config('setting.encrypted_keys');
return new Database($connection, $table, $key, $value, $encryptedKeys);
}
public function createMemoryDriver()
{
return new Memory();
}
public function createArrayDriver()
{
return $this->createMemoryDriver();
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Akaunting\Setting\Middleware;
use Closure;
class AutoSaveSetting
{
/**
* Create a new save settings middleware.
*/
public function __construct()
{
$this->setting = app('setting');
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
public function handle($request, Closure $next)
{
$response = $next($request);
$this->setting->save();
return $response;
}
}

View File

@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateSettingsTable extends Migration
{
/**
* Set up the options.
*/
public function __construct()
{
$this->table = config('setting.database.table');
$this->key = config('setting.database.key');
$this->value = config('setting.database.value');
}
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create($this->table, function (Blueprint $table) {
$table->increments('id');
$table->string($this->key)->index();
$table->text($this->value);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop($this->table);
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace Akaunting\Setting;
use Akaunting\Setting\Middleware\AutoSaveSetting;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Arr;
use Illuminate\View\Compilers\BladeCompiler;
class Provider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* @return void
*/
public function boot()
{
$this->publishes([
__DIR__ . '/Config/setting.php' => config_path('setting.php'),
__DIR__ . '/Migrations/2017_08_24_000000_create_settings_table.php' => database_path('migrations/2017_08_24_000000_create_settings_table.php'),
], 'setting');
// Auto save setting
if (config('setting.auto_save')) {
$kernel = $this->app['Illuminate\Contracts\Http\Kernel'];
$kernel->pushMiddleware(AutoSaveSetting::class);
}
$this->override();
// Register blade directive
$this->callAfterResolving('blade.compiler', function (BladeCompiler $compiler) {
$compiler->directive('setting', function ($expression) {
return "<?php echo setting($expression); ?>";
});
});
}
/**
* Register the application services.
*
* @return void
*/
public function register()
{
$this->app->singleton('setting.manager', function ($app) {
return new Manager($app);
});
$this->app->singleton('setting', function ($app) {
return $app['setting.manager']->driver();
});
$this->mergeConfigFrom(__DIR__ . '/Config/setting.php', 'setting');
}
private function override()
{
$override = config('setting.override', []);
foreach (Arr::dot($override) as $config_key => $setting_key) {
$config_key = is_string($config_key) ? $config_key : $setting_key;
try {
if (! is_null($value = setting($setting_key))) {
config([$config_key => $value]);
}
} catch (\Exception $e) {
continue;
}
}
}
}

View File

@ -0,0 +1,164 @@
<?php
namespace Akaunting\Setting\Support;
class Arr
{
/**
* This class is a static class and should not be instantiated.
*/
private function __construct()
{
//
}
/**
* Get an element from an array.
*
* @param array $data
* @param string $key Specify a nested element by separating keys with full stops.
* @param mixed $default If the element is not found, return this.
*
* @return mixed
*/
public static function get(array $data, $key, $default = null)
{
if ($key === null) {
return $data;
}
if (is_array($key)) {
return static::getArray($data, $key, $default);
}
foreach (explode('.', $key) as $segment) {
if (!is_array($data)) {
return $default;
}
if (!array_key_exists($segment, $data)) {
return $default;
}
$data = $data[$segment];
}
return $data;
}
protected static function getArray(array $input, $keys, $default = null)
{
$output = [];
foreach ($keys as $key) {
static::set($output, $key, static::get($input, $key, $default));
}
return $output;
}
/**
* Determine if an array has a given key.
*
* @param array $data
* @param string $key
*
* @return bool
*/
public static function has(array $data, $key)
{
foreach (explode('.', $key) as $segment) {
if (!is_array($data)) {
return false;
}
if (!array_key_exists($segment, $data)) {
return false;
}
$data = $data[$segment];
}
return true;
}
/**
* Set an element of an array.
*
* @param array $data
* @param string $key Specify a nested element by separating keys with full stops.
* @param mixed $value
*/
public static function set(array &$data, $key, $value)
{
$segments = explode('.', $key);
$key = array_pop($segments);
// iterate through all of $segments except the last one
foreach ($segments as $segment) {
if (!array_key_exists($segment, $data)) {
$data[$segment] = array();
} elseif (!is_array($data[$segment])) {
throw new \UnexpectedValueException('Non-array segment encountered');
}
$data = &$data[$segment];
}
$data[$key] = $value;
}
/**
* Unset an element from an array.
*
* @param array &$data
* @param string $key Specify a nested element by separating keys with full stops.
*/
public static function forget(array &$data, $key)
{
$segments = explode('.', $key);
$key = array_pop($segments);
// iterate through all of $segments except the last one
foreach ($segments as $segment) {
if (!array_key_exists($segment, $data)) {
return;
} elseif (!is_array($data[$segment])) {
throw new \UnexpectedValueException('Non-array segment encountered');
}
$data = &$data[$segment];
}
unset($data[$key]);
}
/**
* Merge two multidimensional arrays recursive
*
* @param array $array_1
* @param array $array_2
*
* @return array
*/
public static function merge(array $array_1, array $array_2)
{
$merged = $array_1;
foreach ($array_2 as $key => $value) {
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
$merged[$key] = static::merge($merged[$key], $value);
} elseif (is_numeric($key)) {
if (!in_array($value, $merged)) {
$merged[] = $value;
}
} else {
$merged[$key] = $value;
}
}
return $merged;
}
}

View File

@ -0,0 +1,45 @@
<?php
if (!function_exists('array_keys_exists')) {
/**
* Easily check if multiple array keys exist.
*
* @param array $keys
* @param array $arr
*
* @return boolean
*/
function array_keys_exists(array $keys, array $arr)
{
return !array_diff_key(array_flip($keys), $arr);
}
}
if (!function_exists('setting')) {
/**
* Get / set the specified setting value.
*
* If an array is passed as the key, we will assume you want to set an array of values.
*
* @param array|string $key
* @param mixed $default
*
* @return mixed
*/
function setting($key = null, $default = null)
{
$setting = app('setting');
if (is_null($key)) {
return $setting;
}
if (is_array($key)) {
$setting->set($key);
return $setting;
}
return $setting->get($key, $default);
}
}

View File

@ -0,0 +1,118 @@
<?php
use Akaunting\Setting\Drivers\Database;
abstract class AbstractFunctionalTest extends PHPUnit_Framework_TestCase
{
abstract protected function createStore(array $data = []);
protected function assertStoreEquals($store, $expected, $message = null)
{
$this->assertEquals($expected, $store->all(), $message);
$store->save();
$store = $this->createStore();
$this->assertEquals($expected, $store->all(), $message);
}
protected function assertStoreKeyEquals($store, $key, $expected, $message = null)
{
$this->assertEquals($expected, $store->get($key), $message);
$store->save();
$store = $this->createStore();
$this->assertEquals($expected, $store->get($key), $message);
}
/** @test */
public function store_is_initially_empty()
{
$store = $this->createStore();
$this->assertEquals([], $store->all());
}
/** @test */
public function written_changes_are_saved()
{
$store = $this->createStore();
$store->set('foo', 'bar');
$this->assertStoreKeyEquals($store, 'foo', 'bar');
}
/** @test */
public function nested_keys_are_nested()
{
$store = $this->createStore();
$store->set('foo.bar', 'baz');
$this->assertStoreEquals($store, ['foo' => ['bar' => 'baz']]);
}
/** @test */
public function cannot_set_nested_key_on_non_array_member()
{
$store = $this->createStore();
$store->set('foo', 'bar');
$this->setExpectedException('UnexpectedValueException', 'Non-array segment encountered');
$store->set('foo.bar', 'baz');
}
/** @test */
public function can_forget_key()
{
$store = $this->createStore();
$store->set('foo', 'bar');
$store->set('bar', 'baz');
$this->assertStoreEquals($store, ['foo' => 'bar', 'bar' => 'baz']);
$store->forget('foo');
$this->assertStoreEquals($store, ['bar' => 'baz']);
}
/** @test */
public function can_forget_nested_key()
{
$store = $this->createStore();
$store->set('foo.bar', 'baz');
$store->set('foo.baz', 'bar');
$store->set('bar.foo', 'baz');
$this->assertStoreEquals($store, [
'foo' => [
'bar' => 'baz',
'baz' => 'bar',
],
'bar' => [
'foo' => 'baz',
],
]);
$store->forget('foo.bar');
$this->assertStoreEquals($store, [
'foo' => [
'baz' => 'bar',
],
'bar' => [
'foo' => 'baz',
],
]);
$store->forget('bar.foo');
$expected = [
'foo' => [
'baz' => 'bar',
],
'bar' => [
],
];
if ($store instanceof Database) {
unset($expected['bar']);
}
$this->assertStoreEquals($store, $expected);
}
/** @test */
public function can_forget_all()
{
$store = $this->createStore(['foo' => 'bar']);
$this->assertStoreEquals($store, ['foo' => 'bar']);
$store->forgetAll();
$this->assertStoreEquals($store, []);
}
}

View File

@ -0,0 +1,43 @@
<?php
class DatabaseTest extends AbstractFunctionalTest
{
public function setUp()
{
$this->container = new \Illuminate\Container\Container();
$this->capsule = new \Illuminate\Database\Capsule\Manager($this->container);
$this->capsule->setAsGlobal();
$this->container['db'] = $this->capsule;
$this->capsule->addConnection([
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
]);
$this->capsule->schema()->create('settings', function ($t) {
$t->string('key', 64)->unique();
$t->string('value', 4096);
});
}
public function tearDown()
{
$this->capsule->schema()->drop('settings');
unset($this->capsule);
unset($this->container);
}
protected function createStore(array $data = [])
{
if ($data) {
$store = $this->createStore();
$store->set($data);
$store->save();
unset($store);
}
return new \Akaunting\Setting\Drivers\Database(
$this->capsule->getConnection()
);
}
}

View File

@ -0,0 +1,30 @@
<?php
class JsonTest extends AbstractFunctionalTest
{
protected function createStore(array $data = null)
{
$path = dirname(__DIR__) . '/tmp/store.json';
if ($data !== null) {
if ($data) {
$json = json_encode($data);
} else {
$json = '{}';
}
file_put_contents($path, $json);
}
return new \Akaunting\Setting\Drivers\Json(
new \Illuminate\Filesystem\Filesystem(),
$path
);
}
public function tearDown()
{
$path = dirname(__DIR__) . '/tmp/store.json';
unlink($path);
}
}

View File

@ -0,0 +1,21 @@
<?php
class MemoryTest extends AbstractFunctionalTest
{
protected function assertStoreEquals($store, $expected, $message = null)
{
$this->assertEquals($expected, $store->all(), $message);
// removed persistance test assertions
}
protected function assertStoreKeyEquals($store, $key, $expected, $message = null)
{
$this->assertEquals($expected, $store->get($key), $message);
// removed persistance test assertions
}
protected function createStore(array $data = null)
{
return new \Akaunting\Setting\Drivers\Memory($data);
}
}

View File

@ -0,0 +1,132 @@
<?php
use Akaunting\Setting\Support\Arr;
class ArrayUtilityTest extends PHPUnit_Framework_TestCase
{
/**
* @test
* @dataProvider getGetData
*/
public function getReturnsCorrectValue(array $data, $key, $expected)
{
$this->assertEquals($expected, Arr::get($data, $key));
}
public function getGetData()
{
return [
[[], 'foo', null],
[['foo' => 'bar'], 'foo', 'bar'],
[['foo' => 'bar'], 'bar', null],
[['foo' => 'bar'], 'foo.bar', null],
[['foo' => ['bar' => 'baz']], 'foo.bar', 'baz'],
[['foo' => ['bar' => 'baz']], 'foo.baz', null],
[['foo' => ['bar' => 'baz']], 'foo', ['bar' => 'baz']],
[
['foo' => 'bar', 'bar' => 'baz'],
['foo', 'bar'],
['foo' => 'bar', 'bar' => 'baz'],
],
[
['foo' => ['bar' => 'baz'], 'bar' => 'baz'],
['foo.bar', 'bar'],
['foo' => ['bar' => 'baz'], 'bar' => 'baz'],
],
[
['foo' => ['bar' => 'baz'], 'bar' => 'baz'],
['foo.bar'],
['foo' => ['bar' => 'baz']],
],
[
['foo' => ['bar' => 'baz'], 'bar' => 'baz'],
['foo.bar', 'baz'],
['foo' => ['bar' => 'baz'], 'baz' => null],
],
];
}
/**
* @test
* @dataProvider getSetData
*/
public function setSetsCorrectKeyToValue(array $input, $key, $value, array $expected)
{
Arr::set($input, $key, $value);
$this->assertEquals($expected, $input);
}
public function getSetData()
{
return [
[
['foo' => 'bar'],
'foo',
'baz',
['foo' => 'baz'],
],
[
[],
'foo',
'bar',
['foo' => 'bar'],
],
[
[],
'foo.bar',
'baz',
['foo' => ['bar' => 'baz']],
],
[
['foo' => ['bar' => 'baz']],
'foo.baz',
'foo',
['foo' => ['bar' => 'baz', 'baz' => 'foo']],
],
[
['foo' => ['bar' => 'baz']],
'foo.baz.bar',
'baz',
['foo' => ['bar' => 'baz', 'baz' => ['bar' => 'baz']]],
],
[
[],
'foo.bar.baz',
'foo',
['foo' => ['bar' => ['baz' => 'foo']]],
],
];
}
/** @test */
public function setThrowsExceptionOnNonArraySegment()
{
$data = ['foo' => 'bar'];
$this->setExpectedException('UnexpectedValueException', 'Non-array segment encountered');
Arr::set($data, 'foo.bar', 'baz');
}
/**
* @test
* @dataProvider getHasData
*/
public function hasReturnsCorrectly(array $input, $key, $expected)
{
$this->assertEquals($expected, Arr::has($input, $key));
}
public function getHasData()
{
return [
[[], 'foo', false],
[['foo' => 'bar'], 'foo', true],
[['foo' => 'bar'], 'bar', false],
[['foo' => 'bar'], 'foo.bar', false],
[['foo' => ['bar' => 'baz']], 'foo.bar', true],
[['foo' => ['bar' => 'baz']], 'foo.baz', false],
[['foo' => ['bar' => 'baz']], 'foo', true],
[['foo' => null], 'foo', true],
[['foo' => ['bar' => null]], 'foo.bar', true],
];
}
}

View File

@ -0,0 +1,107 @@
<?php
use Mockery as m;
class DatabaseDriverTest extends PHPUnit_Framework_TestCase
{
public function tearDown()
{
m::close();
}
/** @test */
public function correct_data_is_inserted_and_updated()
{
$connection = $this->mockConnection();
$query = $this->mockQuery($connection);
$query->shouldReceive('get')->once()->andReturn([
['key' => 'nest.one', 'value' => 'old'],
]);
$query->shouldReceive('lists')->atMost(1)->andReturn(['nest.one']);
$query->shouldReceive('pluck')->atMost(1)->andReturn(['nest.one']);
$dbData = $this->getDbData();
unset($dbData[1]); // remove the nest.one array member
$query->shouldReceive('where')->with('key', '=', 'nest.one')->andReturn(m::self())->getMock()
->shouldReceive('update')->with(['value' => 'nestone']);
$self = $this; // 5.3 compatibility
$query->shouldReceive('insert')->once()->andReturnUsing(function ($arg) use ($dbData, $self) {
$self->assertEquals(count($dbData), count($arg));
foreach ($dbData as $key => $value) {
$self->assertContains($value, $arg);
}
});
$store = $this->makeStore($connection);
$store->set('foo', 'bar');
$store->set('nest.one', 'nestone');
$store->set('nest.two', 'nesttwo');
$store->set('array', ['one', 'two']);
$store->save();
}
/** @test */
public function extra_columns_are_queried()
{
$connection = $this->mockConnection();
$query = $this->mockQuery($connection);
$query->shouldReceive('where')->once()->with('foo', '=', 'bar')
->andReturn(m::self())->getMock()
->shouldReceive('get')->once()->andReturn([
['key' => 'foo', 'value' => 'bar'],
]);
$store = $this->makeStore($connection);
$store->setExtraColumns(['foo' => 'bar']);
$this->assertEquals('bar', $store->get('foo'));
}
/** @test */
public function extra_columns_are_inserted()
{
$connection = $this->mockConnection();
$query = $this->mockQuery($connection);
$query->shouldReceive('where')->times(2)->with('extracol', '=', 'extradata')
->andReturn(m::self());
$query->shouldReceive('get')->once()->andReturn([]);
$query->shouldReceive('lists')->atMost(1)->andReturn([]);
$query->shouldReceive('pluck')->atMost(1)->andReturn([]);
$query->shouldReceive('insert')->once()->with([
['key' => 'foo', 'value' => 'bar', 'extracol' => 'extradata'],
]);
$store = $this->makeStore($connection);
$store->setExtraColumns(['extracol' => 'extradata']);
$store->set('foo', 'bar');
$store->save();
}
protected function getDbData()
{
return [
['key' => 'foo', 'value' => 'bar'],
['key' => 'nest.one', 'value' => 'nestone'],
['key' => 'nest.two', 'value' => 'nesttwo'],
['key' => 'array.0', 'value' => 'one'],
['key' => 'array.1', 'value' => 'two'],
];
}
protected function mockConnection()
{
return m::mock('Illuminate\Database\Connection');
}
protected function mockQuery($connection)
{
$query = m::mock('Illuminate\Database\Query\Builder');
$connection->shouldReceive('table')->andReturn($query);
return $query;
}
protected function makeStore($connection)
{
return new Akaunting\Setting\Drivers\Database($connection);
}
}

View File

@ -0,0 +1,51 @@
<?php
use Illuminate\Container\Container;
use Mockery as m;
class HelperTest extends PHPUnit_Framework_TestCase
{
public static $functions;
public function setUp()
{
self::$functions = m::mock();
Container::setInstance(new Container());
$store = m::mock('Akaunting\Setting\Contracts\Driver');
app()->bind('setting', function () use ($store) {
return $store;
});
}
/** @test */
public function helper_without_parameters_returns_store()
{
$this->assertInstanceOf('Akaunting\Setting\Contracts\Driver', setting());
}
/** @test */
public function single_parameter_get_a_key_from_store()
{
app('setting')->shouldReceive('get')->with('foo', null)->once();
setting('foo');
}
public function two_parameters_return_a_default_value()
{
app('setting')->shouldReceive('get')->with('foo', 'bar')->once();
setting('foo', 'bar');
}
/** @test */
public function array_parameter_call_set_method_into_store()
{
app('setting')->shouldReceive('set')->with(['foo', 'bar'])->once();
setting(['foo', 'bar']);
}
}

View File

@ -0,0 +1,60 @@
<?php
use Mockery as m;
class JsonDriverTest extends PHPUnit_Framework_TestCase
{
public function tearDown()
{
m::close();
}
protected function mockFilesystem()
{
return m::mock('Illuminate\Filesystem\Filesystem');
}
protected function makeStore($files, $path = 'fakepath')
{
return new Akaunting\Setting\Drivers\Json($files, $path);
}
/**
* @test
* @expectedException InvalidArgumentException
*/
public function throws_exception_when_file_not_writeable()
{
$files = $this->mockFilesystem();
$files->shouldReceive('exists')->once()->with('fakepath')->andReturn(true);
$files->shouldReceive('isWritable')->once()->with('fakepath')->andReturn(false);
$store = $this->makeStore($files);
}
/**
* @test
* @expectedException InvalidArgumentException
*/
public function throws_exception_when_files_put_fails()
{
$files = $this->mockFilesystem();
$files->shouldReceive('exists')->once()->with('fakepath')->andReturn(false);
$files->shouldReceive('put')->once()->with('fakepath', '{}')->andReturn(false);
$store = $this->makeStore($files);
}
/**
* @test
* @expectedException RuntimeException
*/
public function throws_exception_when_file_contains_invalid_json()
{
$files = $this->mockFilesystem();
$files->shouldReceive('exists')->once()->with('fakepath')->andReturn(true);
$files->shouldReceive('isWritable')->once()->with('fakepath')->andReturn(true);
$files->shouldReceive('get')->once()->with('fakepath')->andReturn('[[!1!11]');
$store = $this->makeStore($files);
$store->get('foo');
}
}

View File

@ -0,0 +1,4 @@
/vendor
composer.phar
composer.lock
.DS_Store

18
vendor/anhskohbo/no-captcha/.travis.yml vendored Normal file
View File

@ -0,0 +1,18 @@
language: php
dist: trusty
php:
- 5.5
- 5.6
- 7.0
- 7.1
- 7.2
before_script:
- travis_retry composer self-update
- travis_retry composer install --prefer-source --no-interaction --dev
script:
- composer install
- vendor/bin/phpunit

22
vendor/anhskohbo/no-captcha/LICENSE vendored Normal file
View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2014 Nguyễn Văn Ánh
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

197
vendor/anhskohbo/no-captcha/README.md vendored Normal file
View File

@ -0,0 +1,197 @@
No CAPTCHA reCAPTCHA
==========
[![Build Status](https://travis-ci.org/anhskohbo/no-captcha.svg?branch=master&style=flat-square)](https://travis-ci.org/anhskohbo/no-captcha)
[![Latest Stable Version](https://poser.pugx.org/anhskohbo/no-captcha/v/stable)](https://packagist.org/packages/anhskohbo/no-captcha)
[![Total Downloads](https://poser.pugx.org/anhskohbo/no-captcha/downloads)](https://packagist.org/packages/anhskohbo/no-captcha)
[![Latest Unstable Version](https://poser.pugx.org/anhskohbo/no-captcha/v/unstable)](https://packagist.org/packages/anhskohbo/no-captcha)
[![License](https://poser.pugx.org/anhskohbo/no-captcha/license)](https://packagist.org/packages/anhskohbo/no-captcha)
![recaptcha_anchor 2x](https://cloud.githubusercontent.com/assets/1529454/5291635/1c426412-7b88-11e4-8d16-46161a081ece.gif)
> For Laravel 4 use [v1](https://github.com/anhskohbo/no-captcha/tree/v1) branch.
## Installation
```
composer require anhskohbo/no-captcha
```
## Laravel 5 and above
### Setup
**_NOTE_** This package supports the auto-discovery feature of Laravel 5.5 and above, So skip these `Setup` instructions if you're using Laravel 5.5 and above.
In `app/config/app.php` add the following :
1- The ServiceProvider to the providers array :
```php
Anhskohbo\NoCaptcha\NoCaptchaServiceProvider::class,
```
2- The class alias to the aliases array :
```php
'NoCaptcha' => Anhskohbo\NoCaptcha\Facades\NoCaptcha::class,
```
3- Publish the config file
```ssh
php artisan vendor:publish --provider="Anhskohbo\NoCaptcha\NoCaptchaServiceProvider"
```
### Configuration
Add `NOCAPTCHA_SECRET` and `NOCAPTCHA_SITEKEY` in **.env** file :
```
NOCAPTCHA_SECRET=secret-key
NOCAPTCHA_SITEKEY=site-key
```
(You can obtain them from [here](https://www.google.com/recaptcha/admin))
### Usage
#### Init js source
With default options :
```php
{!! NoCaptcha::renderJs() !!}
```
With [language support](https://developers.google.com/recaptcha/docs/language) or [onloadCallback](https://developers.google.com/recaptcha/docs/display#explicit_render) option :
```php
{!! NoCaptcha::renderJs('fr', true, 'recaptchaCallback') !!}
```
#### Display reCAPTCHA
Default widget :
```php
{!! NoCaptcha::display() !!}
```
With [custom attributes](https://developers.google.com/recaptcha/docs/display#render_param) (theme, size, callback ...) :
```php
{!! NoCaptcha::display(['data-theme' => 'dark']) !!}
```
Invisible reCAPTCHA using a [submit button](https://developers.google.com/recaptcha/docs/invisible):
```php
{!! NoCaptcha::displaySubmit('my-form-id', 'submit now!', ['data-theme' => 'dark']) !!}
```
Notice that the id of the form is required in this method to let the autogenerated
callback submit the form on a successful captcha verification.
#### Validation
Add `'g-recaptcha-response' => 'required|captcha'` to rules array :
```php
$validate = Validator::make(Input::all(), [
'g-recaptcha-response' => 'required|captcha'
]);
```
##### Custom Validation Message
Add the following values to the `custom` array in the `validation` language file :
```php
'custom' => [
'g-recaptcha-response' => [
'required' => 'Please verify that you are not a robot.',
'captcha' => 'Captcha error! try again later or contact site admin.',
],
],
```
Then check for captcha errors in the `Form` :
```php
@if ($errors->has('g-recaptcha-response'))
<span class="help-block">
<strong>{{ $errors->first('g-recaptcha-response') }}</strong>
</span>
@endif
```
### Testing
When using the [Laravel Testing functionality](http://laravel.com/docs/5.5/testing), you will need to mock out the response for the captcha form element.
So for any form tests involving the captcha, you can do this by mocking the facade behavior:
```php
// prevent validation error on captcha
NoCaptcha::shouldReceive('verifyResponse')
->once()
->andReturn(true);
// provide hidden input for your 'required' validation
NoCaptcha::shouldReceive('display')
->zeroOrMoreTimes()
->andReturn('<input type="hidden" name="g-recaptcha-response" value="1" />');
```
You can then test the remainder of your form as normal.
When using HTTP tests you can add the `g-recaptcha-response` to the request body for the 'required' validation:
```php
// prevent validation error on captcha
NoCaptcha::shouldReceive('verifyResponse')
->once()
->andReturn(true);
// POST request, with request body including g-recaptcha-response
$response = $this->json('POST', '/register', [
'g-recaptcha-response' => '1',
'name' => 'John',
'email' => 'john@example.com',
'password' => '123456',
'password_confirmation' => '123456',
]);
```
## Without Laravel
Checkout example below:
```php
<?php
require_once "vendor/autoload.php";
$secret = 'CAPTCHA-SECRET';
$sitekey = 'CAPTCHA-SITEKEY';
$captcha = new \Anhskohbo\NoCaptcha\NoCaptcha($secret, $sitekey);
if (! empty($_POST)) {
var_dump($captcha->verifyResponse($_POST['g-recaptcha-response']));
exit();
}
?>
<form action="?" method="POST">
<?php echo $captcha->display(); ?>
<button type="submit">Submit</button>
</form>
<?php echo $captcha->renderJs(); ?>
```
## Contribute
https://github.com/anhskohbo/no-captcha/pulls

View File

@ -0,0 +1,43 @@
{
"name": "anhskohbo/no-captcha",
"description": "No CAPTCHA reCAPTCHA For Laravel.",
"keywords": [
"recaptcha",
"no-captcha",
"captcha",
"laravel",
"laravel4",
"laravel5",
"laravel6"
],
"license": "MIT",
"authors": [
{
"name": "anhskohbo",
"email": "anhskohbo@gmail.com"
}
],
"require": {
"php": ">=5.5.5",
"illuminate/support": "^5.0|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
"guzzlehttp/guzzle": "^6.2|^7.0"
},
"require-dev": {
"phpunit/phpunit": "~4.8|^9.5.10|^10.5"
},
"autoload": {
"psr-4": {
"Anhskohbo\\NoCaptcha\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"Anhskohbo\\NoCaptcha\\NoCaptchaServiceProvider"
],
"aliases": {
"NoCaptcha": "Anhskohbo\\NoCaptcha\\Facades\\NoCaptcha"
}
}
}
}

18
vendor/anhskohbo/no-captcha/phpunit.xml vendored Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
>
<testsuites>
<testsuite name="Package Test Suite">
<directory suffix=".php">./tests/</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@ -0,0 +1,18 @@
<?php
namespace Anhskohbo\NoCaptcha\Facades;
use Illuminate\Support\Facades\Facade;
class NoCaptcha extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'captcha';
}
}

View File

@ -0,0 +1,246 @@
<?php
namespace Anhskohbo\NoCaptcha;
use Symfony\Component\HttpFoundation\Request;
use GuzzleHttp\Client;
class NoCaptcha
{
const CLIENT_API = 'https://www.google.com/recaptcha/api.js';
const VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify';
/**
* The recaptcha secret key.
*
* @var string
*/
protected $secret;
/**
* The recaptcha sitekey key.
*
* @var string
*/
protected $sitekey;
/**
* @var \GuzzleHttp\Client
*/
protected $http;
/**
* The cached verified responses.
*
* @var array
*/
protected $verifiedResponses = [];
/**
* NoCaptcha.
*
* @param string $secret
* @param string $sitekey
* @param array $options
*/
public function __construct($secret, $sitekey, $options = [])
{
$this->secret = $secret;
$this->sitekey = $sitekey;
$this->http = new Client($options);
}
/**
* Render HTML captcha.
*
* @param array $attributes
*
* @return string
*/
public function display($attributes = [])
{
$attributes = $this->prepareAttributes($attributes);
return '<div' . $this->buildAttributes($attributes) . '></div>';
}
/**
* @see display()
*/
public function displayWidget($attributes = [])
{
return $this->display($attributes);
}
/**
* Display a Invisible reCAPTCHA by embedding a callback into a form submit button.
*
* @param string $formIdentifier the html ID of the form that should be submitted.
* @param string $text the text inside the form button
* @param array $attributes array of additional html elements
*
* @return string
*/
public function displaySubmit($formIdentifier, $text = 'submit', $attributes = [])
{
$javascript = '';
if (!isset($attributes['data-callback'])) {
$functionName = 'onSubmit' . str_replace(['-', '=', '\'', '"', '<', '>', '`'], '', $formIdentifier);
$attributes['data-callback'] = $functionName;
$javascript = sprintf(
'<script>function %s(){document.getElementById("%s").submit();}</script>',
$functionName,
$formIdentifier
);
}
$attributes = $this->prepareAttributes($attributes);
$button = sprintf('<button%s><span>%s</span></button>', $this->buildAttributes($attributes), $text);
return $button . $javascript;
}
/**
* Render js source
*
* @param null $lang
* @param bool $callback
* @param string $onLoadClass
* @return string
*/
public function renderJs($lang = null, $callback = false, $onLoadClass = 'onloadCallBack')
{
return '<script src="'.$this->getJsLink($lang, $callback, $onLoadClass).'" async defer></script>'."\n";
}
/**
* Verify no-captcha response.
*
* @param string $response
* @param string $clientIp
*
* @return bool
*/
public function verifyResponse($response, $clientIp = null)
{
if (empty($response)) {
return false;
}
// Return true if response already verfied before.
if (in_array($response, $this->verifiedResponses)) {
return true;
}
$verifyResponse = $this->sendRequestVerify([
'secret' => $this->secret,
'response' => $response,
'remoteip' => $clientIp,
]);
if (isset($verifyResponse['success']) && $verifyResponse['success'] === true) {
// A response can only be verified once from google, so we need to
// cache it to make it work in case we want to verify it multiple times.
$this->verifiedResponses[] = $response;
return true;
} else {
return false;
}
}
/**
* Verify no-captcha response by Symfony Request.
*
* @param Request $request
*
* @return bool
*/
public function verifyRequest(Request $request)
{
return $this->verifyResponse(
$request->get('g-recaptcha-response'),
$request->getClientIp()
);
}
/**
* Get recaptcha js link.
*
* @param string $lang
* @param boolean $callback
* @param string $onLoadClass
* @return string
*/
public function getJsLink($lang = null, $callback = false, $onLoadClass = 'onloadCallBack')
{
$client_api = static::CLIENT_API;
$params = [];
$callback ? $this->setCallBackParams($params, $onLoadClass) : false;
$lang ? $params['hl'] = $lang : null;
return $client_api . '?'. http_build_query($params);
}
/**
* @param $params
* @param $onLoadClass
*/
protected function setCallBackParams(&$params, $onLoadClass)
{
$params['render'] = 'explicit';
$params['onload'] = $onLoadClass;
}
/**
* Send verify request.
*
* @param array $query
*
* @return array
*/
protected function sendRequestVerify(array $query = [])
{
$response = $this->http->request('POST', static::VERIFY_URL, [
'form_params' => $query,
]);
return json_decode($response->getBody(), true);
}
/**
* Prepare HTML attributes and assure that the correct classes and attributes for captcha are inserted.
*
* @param array $attributes
*
* @return array
*/
protected function prepareAttributes(array $attributes)
{
$attributes['data-sitekey'] = $this->sitekey;
if (!isset($attributes['class'])) {
$attributes['class'] = '';
}
$attributes['class'] = trim('g-recaptcha ' . $attributes['class']);
return $attributes;
}
/**
* Build HTML attributes.
*
* @param array $attributes
*
* @return string
*/
protected function buildAttributes(array $attributes)
{
$html = [];
foreach ($attributes as $key => $value) {
$html[] = $key.'="'.$value.'"';
}
return count($html) ? ' '.implode(' ', $html) : '';
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace Anhskohbo\NoCaptcha;
use Illuminate\Support\ServiceProvider;
class NoCaptchaServiceProvider extends ServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* @var bool
*/
protected $defer = false;
/**
* Bootstrap the application events.
*/
public function boot()
{
$app = $this->app;
$this->bootConfig();
$app['validator']->extend('captcha', function ($attribute, $value) use ($app) {
return $app['captcha']->verifyResponse($value, $app['request']->getClientIp());
});
if ($app->bound('form')) {
$app['form']->macro('captcha', function ($attributes = []) use ($app) {
return $app['captcha']->display($attributes, $app->getLocale());
});
}
}
/**
* Booting configure.
*/
protected function bootConfig()
{
$path = __DIR__.'/config/captcha.php';
$this->mergeConfigFrom($path, 'captcha');
if (function_exists('config_path')) {
$this->publishes([$path => config_path('captcha.php')]);
}
}
/**
* Register the service provider.
*/
public function register()
{
$this->app->singleton('captcha', function ($app) {
return new NoCaptcha(
$app['config']['captcha.secret'],
$app['config']['captcha.sitekey'],
$app['config']['captcha.options']
);
});
}
/**
* Get the services provided by the provider.
*
* @return array
*/
public function provides()
{
return ['captcha'];
}
}

View File

View File

@ -0,0 +1,9 @@
<?php
return [
'secret' => env('NOCAPTCHA_SECRET'),
'sitekey' => env('NOCAPTCHA_SITEKEY'),
'options' => [
'timeout' => 30,
],
];

View File

View File

@ -0,0 +1,69 @@
<?php
use Anhskohbo\NoCaptcha\NoCaptcha;
class NoCaptchaTest extends PHPUnit_Framework_TestCase
{
/**
* @var NoCaptcha
*/
private $captcha;
public function setUp()
{
parent::setUp();
$this->captcha = new NoCaptcha('{secret-key}', '{site-key}');
}
public function testRequestShouldWorks()
{
$response = $this->captcha->verifyResponse('should_false');
}
public function testJsLink()
{
$this->assertTrue($this->captcha instanceof NoCaptcha);
$simple = '<script src="https://www.google.com/recaptcha/api.js?" async defer></script>'."\n";
$withLang = '<script src="https://www.google.com/recaptcha/api.js?hl=vi" async defer></script>'."\n";
$withCallback = '<script src="https://www.google.com/recaptcha/api.js?render=explicit&onload=myOnloadCallback" async defer></script>'."\n";
$this->assertEquals($simple, $this->captcha->renderJs());
$this->assertEquals($withLang, $this->captcha->renderJs('vi'));
$this->assertEquals($withCallback, $this->captcha->renderJs(null, true, 'myOnloadCallback'));
}
public function testDisplay()
{
$this->assertTrue($this->captcha instanceof NoCaptcha);
$simple = '<div data-sitekey="{site-key}" class="g-recaptcha"></div>';
$withAttrs = '<div data-theme="light" data-sitekey="{site-key}" class="g-recaptcha"></div>';
$this->assertEquals($simple, $this->captcha->display());
$this->assertEquals($withAttrs, $this->captcha->display(['data-theme' => 'light']));
}
public function testdisplaySubmit()
{
$this->assertTrue($this->captcha instanceof NoCaptcha);
$javascript = '<script>function onSubmittest(){document.getElementById("test").submit();}</script>';
$simple = '<button data-callback="onSubmittest" data-sitekey="{site-key}" class="g-recaptcha"><span>submit</span></button>';
$withAttrs = '<button data-theme="light" class="g-recaptcha 123" data-callback="onSubmittest" data-sitekey="{site-key}"><span>submit123</span></button>';
$this->assertEquals($simple . $javascript, $this->captcha->displaySubmit('test'));
$withAttrsResult = $this->captcha->displaySubmit('test','submit123',['data-theme' => 'light', 'class' => '123']);
$this->assertEquals($withAttrs . $javascript, $withAttrsResult);
}
public function testdisplaySubmitWithCustomCallback()
{
$this->assertTrue($this->captcha instanceof NoCaptcha);
$withAttrs = '<button data-theme="light" class="g-recaptcha 123" data-callback="onSubmitCustomCallback" data-sitekey="{site-key}"><span>submit123</span></button>';
$withAttrsResult = $this->captcha->displaySubmit('test-custom','submit123',['data-theme' => 'light', 'class' => '123', 'data-callback' => 'onSubmitCustomCallback']);
$this->assertEquals($withAttrs, $withAttrsResult);
}
}

25
vendor/autoload.php vendored Normal file
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 ComposerAutoloaderInitc91cd9c5b1e6a9e8573a14b799ea9342::getLoader();

22
vendor/bacon/bacon-qr-code/LICENSE vendored Normal file
View File

@ -0,0 +1,22 @@
Copyright (c) 2017, 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.

57
vendor/bacon/bacon-qr-code/README.md vendored Normal file
View File

@ -0,0 +1,57 @@
# 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');
```

View File

@ -0,0 +1,50 @@
{
"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",
"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"
]
}
}

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;
}
}

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;
}
}

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;
}
}

View File

@ -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;
}
}

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;
}
}

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;
}
}

View File

@ -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;
}
}

View File

@ -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
);
}
}

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;
}
}

View File

@ -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;
}
}

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))
),
];
}
}

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;
}
}

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;
}
}

View File

@ -0,0 +1,679 @@
<?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;
/**
* 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 = (Mode::BYTE() === $mode ? $dataBits->getSizeInBytes() : strlen($content));
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 $code) : int
{
if (isset(self::ALPHANUMERIC_TABLE[$code])) {
return self::ALPHANUMERIC_TABLE[$code];
}
return -1;
}
/**
* Chooses the best mode for a given content.
*/
private static function chooseMode(string $content, ?string $encoding = null) : Mode
{
if (null !== $encoding && 0 === strcasecmp($encoding, 'SHIFT-JIS')) {
return self::isOnlyDoubleByteKanji($content) ? Mode::KANJI() : Mode::BYTE();
}
$hasNumeric = false;
$hasAlphanumeric = false;
$contentLength = strlen($content);
for ($i = 0; $i < $contentLength; ++$i) {
$char = $content[$i];
if (ctype_digit($char)) {
$hasNumeric = true;
} elseif (-1 !== self::getAlphanumericCode(ord($char))) {
$hasAlphanumeric = true;
} else {
return Mode::BYTE();
}
}
if ($hasAlphanumeric) {
return Mode::ALPHANUMERIC();
} elseif ($hasNumeric) {
return Mode::NUMERIC();
}
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.
*/
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]) & 0xff;
if (($byte < 0x81 || $byte > 0x9f) && $byte < 0xe0 || $byte > 0xeb) {
return false;
}
}
return true;
}
/**
* 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] & 0xff;
}
$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.
*
* @throws WriterException if an invalid mode was supplied
*/
private static function appendBytes(string $content, Mode $mode, BitArray $bits, string $encoding) : void
{
switch ($mode) {
case Mode::NUMERIC():
self::appendNumericBytes($content, $bits);
break;
case Mode::ALPHANUMERIC():
self::appendAlphanumericBytes($content, $bits);
break;
case Mode::BYTE():
self::append8BitBytes($content, $bits, $encoding);
break;
case Mode::KANJI():
self::appendKanjiBytes($content, $bits);
break;
default:
throw new WriterException('Invalid mode: ' . $mode);
}
}
/**
* 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]) & 0xff;
$byte2 = ord($bytes[$i + 1]) & 0xff;
$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);
}
}

View File

@ -0,0 +1,271 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Encoder;
use BaconQrCode\Common\BitUtils;
use BaconQrCode\Exception\InvalidArgumentException;
/**
* 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) (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.
*
* @throws InvalidArgumentException if an invalid mask pattern was supplied
*/
public static function getDataMaskBit(int $maskPattern, int $x, int $y) : bool
{
switch ($maskPattern) {
case 0:
$intermediate = ($y + $x) & 0x1;
break;
case 1:
$intermediate = $y & 0x1;
break;
case 2:
$intermediate = $x % 3;
break;
case 3:
$intermediate = ($y + $x) % 3;
break;
case 4:
$intermediate = (BitUtils::unsignedRightShift($y, 1) + (int) ($x / 3)) & 0x1;
break;
case 5:
$temp = $y * $x;
$intermediate = ($temp & 0x1) + ($temp % 3);
break;
case 6:
$temp = $y * $x;
$intermediate = (($temp & 0x1) + ($temp % 3)) & 0x1;
break;
case 7:
$temp = $y * $x;
$intermediate = (($temp % 3) + (($y + $x) & 0x1)) & 0x1;
break;
default:
throw new InvalidArgumentException('Invalid mask pattern: ' . $maskPattern);
}
return 0 == $intermediate;
}
/**
* 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;
}
}

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() .')');
}
}
}

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;
}
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Exception;
use Throwable;
interface ExceptionInterface extends Throwable
{
}

View File

@ -0,0 +1,8 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Exception;
final class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View File

@ -0,0 +1,8 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Exception;
final class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface
{
}

View File

@ -0,0 +1,8 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Exception;
final class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

View File

@ -0,0 +1,8 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Exception;
final class UnexpectedValueException extends \UnexpectedValueException implements ExceptionInterface
{
}

View File

@ -0,0 +1,8 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Exception;
final class WriterException extends \RuntimeException implements ExceptionInterface
{
}

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();
}
}

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
{
$k = $this->black / 100;
$c = (-$k * $this->cyan + $k * 100 + $this->cyan) / 100;
$m = (-$k * $this->magenta + $k * 100 + $this->magenta) / 100;
$y = (-$k * $this->yellow + $k * 100 + $this->yellow) / 100;
return new Rgb(
(int) (-$c * 255 + 255),
(int) (-$m * 255 + 255),
(int) (-$y * 255 + 255)
);
}
public function toCmyk() : Cmyk
{
return $this;
}
public function toGray() : Gray
{
return $this->toRgb()->toGray();
}
}

View File

@ -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;
}

View File

@ -0,0 +1,39 @@
<?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
{
return new Rgb((int) ($this->gray * 2.55), (int) ($this->gray * 2.55), (int) ($this->gray * 2.55));
}
public function toCmyk() : Cmyk
{
return new Cmyk(0, 0, 0, 100 - $this->gray);
}
public function toGray() : Gray
{
return $this;
}
}

View File

@ -0,0 +1,73 @@
<?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
{
$c = 1 - ($this->red / 255);
$m = 1 - ($this->green / 255);
$y = 1 - ($this->blue / 255);
$k = min($c, $m, $y);
if ($k === 0) {
return new Cmyk(0, 0, 0, 0);
}
return new Cmyk(
(int) (100 * ($c - $k) / (1 - $k)),
(int) (100 * ($m - $k) / (1 - $k)),
(int) (100 * ($y - $k) / (1 - $k)),
(int) (100 * $k)
);
}
public function toGray() : Gray
{
return new Gray((int) (($this->red * 0.21 + $this->green * 0.71 + $this->blue * 0.07) / 2.55));
}
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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()
;
}
}

View File

@ -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()
;
}
}

View File

@ -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()
;
}
}

View File

@ -0,0 +1,238 @@
<?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
);
}
imagedestroy($this->image);
$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];
}
}

View File

@ -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());
}
}

View File

@ -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;
}

View File

@ -0,0 +1,318 @@
<?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;
public function __construct(string $imageFormat = 'png', int $compressionQuality = 100)
{
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;
}
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();
$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()));
}
}

View File

@ -0,0 +1,359 @@
<?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 const SCALE_FORMAT = 'scale(%.' . self::PRECISION . 'F)';
private const TRANSLATE_FORMAT = 'translate(%.' . self::PRECISION . 'F,%.' . self::PRECISION . 'F)';
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 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(self::SCALE_FORMAT, 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(self::TRANSLATE_FORMAT, 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;
}
$id = sprintf('g%d', ++$this->gradientCount);
$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()
);
}
}

View File

@ -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],
];
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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
);
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Module;
use BaconQrCode\Encoder\ByteMatrix;
use BaconQrCode\Renderer\Path\Path;
/**
* Interface describing how modules should be rendered.
*
* A module always receives a byte matrix (with values either being 1 or 0). It returns a path, where the origin
* coordinate (0, 0) equals the top left corner of the first matrix value.
*/
interface ModuleInterface
{
public function createPath(ByteMatrix $matrix) : Path;
}

View File

@ -0,0 +1,124 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Module;
use BaconQrCode\Encoder\ByteMatrix;
use BaconQrCode\Exception\InvalidArgumentException;
use BaconQrCode\Renderer\Module\EdgeIterator\EdgeIterator;
use BaconQrCode\Renderer\Path\Path;
/**
* Rounds the corners of module groups.
*/
final class RoundnessModule implements ModuleInterface
{
public const STRONG = 1;
public const MEDIUM = .5;
public const SOFT = .25;
public function __construct(private float $intensity)
{
if ($intensity <= 0 || $intensity > 1) {
throw new InvalidArgumentException('Intensity must between 0 (exclusive) and 1 (inclusive)');
}
$this->intensity = $intensity / 2;
}
public function createPath(ByteMatrix $matrix) : Path
{
$path = new Path();
foreach (new EdgeIterator($matrix) as $edge) {
$points = $edge->getSimplifiedPoints();
$length = count($points);
$currentPoint = $points[0];
$nextPoint = $points[1];
$horizontal = ($currentPoint[1] === $nextPoint[1]);
if ($horizontal) {
$right = $nextPoint[0] > $currentPoint[0];
$path = $path->move(
$currentPoint[0] + ($right ? $this->intensity : -$this->intensity),
$currentPoint[1]
);
} else {
$up = $nextPoint[0] < $currentPoint[0];
$path = $path->move(
$currentPoint[0],
$currentPoint[1] + ($up ? -$this->intensity : $this->intensity)
);
}
for ($i = 1; $i <= $length; ++$i) {
if ($i === $length) {
$previousPoint = $points[$length - 1];
$currentPoint = $points[0];
$nextPoint = $points[1];
} else {
$previousPoint = $points[(0 === $i ? $length : $i) - 1];
$currentPoint = $points[$i];
$nextPoint = $points[($length - 1 === $i ? -1 : $i) + 1];
}
$horizontal = ($previousPoint[1] === $currentPoint[1]);
if ($horizontal) {
$right = $previousPoint[0] < $currentPoint[0];
$up = $nextPoint[1] < $currentPoint[1];
$sweep = ($up xor $right);
if ($this->intensity < 0.5
|| ($right && $previousPoint[0] !== $currentPoint[0] - 1)
|| (! $right && $previousPoint[0] - 1 !== $currentPoint[0])
) {
$path = $path->line(
$currentPoint[0] + ($right ? -$this->intensity : $this->intensity),
$currentPoint[1]
);
}
$path = $path->ellipticArc(
$this->intensity,
$this->intensity,
0,
false,
$sweep,
$currentPoint[0],
$currentPoint[1] + ($up ? -$this->intensity : $this->intensity)
);
} else {
$up = $previousPoint[1] > $currentPoint[1];
$right = $nextPoint[0] > $currentPoint[0];
$sweep = ! ($up xor $right);
if ($this->intensity < 0.5
|| ($up && $previousPoint[1] !== $currentPoint[1] + 1)
|| (! $up && $previousPoint[0] + 1 !== $currentPoint[0])
) {
$path = $path->line(
$currentPoint[0],
$currentPoint[1] + ($up ? $this->intensity : -$this->intensity)
);
}
$path = $path->ellipticArc(
$this->intensity,
$this->intensity,
0,
false,
$sweep,
$currentPoint[0] + ($right ? $this->intensity : -$this->intensity),
$currentPoint[1]
);
}
}
$path = $path->close();
}
return $path;
}
}

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Module;
use BaconQrCode\Encoder\ByteMatrix;
use BaconQrCode\Renderer\Module\EdgeIterator\EdgeIterator;
use BaconQrCode\Renderer\Path\Path;
/**
* Groups modules together to a single path.
*/
final class SquareModule implements ModuleInterface
{
private static ?SquareModule $instance = null;
private function __construct()
{
}
public static function instance() : self
{
return self::$instance ?: self::$instance = new self();
}
public function createPath(ByteMatrix $matrix) : Path
{
$path = new Path();
foreach (new EdgeIterator($matrix) as $edge) {
$points = $edge->getSimplifiedPoints();
$length = count($points);
$path = $path->move($points[0][0], $points[0][1]);
for ($i = 1; $i < $length; ++$i) {
$path = $path->line($points[$i][0], $points[$i][1]);
}
$path = $path->close();
}
return $path;
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
final class Close implements OperationInterface
{
private static ?Close $instance = null;
private function __construct()
{
}
public static function instance() : self
{
return self::$instance ?: self::$instance = new self();
}
/**
* @return self
*/
public function translate(float $x, float $y) : OperationInterface
{
return $this;
}
/**
* @return self
*/
public function rotate(int $degrees) : OperationInterface
{
return $this;
}
}

View File

@ -0,0 +1,86 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
final class Curve implements OperationInterface
{
public function __construct(
private readonly float $x1,
private readonly float $y1,
private readonly float $x2,
private readonly float $y2,
private readonly float $x3,
private readonly float $y3
) {
}
public function getX1() : float
{
return $this->x1;
}
public function getY1() : float
{
return $this->y1;
}
public function getX2() : float
{
return $this->x2;
}
public function getY2() : float
{
return $this->y2;
}
public function getX3() : float
{
return $this->x3;
}
public function getY3() : float
{
return $this->y3;
}
/**
* @return self
*/
public function translate(float $x, float $y) : OperationInterface
{
return new self(
$this->x1 + $x,
$this->y1 + $y,
$this->x2 + $x,
$this->y2 + $y,
$this->x3 + $x,
$this->y3 + $y
);
}
/**
* @return self
*/
public function rotate(int $degrees) : OperationInterface
{
$radians = deg2rad($degrees);
$sin = sin($radians);
$cos = cos($radians);
$x1r = $this->x1 * $cos - $this->y1 * $sin;
$y1r = $this->x1 * $sin + $this->y1 * $cos;
$x2r = $this->x2 * $cos - $this->y2 * $sin;
$y2r = $this->x2 * $sin + $this->y2 * $cos;
$x3r = $this->x3 * $cos - $this->y3 * $sin;
$y3r = $this->x3 * $sin + $this->y3 * $cos;
return new self(
$x1r,
$y1r,
$x2r,
$y2r,
$x3r,
$y3r
);
}
}

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