Add new fields to items: id_code, image, location; implement QR code generation and printing; update translations and UI

This commit is contained in:
Edwin Noorlander 2025-11-11 17:59:23 +01:00
parent 921a74bbe2
commit a15c976106
61 changed files with 5514 additions and 83 deletions

1
add_col.php Normal file
View File

@ -0,0 +1 @@
<?php $db = new PDO("sqlite:collections.sqlite"); $db->exec("ALTER TABLE items ADD COLUMN id_code TEXT"); echo "done";

1
add_col2.php Normal file
View File

@ -0,0 +1 @@
<?php $db = new PDO("sqlite:collections.sqlite"); $db->exec("ALTER TABLE items ADD COLUMN image TEXT"); echo "done";

1
add_col3.php Normal file
View File

@ -0,0 +1 @@
<?php $db = new PDO("sqlite:collections.sqlite"); $db->exec("ALTER TABLE items ADD COLUMN location TEXT"); echo "done";

8
add_columns.php Normal file
View File

@ -0,0 +1,8 @@
<?php
require 'vendor/autoload.php';
require 'config.php';
= new PDO('sqlite:' . DB_PATH);
->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
try {
->exec('ALTER TABLE items ADD COLUMN id_code TEXT');
echo id_code

7
check_items.php Normal file
View File

@ -0,0 +1,7 @@
<?php
require 'vendor/autoload.php';
require 'config.php';
= App\Database\Database::getInstance();
= ->query('SELECT id, name FROM items');
= ->fetchAll(PDO::FETCH_ASSOC);
print_r();

Binary file not shown.

View File

@ -4,7 +4,8 @@
"nikic/fast-route": "^1.3",
"symfony/translation": "^7.3",
"twig/twig": "^3.22",
"guzzlehttp/psr7": "^2.6"
"guzzlehttp/psr7": "^2.6",
"chillerlan/php-qrcode": "^4.3"
},
"autoload": {
"psr-4": {

145
composer.lock generated
View File

@ -4,8 +4,151 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "70a9315b6260e925e9e24d4f9c5a02b3",
"content-hash": "b8e1d1dcdd0595d7ca4939c15aae5295",
"packages": [
{
"name": "chillerlan/php-qrcode",
"version": "4.4.2",
"source": {
"type": "git",
"url": "https://github.com/chillerlan/php-qrcode.git",
"reference": "345ed8e4ffb56e6b3fcd9f42e3970b9026fa6ce4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/345ed8e4ffb56e6b3fcd9f42e3970b9026fa6ce4",
"reference": "345ed8e4ffb56e6b3fcd9f42e3970b9026fa6ce4",
"shasum": ""
},
"require": {
"chillerlan/php-settings-container": "^2.1.6 || ^3.2.1",
"ext-mbstring": "*",
"php": "^7.4 || ^8.0"
},
"require-dev": {
"phan/phan": "^5.4.5",
"phpmd/phpmd": "^2.15",
"phpunit/phpunit": "^9.6",
"setasign/fpdf": "^1.8.2",
"squizlabs/php_codesniffer": "^3.11"
},
"suggest": {
"chillerlan/php-authenticator": "Yet another Google authenticator! Also creates URIs for mobile apps.",
"setasign/fpdf": "Required to use the QR FPDF output.",
"simple-icons/simple-icons": "SVG icons that you can use to embed as logos in the QR Code"
},
"type": "library",
"autoload": {
"psr-4": {
"chillerlan\\QRCode\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Kazuhiko Arase",
"homepage": "https://github.com/kazuhikoarase"
},
{
"name": "Smiley",
"email": "smiley@chillerlan.net",
"homepage": "https://github.com/codemasher"
},
{
"name": "Contributors",
"homepage": "https://github.com/chillerlan/php-qrcode/graphs/contributors"
}
],
"description": "A QR code generator with a user friendly API. PHP 7.4+",
"homepage": "https://github.com/chillerlan/php-qrcode",
"keywords": [
"phpqrcode",
"qr",
"qr code",
"qrcode",
"qrcode-generator"
],
"support": {
"issues": "https://github.com/chillerlan/php-qrcode/issues",
"source": "https://github.com/chillerlan/php-qrcode/tree/4.4.2"
},
"funding": [
{
"url": "https://ko-fi.com/codemasher",
"type": "ko_fi"
}
],
"time": "2024-11-15T15:36:24+00:00"
},
{
"name": "chillerlan/php-settings-container",
"version": "3.2.1",
"source": {
"type": "git",
"url": "https://github.com/chillerlan/php-settings-container.git",
"reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/95ed3e9676a1d47cab2e3174d19b43f5dbf52681",
"reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": "^8.1"
},
"require-dev": {
"phpmd/phpmd": "^2.15",
"phpstan/phpstan": "^1.11",
"phpstan/phpstan-deprecation-rules": "^1.2",
"phpunit/phpunit": "^10.5",
"squizlabs/php_codesniffer": "^3.10"
},
"type": "library",
"autoload": {
"psr-4": {
"chillerlan\\Settings\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Smiley",
"email": "smiley@chillerlan.net",
"homepage": "https://github.com/codemasher"
}
],
"description": "A container class for immutable settings objects. Not a DI container.",
"homepage": "https://github.com/chillerlan/php-settings-container",
"keywords": [
"Settings",
"configuration",
"container",
"helper"
],
"support": {
"issues": "https://github.com/chillerlan/php-settings-container/issues",
"source": "https://github.com/chillerlan/php-settings-container"
},
"funding": [
{
"url": "https://www.paypal.com/donate?hosted_button_id=WLYUNAT9ZTJZ4",
"type": "custom"
},
{
"url": "https://ko-fi.com/codemasher",
"type": "ko_fi"
}
],
"time": "2024-07-16T11:13:48+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "2.8.0",

View File

@ -41,3 +41,7 @@ $twig->addFunction(new \Twig\TwigFunction('trans', function (string $id, array $
$twig->addGlobal('current_locale', $locale);
$twig->addGlobal('supported_locales', SUPPORTED_LOCALES);
$twig->addGlobal('app_name', APP_NAME);
// Add translated confirm messages for JS
$twig->addGlobal('delete_part_confirm', $translator->trans('Are you sure you want to delete this part?'));
$twig->addGlobal('delete_category_confirm', $translator->trans('Are you sure you want to delete this category?'));

24
fix_db.php Normal file
View File

@ -0,0 +1,24 @@
<?php
require __DIR__ . '/vendor/autoload.php';
require __DIR__ . '/config.php';
use App\Database;
use App\Models\Category;
$db = Database::getInstance();
$categories = Category::getAll($db);
foreach ($categories as $cat) {
$path = Category::getFullPath($db, $cat['id']);
if (strpos($path, '[Circular]') !== false) {
echo "Circular reference found for category {$cat['id']}: {$cat['name']}\n";
// Reset parent_id to null
$stmt = $db->prepare('UPDATE categories SET parent_id = NULL WHERE id = :id');
$stmt->execute([':id' => $cat['id']]);
echo "Fixed by setting parent_id to NULL\n";
}
}
echo "Done checking and fixing circular references.\n";

View File

@ -54,5 +54,14 @@
"Edit Category": "Edit Category",
"Edit Part": "Edit Part",
"Save Changes": "Save Changes",
"Cancel": "Cancel"
"Cancel": "Cancel",
"A category cannot be its own parent": "A category cannot be its own parent",
"Cannot set a descendant as parent (circular reference)": "Cannot set a descendant as parent (circular reference)",
"Category not found": "Category not found",
"Server error": "Server error",
"Part not found": "Part not found",
"Are you sure you want to delete this part?": "Are you sure you want to delete this part?",
"Are you sure you want to delete this category?": "Are you sure you want to delete this category?",
"Image": "Image",
"Location": "Location"
}

View File

@ -54,5 +54,14 @@
"Edit Category": "Categorie Bewerken",
"Edit Part": "Onderdeel Bewerken",
"Save Changes": "Wijzigingen Opslaan",
"Cancel": "Annuleren"
"Cancel": "Annuleren",
"A category cannot be its own parent": "Een categorie kan niet zijn eigen bovenliggende zijn",
"Cannot set a descendant as parent (circular reference)": "Kan geen afstammeling als bovenliggende instellen (circulaire referentie)",
"Category not found": "Categorie niet gevonden",
"Server error": "Server fout",
"Part not found": "Onderdeel niet gevonden",
"Are you sure you want to delete this part?": "Weet je zeker dat je dit onderdeel wilt verwijderen?",
"Are you sure you want to delete this category?": "Weet je zeker dat je deze categorie wilt verwijderen?",
"Image": "Afbeelding",
"Location": "Locatie"
}

9
migrate_db.php Normal file
View File

@ -0,0 +1,9 @@
<?php
require 'vendor/autoload.php';
require 'config.php';
use AppDatabaseDatabase;
$db = App\Database\Database::getInstance();
$db->exec('ALTER TABLE items ADD COLUMN id_code TEXT UNIQUE');
$db->exec('ALTER TABLE items ADD COLUMN image TEXT');
$db->exec('ALTER TABLE items ADD COLUMN location TEXT');
echo 'Columns added to items table.\n';

View File

@ -1,13 +1,13 @@
<?php
<?php
$autoloadPath = __DIR__ . '/../vendor/autoload.php';
if (!file_exists($autoloadPath)) {
http_response_code(500);
die("FATAL ERROR: Composer autoloader not found. Please run 'composer install' in the project root.");
}
require $autoloadPath;
require $autoloadPath;
require __DIR__ . '/../config.php';
require __DIR__ . '/../config.php';
use App\Router;
use App\Controllers\ItemController;
@ -34,17 +34,19 @@ $router->addRoute('GET', '/lang/{locale}', function ($locale) {
$router->addRoute('GET', '/', [ItemController::class, 'overview']);
$router->addRoute('GET', '/categories', [CategoryController::class, 'index']);
$router->addRoute('GET', '/parts', [ItemController::class, 'addForm']);
$router->addRoute('GET', '/print/{id:\d+}', [ItemController::class, 'printQR']);
// --- API Routes (AJAX Content) ---
// These routes return only the Twig block content, not the full layout.
$router->addRoute('GET', '/api/items', [ItemController::class, 'listItems']);
$router->addRoute('GET', '/api/items/{id:\d+}', [ItemController::class, 'getItem']);
$router->addRoute('GET', '/api/categories', [CategoryController::class, 'listCategories']);
$router->addRoute('GET', '/api/categories/{id:\d+}', [CategoryController::class, 'getCategory']);
$router->addRoute('GET', '/api/categories/list', [CategoryController::class, 'listCategoriesJson']);
$router->addRoute('GET', '/api/categories/{id}', [CategoryController::class, 'getCategory']);
$router->addRoute('GET', '/api/parts', [ItemController::class, 'renderAddForm']);
// --- API CRUD Routes ---
// Items
$router->addRoute('GET', '/api/items/{id:\d+}', [ItemController::class, 'getItem']);
$router->addRoute('POST', '/api/items', [ItemController::class, 'create']);
$router->addRoute('PUT', '/api/items/{id:\d+}', [ItemController::class, 'update']);
$router->addRoute('DELETE', '/api/items/{id:\d+}', [ItemController::class, 'delete']);

View File

@ -159,7 +159,7 @@ document.addEventListener('DOMContentLoaded', function() {
.then(response => response.json())
.then(data => {
if (data.success) {
fetchContent('/parts', false);
fetchContent('/', false); // Go to overview to see the new part
} else {
alert(data.error || 'Error adding part');
}
@ -226,9 +226,23 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('edit_item_id').value = data.id;
document.getElementById('edit_item_name').value = data.name;
document.getElementById('edit_item_description').value = data.description || '';
document.getElementById('edit_item_category_id').value = data.category_id || '';
const modal = new bootstrap.Modal(document.getElementById('editItemModal'));
modal.show();
document.getElementById('edit_location').value = data.location || '';
// Populate category select
const categorySelect = document.getElementById('edit_item_category_id');
categorySelect.innerHTML = '<option value="">-- Select Category --</option>';
fetch('/api/categories/list')
.then(resp => resp.json())
.then(categories => {
categories.forEach(cat => {
const option = document.createElement('option');
option.value = cat.id;
option.textContent = cat.path;
categorySelect.appendChild(option);
});
categorySelect.value = data.category_id || '';
const modal = new bootstrap.Modal(document.getElementById('editItemModal'));
modal.show();
});
} else {
alert('Part not found');
}
@ -241,7 +255,7 @@ document.addEventListener('DOMContentLoaded', function() {
function handleDeleteItem() {
const id = this.getAttribute('data-id');
if (confirm('Are you sure you want to delete this part?')) {
if (confirm(window.translations.deletePartConfirm)) {
fetch('/api/items/' + id, {
method: 'DELETE'
})
@ -285,30 +299,32 @@ document.addEventListener('DOMContentLoaded', function() {
const name = document.getElementById('edit_item_name').value.trim();
const description = document.getElementById('edit_item_description').value;
const categoryId = document.getElementById('edit_item_category_id').value;
const location = document.getElementById('edit_location').value.trim();
const imageFile = document.getElementById('edit_image').files[0];
if (!name) {
alert('Part name is required');
return;
}
const data = {
item_name: name,
item_description: description,
category_id: categoryId || null
};
const formData = new FormData();
formData.append('item_name', name);
formData.append('item_description', description);
formData.append('category_id', categoryId || '');
formData.append('location', location);
if (imageFile) {
formData.append('image', imageFile);
}
fetch('/api/items/' + id, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById('editItemModal')).hide();
fetchContent('/overview', false); // Assuming current is overview
fetchContent('/', false); // Reload overview
} else {
alert(data.error || 'Error updating part');
}
@ -360,27 +376,47 @@ document.addEventListener('DOMContentLoaded', function() {
const id = this.getAttribute('data-id');
// Fetch current category data
fetch('/api/categories/' + id)
.then(response => response.json())
.then(response => {
if (!response.ok) {
throw new Error('HTTP ' + response.status + ': ' + response.statusText);
}
return response.json();
})
.then(data => {
if (data) {
if (data && !data.error) {
document.getElementById('edit_category_id').value = data.id;
document.getElementById('edit_category_name').value = data.name;
document.getElementById('edit_parent_category_id').value = data.parent_id || '';
const modal = new bootstrap.Modal(document.getElementById('editCategoryModal'));
modal.show();
// Populate parent select, excluding current category
const parentSelect = document.getElementById('edit_parent_category_id');
parentSelect.innerHTML = '<option value="">-- No Parent --</option>';
fetch('/api/categories/list')
.then(resp => resp.json())
.then(categories => {
categories.forEach(cat => {
if (cat.id != data.id) {
const option = document.createElement('option');
option.value = cat.id;
option.textContent = cat.path;
parentSelect.appendChild(option);
}
});
parentSelect.value = data.parent_id || '';
const modal = new bootstrap.Modal(document.getElementById('editCategoryModal'));
modal.show();
});
} else {
alert('Category not found');
alert(data.error || 'Category not found');
}
})
.catch(error => {
console.error('Error:', error);
alert('Error fetching category');
alert('Error fetching category: ' + error.message);
});
}
function handleDeleteCategory() {
const id = this.getAttribute('data-id');
if (confirm('Are you sure you want to delete this category?')) {
if (confirm(window.translations.deleteCategoryConfirm)) {
fetch('/api/categories/' + id, {
method: 'DELETE'
})

View File

@ -14,6 +14,8 @@ class CategoryController
echo $twig->render('layout.twig', ['active_page' => 'categories']);
}
// Get single category (JSON)
public static function getCategory($id) {
header('Content-Type: application/json');
@ -24,8 +26,24 @@ class CategoryController
echo json_encode($category);
} else {
http_response_code(404);
echo json_encode(['error' => 'Category not found']);
global $translator;
echo json_encode(['error' => $translator->trans('Category not found')]);
}
} catch (Exception $e) {
error_log('Error in getCategory: ' . $e->getMessage());
http_response_code(500);
global $translator;
echo json_encode(['error' => $translator->trans('Server error')]);
}
}
// Returns JSON list of categories
public static function listCategoriesJson() {
header('Content-Type: application/json');
try {
$db = Database::getInstance();
$categories = Category::getAll($db);
echo json_encode($categories);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
@ -63,7 +81,8 @@ class CategoryController
if (empty($name)) {
http_response_code(400);
echo json_encode(['error' => 'Category name is required']);
global $translator;
echo json_encode(['error' => $translator->trans('Category name is required')]);
return;
}
@ -88,19 +107,39 @@ class CategoryController
$existing = Category::getById($db, $id);
if (!$existing) {
http_response_code(404);
echo json_encode(['error' => 'Category not found']);
global $translator;
echo json_encode(['error' => $translator->trans('Category not found')]);
return;
}
$name = trim($data['category_name'] ?? $existing['name']);
$parentId = !empty($data['parent_category_id']) ? (int)$data['parent_category_id'] : null;
if (empty($name)) {
http_response_code(400);
echo json_encode(['error' => 'Category name is required']);
global $translator;
echo json_encode(['error' => $translator->trans('Category name is required')]);
return;
}
$category = new Category($db, $id, $name);
// Prevent circular references
if ($parentId !== null) {
if ($parentId == $id) {
http_response_code(400);
global $translator;
echo json_encode(['error' => $translator->trans('A category cannot be its own parent')]);
return;
}
$descendants = Category::getDescendantIds($db, $id);
if (in_array($parentId, $descendants)) {
http_response_code(400);
global $translator;
echo json_encode(['error' => $translator->trans('Cannot set a descendant as parent (circular reference)')]);
return;
}
}
$category = new Category($db, $id, $name, $parentId);
if ($category->save()) {
echo json_encode(['success' => true, 'message' => 'Category updated successfully']);
} else {
@ -120,7 +159,8 @@ class CategoryController
echo json_encode(['success' => true, 'message' => 'Category deleted successfully']);
} else {
http_response_code(404);
echo json_encode(['error' => 'Category not found']);
global $translator;
echo json_encode(['error' => $translator->trans('Category not found')]);
}
} catch (Exception $e) {
http_response_code(500);

View File

@ -5,6 +5,8 @@ namespace App\Controllers;
use App\Database;
use App\Models\Item;
use App\Models\Category;
use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions;
class ItemController
{
@ -12,7 +14,11 @@ class ItemController
public static function overview()
{
global $twig;
echo $twig->render('layout.twig', ['active_page' => 'overview']);
try {
echo $twig->render('layout.twig', ['active_page' => 'overview']);
} catch (Exception $e) {
echo "Twig Error: " . $e->getMessage();
}
}
// Renders the full layout for add parts page
@ -32,11 +38,14 @@ class ItemController
echo json_encode($item);
} else {
http_response_code(404);
echo json_encode(['error' => 'Part not found']);
global $translator;
echo json_encode(['error' => $translator->trans('Part not found')]);
}
} catch (Exception $e) {
error_log('Error in getItem: ' . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
global $translator;
echo json_encode(['error' => $translator->trans('Server error')]);
}
}
@ -69,6 +78,13 @@ class ItemController
$categoryId = !empty($_GET['category_id']) ? (int)$_GET['category_id'] : null;
$items = Item::getAllFiltered($db, $search, $categoryId);
foreach ($items as &$item) {
if ($item['category_id']) {
$item['category_path'] = Category::getFullPath($db, $item['category_id']);
} else {
$item['category_path'] = null;
}
}
$categories = Category::getAll($db);
// Render only the content block
@ -79,8 +95,8 @@ class ItemController
'selected_category' => $categoryId,
]);
} catch (\Exception $e) {
error_log("Error in listItems: " . $e->getMessage());
http_response_code(500);
error_log("Error fetching items: " . $e->getMessage());
echo "Error: Could not load items.";
}
}
@ -95,15 +111,28 @@ class ItemController
$name = trim($data['item_name'] ?? '');
$description = trim($data['item_description'] ?? '');
$categoryId = !empty($data['category_id']) ? (int)$data['category_id'] : null;
$location = trim($data['location'] ?? '');
if (empty($name)) {
http_response_code(400);
echo json_encode(['error' => 'Part name is required']);
global $translator;
echo json_encode(['error' => $translator->trans('Part name is required')]);
return;
}
$item = new Item($db, null, $name, $description, $categoryId);
// Generate unique id_code
$idCode = self::generateUniqueIdCode($db);
// Handle image upload
$imagePath = null;
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
$imagePath = self::handleImageUpload($_FILES['image']);
}
$item = new Item($db, null, $name, $description, $categoryId, null, $idCode, $imagePath, $location);
if ($item->save()) {
// Generate QR code
self::generateQRCode($idCode);
echo json_encode(['success' => true, 'message' => 'Part added successfully']);
} else {
throw new Exception('Failed to save part');
@ -114,6 +143,55 @@ class ItemController
}
}
private static function generateUniqueIdCode(PDO $db): string {
do {
$code = strtoupper(substr(md5(uniqid(mt_rand(), true)), 0, 8));
$stmt = $db->prepare('SELECT id FROM items WHERE id_code = :code');
$stmt->execute([':code' => $code]);
} while ($stmt->fetch());
return $code;
}
private static function handleImageUpload(array $file): ?string {
$uploadDir = __DIR__ . '/../../public/uploads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$fileName = uniqid() . '_' . basename($file['name']);
$targetPath = $uploadDir . $fileName;
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
return 'uploads/' . $fileName;
}
return null;
}
private static function generateQRCode(string $idCode): void {
$qrDir = __DIR__ . '/../../public/uploads/qr/';
if (!is_dir($qrDir)) {
mkdir($qrDir, 0755, true);
}
$options = new QROptions([
'outputType' => QRCode::OUTPUT_IMAGE_PNG,
'eccLevel' => QRCode::ECC_L,
'imageTransparent' => false,
'imageBase64' => false,
]);
$qrCode = new QRCode($options);
$qrCode->render($idCode, $qrDir . $idCode . '.png');
}
public static function printQR($id) {
global $twig;
$db = Database::getInstance();
$item = Item::getById($db, $id);
if (!$item) {
http_response_code(404);
echo "Item not found";
return;
}
echo $twig->render('print_qr.twig', ['item' => $item]);
}
public static function update($id) {
header('Content-Type: application/json');
try {
@ -123,21 +201,31 @@ class ItemController
$existing = Item::getById($db, $id);
if (!$existing) {
http_response_code(404);
echo json_encode(['error' => 'Part not found']);
global $translator;
echo json_encode(['error' => $translator->trans('Part not found')]);
return;
}
$name = trim($data['item_name'] ?? $existing['name']);
$description = trim($data['item_description'] ?? $existing['description']);
$categoryId = isset($data['category_id']) && $data['category_id'] !== '' ? (int)$data['category_id'] : $existing['category_id'];
$location = trim($data['location'] ?? $existing['location']);
$idCode = $existing['id_code']; // Keep existing id_code
$imagePath = $existing['image'];
// Handle image upload if new file
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
$imagePath = self::handleImageUpload($_FILES['image']);
}
if (empty($name)) {
http_response_code(400);
echo json_encode(['error' => 'Part name is required']);
global $translator;
echo json_encode(['error' => $translator->trans('Part name is required')]);
return;
}
$item = new Item($db, $id, $name, $description, $categoryId);
$item = new Item($db, $id, $name, $description, $categoryId, null, $idCode, $imagePath, $location);
if ($item->save()) {
echo json_encode(['success' => true, 'message' => 'Part updated successfully']);
} else {
@ -157,7 +245,8 @@ class ItemController
echo json_encode(['success' => true, 'message' => 'Part deleted successfully']);
} else {
http_response_code(404);
echo json_encode(['error' => 'Part not found']);
global $translator;
echo json_encode(['error' => $translator->trans('Part not found')]);
}
} catch (Exception $e) {
http_response_code(500);

View File

@ -53,16 +53,36 @@ class Category {
}
}
public static function getFullPath(PDO $db, int $id, string $separator = ' / '): string {
public static function getFullPath(PDO $db, int $id, string $separator = ' / ', array &$visited = []): string {
if (in_array($id, $visited)) {
return '[Circular]'; // Indicate circular reference
}
$category = self::getById($db, $id);
if (!$category) return '';
if ($category['parent_id']) {
return self::getFullPath($db, $category['parent_id'], $separator) . $separator . $category['name'];
$visited[] = $id;
return self::getFullPath($db, $category['parent_id'], $separator, $visited) . $separator . $category['name'];
} else {
return $category['name'];
}
}
public static function getDescendantIds(PDO $db, int $id, array &$visited = []): array {
if (in_array($id, $visited)) {
return []; // Prevent infinite recursion
}
$visited[] = $id;
$descendants = [];
$stmt = $db->prepare('SELECT id FROM categories WHERE parent_id = :id');
$stmt->execute([':id' => $id]);
$children = $stmt->fetchAll(PDO::FETCH_COLUMN);
foreach ($children as $childId) {
$descendants[] = $childId;
$descendants = array_merge($descendants, self::getDescendantIds($db, $childId, $visited));
}
return $descendants;
}
public static function getById(PDO $db, int $id): ?array {
try {
$stmt = $db->prepare('SELECT id, name, parent_id FROM categories WHERE id = :id');

View File

@ -12,14 +12,20 @@ class Item {
private ?string $description;
private ?int $categoryId;
private ?string $categoryName;
private ?string $idCode;
private ?string $image;
private ?string $location;
public function __construct(PDO $db, ?int $id = null, string $name = '', ?string $description = null, ?int $categoryId = null, ?string $categoryName = null) {
public function __construct(PDO $db, ?int $id = null, string $name = '', ?string $description = null, ?int $categoryId = null, ?string $categoryName = null, ?string $idCode = null, ?string $image = null, ?string $location = null) {
$this->db = $db;
$this->id = $id;
$this->name = $name;
$this->description = $description;
$this->categoryId = $categoryId;
$this->categoryName = $categoryName;
$this->idCode = $idCode;
$this->image = $image;
$this->location = $location;
}
public function getId(): ?int {
@ -54,13 +60,37 @@ class Item {
return $this->categoryName;
}
public function getIdCode(): ?string {
return $this->idCode;
}
public function setIdCode(?string $idCode): void {
$this->idCode = $idCode;
}
public function getImage(): ?string {
return $this->image;
}
public function setImage(?string $image): void {
$this->image = $image;
}
public function getLocation(): ?string {
return $this->location;
}
public function setLocation(?string $location): void {
$this->location = $location;
}
public static function getAll(PDO $db): array {
return self::getAllFiltered($db, '', null);
}
public static function getAllFiltered(PDO $db, string $search, ?int $categoryId): array {
try {
$query = 'SELECT i.id, i.name, i.description, c.name as category_name, i.category_id
$query = 'SELECT i.id, i.name, i.description, c.name as category_name, i.category_id, i.id_code, i.image, i.location
FROM items i
LEFT JOIN categories c ON i.category_id = c.id
WHERE 1=1';
@ -90,7 +120,7 @@ class Item {
public static function getById(PDO $db, int $id): ?array {
try {
$stmt = $db->prepare(
'SELECT i.id, i.name, i.description, c.name as category_name, i.category_id
'SELECT i.id, i.name, i.description, c.name as category_name, i.category_id, i.id_code, i.image, i.location
FROM items i
LEFT JOIN categories c ON i.category_id = c.id
WHERE i.id = :id'
@ -109,13 +139,16 @@ class Item {
// Add new item
try {
$stmt = $this->db->prepare(
'INSERT INTO items (name, description, category_id)
VALUES (:name, :description, :category_id)'
'INSERT INTO items (name, description, category_id, id_code, image, location)
VALUES (:name, :description, :category_id, :id_code, :image, :location)'
);
$stmt->execute([
':name' => $this->name,
':description' => $this->description,
':category_id' => $this->categoryId
':category_id' => $this->categoryId,
':id_code' => $this->idCode,
':image' => $this->image,
':location' => $this->location
]);
$this->id = (int)$this->db->lastInsertId();
return true;
@ -128,13 +161,16 @@ class Item {
try {
$stmt = $this->db->prepare(
'UPDATE items
SET name = :name, description = :description, category_id = :category_id
SET name = :name, description = :description, category_id = :category_id, id_code = :id_code, image = :image, location = :location
WHERE id = :id'
);
$stmt->execute([
':name' => $this->name,
':description' => $this->description,
':category_id' => $this->categoryId,
':id_code' => $this->idCode,
':image' => $this->image,
':location' => $this->location,
':id' => $this->id
]);
return true;

View File

@ -32,16 +32,23 @@
<ul id="itemsList" class="list-group mb-4">
{% if items %}
{% for item in items %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<h5>{{ item.name }}</h5>
<p class="text-muted">{{ trans('Category') }}: {{ item.category_name ?: trans('Uncategorized') }}</p>
<p>{{ item.description }}</p>
</div>
<div>
<button class="btn btn-sm btn-warning me-2 edit-btn" data-id="{{ item.id }}">Edit</button>
<button class="btn btn-sm btn-danger delete-btn" data-id="{{ item.id }}">Delete</button>
</div>
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<h5>{{ item.name }} <small class="text-muted">({{ item.id_code }})</small></h5>
<p class="text-muted">{{ trans('Category') }}: {{ item.category_path ?: trans('Uncategorized') }}</p>
<p>{{ item.description }}</p>
{% if item.image %}
<img src="{{ item.image }}" alt="Image" style="max-width: 100px; max-height: 100px;">
{% endif %}
{% if item.location %}
<p><strong>{{ trans('Location') }}:</strong> {{ item.location }}</p>
{% endif %}
</div>
<div>
<button class="btn btn-sm btn-warning me-2 edit-btn" data-id="{{ item.id }}">Edit</button>
<button class="btn btn-sm btn-info me-2" onclick="window.open('/print/{{ item.id }}', '_blank')">Print QR</button>
<button class="btn btn-sm btn-danger delete-btn" data-id="{{ item.id }}">Delete</button>
</div>
</li>
{% endfor %}
{% else %}
@ -68,16 +75,24 @@
<label for="edit_item_description" class="form-label">{{ trans('Description') }}</label>
<textarea class="form-control" id="edit_item_description" name="item_description" rows="3"></textarea>
</div>
<div class="mb-3">
<label for="edit_item_category_id" class="form-label">{{ trans('Category') }}</label>
<select class="form-select" id="edit_item_category_id" name="category_id">
<option value="">{{ trans('-- Select Category --') }}</option>
{% for category in categories %}
<option value="{{ category.id }}">{{ category.path }}</option>
{% endfor %}
</select>
</div>
</form>
<div class="mb-3">
<label for="edit_item_category_id" class="form-label">{{ trans('Category') }}</label>
<select class="form-select" id="edit_item_category_id" name="category_id">
<option value="">{{ trans('-- Select Category --') }}</option>
{% for category in categories %}
<option value="{{ category.id }}">{{ category.path }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="edit_image" class="form-label">{{ trans('Image') }}</label>
<input type="file" class="form-control" id="edit_image" name="image" accept="image/*">
</div>
<div class="mb-3">
<label for="edit_location" class="form-label">{{ trans('Location') }}</label>
<input type="text" class="form-control" id="edit_location" name="location">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ trans('Cancel') }}</button>

View File

@ -29,7 +29,14 @@
</div>
<!-- Bootstrap JS Bundle -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2AEu8T+c+7f+z/j43" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<!-- Translated messages for JS -->
<script>
window.translations = {
deletePartConfirm: "{{ delete_part_confirm|escape('js') }}",
deleteCategoryConfirm: "{{ delete_category_confirm|escape('js') }}"
};
</script>
<!-- Custom JS -->
<script src="js/app.js"></script>
</body>

View File

@ -22,6 +22,14 @@
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="image" class="form-label">{{ trans('Image') }}</label>
<input type="file" class="form-control" id="image" name="image" accept="image/*">
</div>
<div class="mb-3">
<label for="location" class="form-label">{{ trans('Location') }}</label>
<input type="text" class="form-control" id="location" name="location">
</div>
<button type="submit" class="btn btn-primary">{{ trans('Add Part') }}</button>
</form>
</div>

32
templates/print_qr.twig Normal file
View File

@ -0,0 +1,32 @@
{% autoescape %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QR Code for {{ item.name }}</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; margin: 20px; }
.qr-container { border: 1px solid #000; padding: 20px; display: inline-block; }
.qr-code { width: 200px; height: 200px; }
.details { margin-top: 20px; }
@media print { body { margin: 0; } .qr-container { page-break-inside: avoid; } }
</style>
</head>
<body>
<div class="qr-container">
<h2>{{ item.name }}</h2>
<p>ID Code: {{ item.id_code }}</p>
<img src="/uploads/qr/{{ item.id_code }}.png" alt="QR Code" class="qr-code">
<div class="details">
<p><strong>Description:</strong> {{ item.description }}</p>
<p><strong>Category:</strong> {{ item.category_path ?: 'Uncategorized' }}</p>
{% if item.location %}
<p><strong>Location:</strong> {{ item.location }}</p>
{% endif %}
</div>
</div>
<script>window.print();</script>
</body>
</html>
{% endautoescape %}

1
test_trans.php Normal file
View File

@ -0,0 +1 @@
<?php require "config.php"; global $translator; echo $translator->trans("Category not found");

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PhpInternalEntityUsedInspection" enabled="true" level="INFO" enabled_by_default="true" />
</profile>
</component>

21
vendor/chillerlan/php-qrcode/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Smiley <smiley@chillerlan.net>
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.

429
vendor/chillerlan/php-qrcode/README.md vendored Normal file
View File

@ -0,0 +1,429 @@
# chillerlan/php-qrcode
A PHP 7.4+ QR Code library based on the [implementation](https://github.com/kazuhikoarase/qrcode-generator) by [Kazuhiko Arase](https://github.com/kazuhikoarase),
namespaced, cleaned up, improved and other stuff.
**Attention:** there is now also a javascript port: [chillerlan/js-qrcode](https://github.com/chillerlan/js-qrcode).
[![PHP Version Support][php-badge]][php]
[![Packagist version][packagist-badge]][packagist]
[![Continuous Integration][gh-action-badge]][gh-action]
[![CodeCov][coverage-badge]][coverage]
[![Codacy][codacy-badge]][codacy]
[![Packagist downloads][downloads-badge]][downloads]
[php-badge]: https://img.shields.io/packagist/php-v/chillerlan/php-qrcode?logo=php&color=8892BF
[php]: https://www.php.net/supported-versions.php
[packagist-badge]: https://img.shields.io/packagist/v/chillerlan/php-qrcode.svg?logo=packagist
[packagist]: https://packagist.org/packages/chillerlan/php-qrcode
[coverage-badge]: https://img.shields.io/codecov/c/github/chillerlan/php-qrcode/v4.3.x?logo=codecov
[coverage]: https://app.codecov.io/gh/chillerlan/php-qrcode/tree/v4.3.x
[codacy-badge]: https://img.shields.io/codacy/grade/edccfc4fe5a34b74b1c53ee03f097b8d/v4.3.x?logo=codacy
[codacy]: https://app.codacy.com/gh/chillerlan/php-qrcode/dashboard?branch=v4.3.x
[downloads-badge]: https://img.shields.io/packagist/dt/chillerlan/php-qrcode?logo=packagist
[downloads]: https://packagist.org/packages/chillerlan/php-qrcode/stats
[gh-action-badge]: https://img.shields.io/github/actions/workflow/status/chillerlan/php-qrcode/tests.yml?branch=v4.3.x&logo=github
[gh-action]: https://github.com/chillerlan/php-qrcode/actions/workflows/tests.yml?query=branch%3Av4.3.x
# Documentation
## Requirements
- PHP 7.4+
- `ext-mbstring`
- optional:
- `ext-json`, `ext-gd`
- `ext-imagick` with [ImageMagick](https://imagemagick.org) installed
- [`setasign/fpdf`](https://github.com/setasign/fpdf) for the PDF output module
## Installation
**requires [composer](https://getcomposer.org)**
via terminal: `composer require chillerlan/php-qrcode`
*composer.json*
```json
{
"require": {
"php": "^7.4 || ^8.0",
"chillerlan/php-qrcode": "v4.3.x-dev#<commit_hash>"
}
}
```
Note: replace `v4.3.x-dev` with a [version constraint](https://getcomposer.org/doc/articles/versions.md#writing-version-constraints), e.g. `^4.3` - see [releases](https://github.com/chillerlan/php-qrcode/releases) for valid versions.
For PHP version ...
- 7.4+ use `^4.3`
- 7.2+ use `^3.4.1` (v3.4.1 also supports PHP8)
- 7.0+ use `^2.0`
- 5.6+ use `^1.0` (please let PHP 5 die!)
In case you want to keep using `v4.3.x-dev`, specify the hash of a commit to avoid running into unforseen issues like so: `v4.3.x-dev#c115f7bc51d466ccb24c544e88329804aad8c2a0`
PSA: [PHP 7.0 - 7.4 are EOL](https://www.php.net/supported-versions.php) and therefore the respective `QRCode` versions are also no longer supported!
## Quickstart
We want to encode this URI for a mobile authenticator into a QRcode image:
```php
$data = 'otpauth://totp/test?secret=B3JX4VCVJDVNXNZ5&issuer=chillerlan.net';
// quick and simple:
echo '<img src="'.(new QRCode)->render($data).'" alt="QR Code" />';
```
<p align="center">
<img alt="QR codes are awesome!" src="https://raw.githubusercontent.com/chillerlan/php-qrcode/v4.3.x/examples/example_image.png">
<img alt="QR codes are awesome!" src="https://raw.githubusercontent.com/chillerlan/php-qrcode/v4.3.x/examples/example_svg.png">
</p>
Wait, what was that? Please again, slower!
## Advanced usage
Ok, step by step. First you'll need a `QRCode` instance, which can be optionally invoked with a `QROptions` (or a [`SettingsContainerInterface`](https://github.com/chillerlan/php-settings-container/blob/master/src/SettingsContainerInterface.php), respectively) object as the only parameter.
```php
$options = new QROptions([
'version' => 5,
'outputType' => QRCode::OUTPUT_MARKUP_SVG,
'eccLevel' => QRCode::ECC_L,
]);
// invoke a fresh QRCode instance
$qrcode = new QRCode($options);
// and dump the output
$qrcode->render($data);
// ...with additional cache file
$qrcode->render($data, '/path/to/file.svg');
```
In case you just want the raw QR code matrix, call `QRCode::getMatrix()` - this method is also called internally from `QRCode::render()`. See also [[Custom output interface]].
```php
$matrix = $qrcode->getMatrix($data);
foreach($matrix->matrix() as $y => $row){
foreach($row as $x => $module){
// get a module's value
$value = $module;
// or via the matrix's getter method
$value = $matrix->get($x, $y);
// boolean check a module
if($matrix->check($x, $y)){ // if($module >> 8 > 0)
// do stuff, the module is dark
}
else{
// do other stuff, the module is light
}
}
}
```
Have a look [in the examples folder](https://github.com/chillerlan/php-qrcode/tree/main/examples) for some more usage examples.
### Notes
The QR encoder, especially the subroutines for mask pattern testing, can cause high CPU load on increased matrix size.
You can avoid a part of this load by choosing a fast output module, like `OUTPUT_IMAGE_*` and maybe setting the mask pattern manually (which may result in unreadable QR Codes).
Oh hey and don't forget to sanitize any user input!
## Custom output interface
Instead of bloating your code you can simply create your own output interface by creating a `QROutputInterface` (i.e. extending `QROutputAbstract`).
```php
class MyCustomOutput extends QROutputAbstract{
// inherited from QROutputAbstract
protected QRMatrix $matrix; // QRMatrix
protected int $moduleCount; // modules QRMatrix::size()
protected QROptions $options; // MyCustomOptions or QROptions
protected int $scale; // scale factor from options
protected int $length; // length of the matrix ($moduleCount * $scale)
// ...check/set default module values (abstract method, called by the constructor)
protected function setModuleValues():void{
// $this->moduleValues = ...
}
// QROutputInterface::dump()
public function dump(string $file = null):string{
$output = '';
for($row = 0; $row < $this->moduleCount; $row++){
for($col = 0; $col < $this->moduleCount; $col++){
$output .= (int)$this->matrix->check($col, $row);
}
}
return $output;
}
}
```
For more examples, have a look at the [built-in output modules](https://github.com/chillerlan/php-qrcode/tree/main/src/Output).
In case you need additional settings for your output module, just extend `QROptions`...
```
class MyCustomOptions extends QROptions{
protected string $myParam = 'defaultValue';
// ...
}
```
...or use the [`SettingsContainerInterface`](https://github.com/chillerlan/php-settings-container/blob/main/src/SettingsContainerInterface.php), which is the more flexible approach.
```php
trait MyCustomOptionsTrait{
protected string $myParam = 'defaultValue';
// ...
}
```
set the options:
```php
$myOptions = [
'version' => 5,
'eccLevel' => QRCode::ECC_L,
'outputType' => QRCode::OUTPUT_CUSTOM,
'outputInterface' => MyCustomOutput::class,
// your custom settings
'myParam' => 'whatever value',
];
// extends QROptions
$myCustomOptions = new MyCustomOptions($myOptions);
// using the SettingsContainerInterface
$myCustomOptions = new class($myOptions) extends SettingsContainerAbstract{
use QROptionsTrait, MyCustomOptionsTrait;
};
```
You can then call `QRCode` with the custom modules...
```php
(new QRCode($myCustomOptions))->render($data);
```
...or invoke the `QROutputInterface` manually.
```php
$qrOutputInterface = new MyCustomOutput($myCustomOptions, (new QRCode($myCustomOptions))->getMatrix($data));
//dump the output, which is equivalent to QRCode::render()
$qrOutputInterface->dump();
```
### Custom module values
You can distinguish between different parts of the matrix, namely the several required patterns from the QR Code specification, and use them in different ways, i.e. to assign different colors for each part of the matrix (see the [image example](https://github.com/chillerlan/php-qrcode/blob/main/examples/image.php)).
The dark value is the module value (light) shifted by 8 bits to the left: `$value = $M_TYPE << ($bool ? 8 : 0);`, where `$M_TYPE` is one of the `QRMatrix::M_*` constants.
You can check the value for a type explicitly like...
```php
// for true (dark)
($value >> 8) === $M_TYPE;
// for false (light)
$value === $M_TYPE;
```
...or you can perform a loose check, ignoring the module value
```php
// for true
($value >> 8) > 0;
// for false
($value >> 8) === 0;
```
See also `QRMatrix::set()`, `QRMatrix::check()` and [`QRMatrix` constants](#qrmatrix-constants).
To map the values and properly render the modules for the given `QROutputInterface`, it's necessary to overwrite the default values:
```php
$options = new QROptions;
// for HTML, SVG and ImageMagick
$options->moduleValues = [
// finder
QRMatrix::M_FINDER_DARK => '#A71111', // dark (true)
QRMatrix::M_FINDER_DOT_DARK => '#A71111', // dark (true)
QRMatrix::M_FINDER => '#FFBFBF', // light (false)
// alignment
QRMatrix::M_ALIGNMENT_DARK => '#A70364',
QRMatrix::M_ALIGNMENT => '#FFC9C9',
// timing
QRMatrix::M_TIMING_DARK => '#98005D',
QRMatrix::M_TIMING => '#FFB8E9',
// format
QRMatrix::M_FORMAT_DARK => '#003804',
QRMatrix::M_FORMAT => '#00FB12',
// version
QRMatrix::M_VERSION_DARK => '#650098',
QRMatrix::M_VERSION => '#E0B8FF',
// data
QRMatrix::M_DATA_DARK => '#4A6000',
QRMatrix::M_DATA => '#ECF9BE',
// darkmodule
QRMatrix::M_DARKMODULE_DARK => '#080063',
// separator
QRMatrix::M_SEPARATOR => '#AFBFBF',
// quietzone
QRMatrix::M_QUIETZONE => '#FFFFFF',
];
// for the image output types
$options->moduleValues = [
QRMatrix::M_DATA_DARK => [0, 0, 0],
// ...
];
// for string/text output
$options->moduleValues = [
QRMatrix::M_DATA_DARK => '#',
// ...
];
```
## Public API
### `QRCode` API
#### Methods
| method | return | description |
|---------------------------------------------|------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------|
| `__construct(QROptions $options = null)` | - | see [`SettingsContainerInterface`](https://github.com/chillerlan/php-settings-container/blob/main/src/SettingsContainerInterface.php) |
| `render(string $data, string $file = null)` | mixed, `QROutputInterface::dump()` | renders a QR Code for the given `$data` and `QROptions`, saves `$file` optional |
| `getMatrix(string $data)` | `QRMatrix` | returns a `QRMatrix` object for the given `$data` and current `QROptions` |
| `initDataInterface(string $data)` | `QRDataInterface` | returns a fresh `QRDataInterface` for the given `$data` |
| `isNumber(string $string)` | bool | checks if a string qualifies for `Number` |
| `isAlphaNum(string $string)` | bool | checks if a string qualifies for `AlphaNum` |
| `isKanji(string $string)` | bool | checks if a string qualifies for `Kanji` |
| `isByte(string $string)` | bool | checks if a string is non-empty |
#### Constants
| name | description |
|------------------------------------------------------------|------------------------------------------------------------------------------|
| `VERSION_AUTO` | `QROptions::$version` |
| `MASK_PATTERN_AUTO` | `QROptions::$maskPattern` |
| `OUTPUT_MARKUP_SVG`, `OUTPUT_MARKUP_HTML` | `QROptions::$outputType` markup |
| `OUTPUT_IMAGE_PNG`, `OUTPUT_IMAGE_JPG`, `OUTPUT_IMAGE_GIF` | `QROptions::$outputType` image |
| `OUTPUT_STRING_JSON`, `OUTPUT_STRING_TEXT` | `QROptions::$outputType` string |
| `OUTPUT_IMAGICK` | `QROptions::$outputType` ImageMagick |
| `OUTPUT_FPDF` | `QROptions::$outputType` PDF, using [FPDF](https://github.com/setasign/fpdf) |
| `OUTPUT_CUSTOM` | `QROptions::$outputType`, requires `QROptions::$outputInterface` |
| `ECC_L`, `ECC_M`, `ECC_Q`, `ECC_H`, | ECC-Level: 7%, 15%, 25%, 30% in `QROptions::$eccLevel` |
| `DATA_NUMBER`, `DATA_ALPHANUM`, `DATA_BYTE`, `DATA_KANJI` | `QRDataInterface::$datamode` |
### `QRMatrix` API
#### Methods
| method | return | description |
|-------------------------------------------------|------------|-------------------------------------------------------------------------------------------------------|
| `__construct(int $version, int $eclevel)` | - | - |
| `init(int $maskPattern, bool $test = null)` | `QRMatrix` | |
| `matrix()` | array | the internal matrix representation as a 2 dimensional array |
| `version()` | int | the current QR Code version |
| `eccLevel()` | int | current ECC level |
| `maskPattern()` | int | the used mask pattern |
| `size()` | int | the absoulute size of the matrix, including quiet zone (if set). `$version * 4 + 17 + 2 * $quietzone` |
| `get(int $x, int $y)` | int | returns the value of the module |
| `set(int $x, int $y, bool $value, int $M_TYPE)` | `QRMatrix` | sets the `$M_TYPE` value for the module |
| `check(int $x, int $y)` | bool | checks whether a module is true (dark) or false (light) |
#### Constants
| name | description |
|----------------------|---------------------------------------------------------------|
| `M_NULL` | module not set (should never appear. if so, there's an error) |
| `M_DARKMODULE` | once per matrix at `$xy = [8, 4 * $version + 9]` |
| `M_DARKMODULE_LIGHT` | (reserved for reflectance reversal) |
| `M_DATA` | the actual encoded data |
| `M_DATA_DARK` | |
| `M_FINDER` | the 7x7 finder patterns |
| `M_FINDER_DARK` | |
| `M_FINDER_DOT` | the 3x3 dot inside the finder patterns |
| `M_FINDER_DOT_LIGHT` | (reserved for reflectance reversal) |
| `M_SEPARATOR` | separator lines around the finder patterns |
| `M_SEPARATOR_DARK` | (reserved for reflectance reversal) |
| `M_ALIGNMENT` | the 5x5 alignment patterns |
| `M_ALIGNMENT_DARK` | |
| `M_TIMING` | the timing pattern lines |
| `M_TIMING_DARK` | |
| `M_FORMAT` | format information pattern |
| `M_FORMAT_DARK` | |
| `M_VERSION` | version information pattern |
| `M_VERSION_DARK` | |
| `M_QUIETZONE` | margin around the QR Code |
| `M_QUIETZONE_DARK` | (reserved for reflectance reversal) |
| `M_LOGO` | space for a logo image |
| `M_LOGO_DARK` | (reserved for reflectance reversal) |
### `QROptions` API
#### Properties
| property | type | default | allowed | description |
|------------------------|--------|-----------------------------|---------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
| `$version` | int | `QRCode::VERSION_AUTO` | 1...40 | the [QR Code version number](http://www.qrcode.com/en/about/version.html) |
| `$versionMin` | int | 1 | 1...40 | Minimum QR version (if `$version = QRCode::VERSION_AUTO`) |
| `$versionMax` | int | 40 | 1...40 | Maximum QR version (if `$version = QRCode::VERSION_AUTO`) |
| `$eccLevel` | int | `QRCode::ECC_L` | `QRCode::ECC_X` | Error correct level, where X = L (7%), M (15%), Q (25%), H (30%) |
| `$maskPattern` | int | `QRCode::MASK_PATTERN_AUTO` | 0...7 | Mask Pattern to use |
| `$addQuietzone` | bool | `true` | - | Add a "quiet zone" (margin) according to the QR code spec |
| `$quietzoneSize` | int | 4 | clamped to 0 ... `$matrixSize / 2` | Size of the quiet zone |
| `$dataModeOverride` | string | `null` | `Number`, `AlphaNum`, `Kanji`, `Byte` | allows overriding the data type detection |
| `$outputType` | string | `QRCode::OUTPUT_IMAGE_PNG` | `QRCode::OUTPUT_*` | built-in output type |
| `$outputInterface` | string | `null` | * | FQCN of the custom `QROutputInterface` if `QROptions::$outputType` is set to `QRCode::OUTPUT_CUSTOM` |
| `$cachefile` | string | `null` | * | optional cache file path |
| `$eol` | string | `PHP_EOL` | * | newline string (HTML, SVG, TEXT) |
| `$scale` | int | 5 | * | size of a QR code pixel (SVG, IMAGE_*), HTML -> via CSS |
| `$cssClass` | string | `null` | * | a common css class |
| `$svgOpacity` | float | 1.0 | 0...1 | |
| `$svgDefs` | string | * | * | anything between [`<defs>`](https://developer.mozilla.org/docs/Web/SVG/Element/defs) |
| `$svgViewBoxSize` | int | `null` | * | a positive integer which defines width/height of the [viewBox attribute](https://css-tricks.com/scale-svg/#article-header-id-3) |
| `$textDark` | string | '██' | * | string substitute for dark |
| `$textLight` | string | '░░' | * | string substitute for light |
| `$markupDark` | string | '#000' | * | markup substitute for dark (CSS value) |
| `$markupLight` | string | '#fff' | * | markup substitute for light (CSS value) |
| `$imageBase64` | bool | `true` | - | whether to return the image data as base64 or raw like from `file_get_contents()` |
| `$imageTransparent` | bool | `true` | - | toggle transparency (no jpeg support) |
| `$imageTransparencyBG` | array | `[255, 255, 255]` | `[R, G, B]` | the RGB values for the transparent color, see [`imagecolortransparent()`](http://php.net/manual/function.imagecolortransparent.php) |
| `$pngCompression` | int | -1 | -1 ... 9 | `imagepng()` compression level, -1 = auto |
| `$jpegQuality` | int | 85 | 0 - 100 | `imagejpeg()` quality |
| `$imagickFormat` | string | 'png' | * | ImageMagick output type, see `Imagick::setType()` |
| `$imagickBG` | string | `null` | * | ImageMagick background color, see `ImagickPixel::__construct()` |
| `$moduleValues` | array | `null` | * | Module values map, see [[Custom output interface]] and `QROutputInterface::DEFAULT_MODULE_VALUES` |
## Framework Integration
- Drupal:
- [Google Authenticator Login `ga_login`](https://www.drupal.org/project/ga_login)
- Symfony
- [phpqrcode-bundle](https://github.com/jonasarts/phpqrcode-bundle)
- WordPress:
- [`wp-two-factor-auth`](https://github.com/sjinks/wp-two-factor-auth)
- [`simple-2fa`](https://wordpress.org/plugins/simple-2fa/)
- [`wordpress-seo`](https://github.com/Yoast/wordpress-seo)
- [`floating-share-button`](https://github.com/qriouslad/floating-share-button)
- WoltLab Suite
- [two-step-verification](http://pluginstore.woltlab.com/file/3007-two-step-verification/)
- [Appwrite](https://github.com/appwrite/appwrite)
- [Cachet](https://github.com/CachetHQ/Cachet)
- [twill](https://github.com/area17/twill)
- other uses: [dependents](https://github.com/chillerlan/php-qrcode/network/dependents) / [packages](https://github.com/chillerlan/php-qrcode/network/dependents?dependent_type=PACKAGE)
## Shameless advertising
Hi, please check out my other projects that are way cooler than qrcodes!
- [php-oauth-core](https://github.com/chillerlan/php-oauth-core) - an OAuth 1/2 client library along with a bunch of [providers](https://github.com/chillerlan/php-oauth-providers)
- [php-httpinterface](https://github.com/chillerlan/php-httpinterface) - a PSR-7/15/17/18 implemetation
- [php-database](https://github.com/chillerlan/php-database) - a database client & querybuilder for MySQL, Postgres, SQLite, MSSQL, Firebird
## Disclaimer!
I don't take responsibility for molten CPUs, misled applications, failed log-ins etc.. Use at your own risk!
### Trademark Notice
The word "QR Code" is a registered trademark of *DENSO WAVE INCORPORATED*<br>
https://www.qrcode.com/en/faq.html#patentH2Title

View File

@ -0,0 +1,62 @@
{
"name": "chillerlan/php-qrcode",
"description": "A QR code generator with a user friendly API. PHP 7.4+",
"homepage": "https://github.com/chillerlan/php-qrcode",
"license": "MIT",
"minimum-stability": "stable",
"type": "library",
"keywords": [
"QR code", "qrcode", "qr", "qrcode-generator", "phpqrcode"
],
"authors": [
{
"name": "Kazuhiko Arase",
"homepage": "https://github.com/kazuhikoarase"
},
{
"name": "Smiley",
"email": "smiley@chillerlan.net",
"homepage": "https://github.com/codemasher"
},
{
"name": "Contributors",
"homepage":"https://github.com/chillerlan/php-qrcode/graphs/contributors"
}
],
"require": {
"php": "^7.4 || ^8.0",
"ext-mbstring": "*",
"chillerlan/php-settings-container": "^2.1.6 || ^3.2.1"
},
"require-dev": {
"phan/phan": "^5.4.5",
"phpmd/phpmd": "^2.15",
"phpunit/phpunit": "^9.6",
"setasign/fpdf": "^1.8.2",
"squizlabs/php_codesniffer": "^3.11"
},
"suggest": {
"chillerlan/php-authenticator": "Yet another Google authenticator! Also creates URIs for mobile apps.",
"setasign/fpdf": "Required to use the QR FPDF output.",
"simple-icons/simple-icons": "SVG icons that you can use to embed as logos in the QR Code"
},
"autoload": {
"psr-4": {
"chillerlan\\QRCode\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"chillerlan\\QRCodeTest\\": "tests/"
}
},
"scripts": {
"phpunit": "@php vendor/bin/phpunit",
"phan": "@php vendor/bin/phan"
},
"config": {
"lock": false,
"sort-packages": true,
"platform-check": true
}
}

View File

@ -0,0 +1,60 @@
<?php
/**
* Class AlphaNum
*
* @filesource AlphaNum.php
* @created 25.11.2015
* @package chillerlan\QRCode\Data
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\QRCode;
use function ord, sprintf;
/**
* Alphanumeric mode: 0 to 9, A to Z, space, $ % * + - . / :
*
* ISO/IEC 18004:2000 Section 8.3.3
* ISO/IEC 18004:2000 Section 8.4.3
*/
final class AlphaNum extends QRDataAbstract{
protected int $datamode = QRCode::DATA_ALPHANUM;
protected array $lengthBits = [9, 11, 13];
/**
* @inheritdoc
*/
protected function write(string $data):void{
for($i = 0; $i + 1 < $this->strlen; $i += 2){
$this->bitBuffer->put($this->getCharCode($data[$i]) * 45 + $this->getCharCode($data[$i + 1]), 11);
}
if($i < $this->strlen){
$this->bitBuffer->put($this->getCharCode($data[$i]), 6);
}
}
/**
* get the code for the given character
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException on an illegal character occurence
*/
protected function getCharCode(string $chr):int{
if(!isset($this::CHAR_MAP_ALPHANUM[$chr])){
throw new QRCodeDataException(sprintf('illegal char: "%s" [%d]', $chr, ord($chr)));
}
return $this::CHAR_MAP_ALPHANUM[$chr];
}
}

View File

@ -0,0 +1,44 @@
<?php
/**
* Class Byte
*
* @filesource Byte.php
* @created 25.11.2015
* @package chillerlan\QRCode\Data
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\QRCode;
use function ord;
/**
* Byte mode, ISO-8859-1 or UTF-8
*
* ISO/IEC 18004:2000 Section 8.3.4
* ISO/IEC 18004:2000 Section 8.4.4
*/
final class Byte extends QRDataAbstract{
protected int $datamode = QRCode::DATA_BYTE;
protected array $lengthBits = [8, 16, 16];
/**
* @inheritdoc
*/
protected function write(string $data):void{
$i = 0;
while($i < $this->strlen){
$this->bitBuffer->put(ord($data[$i]), 8);
$i++;
}
}
}

View File

@ -0,0 +1,69 @@
<?php
/**
* Class Kanji
*
* @filesource Kanji.php
* @created 25.11.2015
* @package chillerlan\QRCode\Data
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\QRCode;
use function mb_strlen, ord, sprintf, strlen;
/**
* Kanji mode: double-byte characters from the Shift JIS character set
*
* ISO/IEC 18004:2000 Section 8.3.5
* ISO/IEC 18004:2000 Section 8.4.5
*/
final class Kanji extends QRDataAbstract{
protected int $datamode = QRCode::DATA_KANJI;
protected array $lengthBits = [8, 10, 12];
/**
* @inheritdoc
*/
protected function getLength(string $data):int{
return mb_strlen($data, 'SJIS');
}
/**
* @inheritdoc
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException on an illegal character occurence
*/
protected function write(string $data):void{
$len = strlen($data);
for($i = 0; $i + 1 < $len; $i += 2){
$c = ((0xff & ord($data[$i])) << 8) | (0xff & ord($data[$i + 1]));
if($c >= 0x8140 && $c <= 0x9FFC){
$c -= 0x8140;
}
elseif($c >= 0xE040 && $c <= 0xEBBF){
$c -= 0xC140;
}
else{
throw new QRCodeDataException(sprintf('illegal char at %d [%d]', $i + 1, $c));
}
$this->bitBuffer->put(((($c >> 8) & 0xff) * 0xC0) + ($c & 0xff), 13);
}
if($i < $len){
throw new QRCodeDataException(sprintf('illegal char at %d', $i + 1));
}
}
}

View File

@ -0,0 +1,203 @@
<?php
/**
* Class MaskPatternTester
*
* @filesource MaskPatternTester.php
* @created 22.11.2017
* @package chillerlan\QRCode\Data
* @author Smiley <smiley@chillerlan.net>
* @copyright 2017 Smiley
* @license MIT
*
* @noinspection PhpUnused
*/
namespace chillerlan\QRCode\Data;
use function abs, array_search, call_user_func_array, min;
/**
* Receives a QRDataInterface object and runs the mask pattern tests on it.
*
* ISO/IEC 18004:2000 Section 8.8.2 - Evaluation of masking results
*
* @see http://www.thonky.com/qr-code-tutorial/data-masking
*/
final class MaskPatternTester{
/**
* The data interface that contains the data matrix to test
*/
protected QRDataInterface $dataInterface;
/**
* Receives the QRDataInterface
*
* @see \chillerlan\QRCode\QROptions::$maskPattern
* @see \chillerlan\QRCode\Data\QRMatrix::$maskPattern
*/
public function __construct(QRDataInterface $dataInterface){
$this->dataInterface = $dataInterface;
}
/**
* shoves a QRMatrix through the MaskPatternTester to find the lowest penalty mask pattern
*
* @see \chillerlan\QRCode\Data\MaskPatternTester
*/
public function getBestMaskPattern():int{
$penalties = [];
for($pattern = 0; $pattern < 8; $pattern++){
$penalties[$pattern] = $this->testPattern($pattern);
}
return array_search(min($penalties), $penalties, true);
}
/**
* Returns the penalty for the given mask pattern
*
* @see \chillerlan\QRCode\QROptions::$maskPattern
* @see \chillerlan\QRCode\Data\QRMatrix::$maskPattern
*/
public function testPattern(int $pattern):int{
$matrix = $this->dataInterface->initMatrix($pattern, true);
$penalty = 0;
for($level = 1; $level <= 4; $level++){
$penalty += call_user_func_array([$this, 'testLevel'.$level], [$matrix->matrix(true), $matrix->size()]);
}
return (int)$penalty;
}
/**
* Checks for each group of five or more same-colored modules in a row (or column)
*/
protected function testLevel1(array $m, int $size):int{
$penalty = 0;
foreach($m as $y => $row){
foreach($row as $x => $val){
$count = 0;
for($ry = -1; $ry <= 1; $ry++){
if($y + $ry < 0 || $size <= $y + $ry){
continue;
}
for($rx = -1; $rx <= 1; $rx++){
if(($ry === 0 && $rx === 0) || (($x + $rx) < 0 || $size <= ($x + $rx))){
continue;
}
if($m[$y + $ry][$x + $rx] === $val){
$count++;
}
}
}
if($count > 5){
$penalty += (3 + $count - 5);
}
}
}
return $penalty;
}
/**
* Checks for each 2x2 area of same-colored modules in the matrix
*/
protected function testLevel2(array $m, int $size):int{
$penalty = 0;
foreach($m as $y => $row){
if($y > $size - 2){
break;
}
foreach($row as $x => $val){
if($x > $size - 2){
break;
}
if(
$val === $row[$x + 1]
&& $val === $m[$y + 1][$x]
&& $val === $m[$y + 1][$x + 1]
){
$penalty++;
}
}
}
return 3 * $penalty;
}
/**
* Checks if there are patterns that look similar to the finder patterns (1:1:3:1:1 ratio)
*/
protected function testLevel3(array $m, int $size):int{
$penalties = 0;
foreach($m as $y => $row){
foreach($row as $x => $val){
if(
$x + 6 < $size
&& $val
&& !$row[$x + 1]
&& $row[$x + 2]
&& $row[$x + 3]
&& $row[$x + 4]
&& !$row[$x + 5]
&& $row[$x + 6]
){
$penalties++;
}
if(
$y + 6 < $size
&& $val
&& !$m[$y + 1][$x]
&& $m[$y + 2][$x]
&& $m[$y + 3][$x]
&& $m[$y + 4][$x]
&& !$m[$y + 5][$x]
&& $m[$y + 6][$x]
){
$penalties++;
}
}
}
return $penalties * 40;
}
/**
* Checks if more than half of the modules are dark or light, with a larger penalty for a larger difference
*/
protected function testLevel4(array $m, int $size):float{
$count = 0;
foreach($m as $row){
foreach($row as $val){
if($val){
$count++;
}
}
}
return (abs(100 * $count / $size / $size - 50) / 5) * 10;
}
}

View File

@ -0,0 +1,77 @@
<?php
/**
* Class Number
*
* @filesource Number.php
* @created 26.11.2015
* @package chillerlan\QRCode\Data
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\QRCode;
use function ord, sprintf, str_split, substr;
/**
* Numeric mode: decimal digits 0 to 9
*
* ISO/IEC 18004:2000 Section 8.3.2
* ISO/IEC 18004:2000 Section 8.4.2
*/
final class Number extends QRDataAbstract{
protected int $datamode = QRCode::DATA_NUMBER;
protected array $lengthBits = [10, 12, 14];
/**
* @inheritdoc
*/
protected function write(string $data):void{
$i = 0;
while($i + 2 < $this->strlen){
$this->bitBuffer->put($this->parseInt(substr($data, $i, 3)), 10);
$i += 3;
}
if($i < $this->strlen){
if($this->strlen - $i === 1){
$this->bitBuffer->put($this->parseInt(substr($data, $i, $i + 1)), 4);
}
elseif($this->strlen - $i === 2){
$this->bitBuffer->put($this->parseInt(substr($data, $i, $i + 2)), 7);
}
}
}
/**
* get the code for the given numeric string
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException on an illegal character occurence
*/
protected function parseInt(string $string):int{
$num = 0;
foreach(str_split($string) as $chr){
$c = ord($chr);
if(!isset($this::CHAR_MAP_NUMBER[$chr])){
throw new QRCodeDataException(sprintf('illegal char: "%s" [%d]', $chr, $c));
}
$c = $c - 48; // ord('0')
$num = $num * 10 + $c;
}
return $num;
}
}

View File

@ -0,0 +1,17 @@
<?php
/**
* Class QRCodeDataException
*
* @filesource QRCodeDataException.php
* @created 09.12.2015
* @package chillerlan\QRCode\Data
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\QRCodeException;
class QRCodeDataException extends QRCodeException{}

View File

@ -0,0 +1,311 @@
<?php
/**
* Class QRDataAbstract
*
* @filesource QRDataAbstract.php
* @created 25.11.2015
* @package chillerlan\QRCode\Data
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\Helpers\{BitBuffer, Polynomial};
use chillerlan\Settings\SettingsContainerInterface;
use function array_fill, array_merge, count, max, mb_convert_encoding, mb_detect_encoding, range, sprintf, strlen;
/**
* Processes the binary data and maps it on a matrix which is then being returned
*/
abstract class QRDataAbstract implements QRDataInterface{
/**
* the string byte count
*/
protected ?int $strlen = null;
/**
* the current data mode: Num, Alphanum, Kanji, Byte
*/
protected int $datamode;
/**
* mode length bits for the version breakpoints 1-9, 10-26 and 27-40
*
* ISO/IEC 18004:2000 Table 3 - Number of bits in Character Count Indicator
*/
protected array $lengthBits = [0, 0, 0];
/**
* current QR Code version
*/
protected int $version;
/**
* ECC temp data
*/
protected array $ecdata;
/**
* ECC temp data
*/
protected array $dcdata;
/**
* the options instance
*
* @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\QRCode\QROptions
*/
protected SettingsContainerInterface $options;
/**
* a BitBuffer instance
*/
protected BitBuffer $bitBuffer;
/**
* QRDataInterface constructor.
*/
public function __construct(SettingsContainerInterface $options, ?string $data = null){
$this->options = $options;
if($data !== null){
$this->setData($data);
}
}
/**
* @inheritDoc
*/
public function setData(string $data):QRDataInterface{
if($this->datamode === QRCode::DATA_KANJI){
$data = mb_convert_encoding($data, 'SJIS', mb_detect_encoding($data));
}
$this->strlen = $this->getLength($data);
$this->version = $this->options->version === QRCode::VERSION_AUTO
? $this->getMinimumVersion()
: $this->options->version;
$this->writeBitBuffer($data);
return $this;
}
/**
* @inheritDoc
*/
public function initMatrix(int $maskPattern, ?bool $test = null):QRMatrix{
return (new QRMatrix($this->version, $this->options->eccLevel))
->init($maskPattern, $test)
->mapData($this->maskECC(), $maskPattern)
;
}
/**
* returns the length bits for the version breakpoints 1-9, 10-26 and 27-40
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
* @codeCoverageIgnore
*/
protected function getLengthBits():int{
foreach([9, 26, 40] as $key => $breakpoint){
if($this->version <= $breakpoint){
return $this->lengthBits[$key];
}
}
throw new QRCodeDataException(sprintf('invalid version number: %d', $this->version));
}
/**
* returns the byte count of the $data string
*/
protected function getLength(string $data):int{
return strlen($data);
}
/**
* returns the minimum version number for the given string
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
protected function getMinimumVersion():int{
$maxlength = 0;
// guess the version number within the given range
$dataMode = QRCode::DATA_MODES[$this->datamode];
$eccMode = QRCode::ECC_MODES[$this->options->eccLevel];
foreach(range($this->options->versionMin, $this->options->versionMax) as $version){
$maxlength = $this::MAX_LENGTH[$version][$dataMode][$eccMode];
if($this->strlen <= $maxlength){
return $version;
}
}
throw new QRCodeDataException(sprintf('data exceeds %d characters', $maxlength));
}
/**
* writes the actual data string to the BitBuffer
*
* @see \chillerlan\QRCode\Data\QRDataAbstract::writeBitBuffer()
*/
abstract protected function write(string $data):void;
/**
* creates a BitBuffer and writes the string data to it
*
* @throws \chillerlan\QRCode\QRCodeException on data overflow
*/
protected function writeBitBuffer(string $data):void{
$this->bitBuffer = new BitBuffer;
$MAX_BITS = $this::MAX_BITS[$this->version][QRCode::ECC_MODES[$this->options->eccLevel]];
$this->bitBuffer
->put($this->datamode, 4)
->put($this->strlen, $this->getLengthBits())
;
$this->write($data);
// overflow, likely caused due to invalid version setting
if($this->bitBuffer->getLength() > $MAX_BITS){
throw new QRCodeDataException(sprintf('code length overflow. (%d > %d bit)', $this->bitBuffer->getLength(), $MAX_BITS));
}
// add terminator (ISO/IEC 18004:2000 Table 2)
if($this->bitBuffer->getLength() + 4 <= $MAX_BITS){
$this->bitBuffer->put(0, 4);
}
// padding
while($this->bitBuffer->getLength() % 8 !== 0){
$this->bitBuffer->putBit(false);
}
// padding
while(true){
if($this->bitBuffer->getLength() >= $MAX_BITS){
break;
}
$this->bitBuffer->put(0xEC, 8);
if($this->bitBuffer->getLength() >= $MAX_BITS){
break;
}
$this->bitBuffer->put(0x11, 8);
}
}
/**
* ECC masking
*
* ISO/IEC 18004:2000 Section 8.5 ff
*
* @see http://www.thonky.com/qr-code-tutorial/error-correction-coding
*/
protected function maskECC():array{
[$l1, $l2, $b1, $b2] = $this::RSBLOCKS[$this->version][QRCode::ECC_MODES[$this->options->eccLevel]];
$rsBlocks = array_fill(0, $l1, [$b1, $b2]);
$rsCount = $l1 + $l2;
$this->ecdata = array_fill(0, $rsCount, []);
$this->dcdata = $this->ecdata;
if($l2 > 0){
$rsBlocks = array_merge($rsBlocks, array_fill(0, $l2, [$b1 + 1, $b2 + 1]));
}
$totalCodeCount = 0;
$maxDcCount = 0;
$maxEcCount = 0;
$offset = 0;
$bitBuffer = $this->bitBuffer->getBuffer();
foreach($rsBlocks as $key => $block){
[$rsBlockTotal, $dcCount] = $block;
$ecCount = $rsBlockTotal - $dcCount;
$maxDcCount = max($maxDcCount, $dcCount);
$maxEcCount = max($maxEcCount, $ecCount);
$this->dcdata[$key] = array_fill(0, $dcCount, null);
foreach($this->dcdata[$key] as $a => $_z){
$this->dcdata[$key][$a] = 0xff & $bitBuffer[$a + $offset];
}
[$num, $add] = $this->poly($key, $ecCount);
foreach($this->ecdata[$key] as $c => $_){
$modIndex = $c + $add;
$this->ecdata[$key][$c] = $modIndex >= 0 ? $num[$modIndex] : 0;
}
$offset += $dcCount;
$totalCodeCount += $rsBlockTotal;
}
$data = array_fill(0, $totalCodeCount, null);
$index = 0;
$mask = function(array $arr, int $count) use (&$data, &$index, $rsCount):void{
for($x = 0; $x < $count; $x++){
for($y = 0; $y < $rsCount; $y++){
if($x < count($arr[$y])){
$data[$index] = $arr[$y][$x];
$index++;
}
}
}
};
$mask($this->dcdata, $maxDcCount);
$mask($this->ecdata, $maxEcCount);
return $data;
}
/**
* helper method for the polynomial operations
*/
protected function poly(int $key, int $count):array{
$rsPoly = new Polynomial;
$modPoly = new Polynomial;
for($i = 0; $i < $count; $i++){
$modPoly->setNum([1, $modPoly->gexp($i)]);
$rsPoly->multiply($modPoly->getNum());
}
$rsPolyCount = count($rsPoly->getNum());
$modPoly
->setNum($this->dcdata[$key], $rsPolyCount - 1)
->mod($rsPoly->getNum())
;
$this->ecdata[$key] = array_fill(0, $rsPolyCount - 1, null);
$num = $modPoly->getNum();
return [
$num,
count($num) - count($this->ecdata[$key]),
];
}
}

View File

@ -0,0 +1,200 @@
<?php
/**
* Interface QRDataInterface
*
* @filesource QRDataInterface.php
* @created 01.12.2015
* @package chillerlan\QRCode\Data
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
/**
* Specifies the methods reqired for the data modules (Number, Alphanum, Byte and Kanji)
* and holds version information in several constants
*/
interface QRDataInterface{
/**
* @var int[]
*/
public const CHAR_MAP_NUMBER = [
'0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9,
];
/**
* ISO/IEC 18004:2000 Table 5
*
* @var int[]
*/
public const CHAR_MAP_ALPHANUM = [
'0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5, '6' => 6, '7' => 7,
'8' => 8, '9' => 9, 'A' => 10, 'B' => 11, 'C' => 12, 'D' => 13, 'E' => 14, 'F' => 15,
'G' => 16, 'H' => 17, 'I' => 18, 'J' => 19, 'K' => 20, 'L' => 21, 'M' => 22, 'N' => 23,
'O' => 24, 'P' => 25, 'Q' => 26, 'R' => 27, 'S' => 28, 'T' => 29, 'U' => 30, 'V' => 31,
'W' => 32, 'X' => 33, 'Y' => 34, 'Z' => 35, ' ' => 36, '$' => 37, '%' => 38, '*' => 39,
'+' => 40, '-' => 41, '.' => 42, '/' => 43, ':' => 44,
];
/**
* ISO/IEC 18004:2000 Tables 7-11 - Number of symbol characters and input data capacity for versions 1 to 40
*
* @see http://www.qrcode.com/en/about/version.html
*
* @var int [][][]
*/
public const MAX_LENGTH =[
// v => [NUMERIC => [L, M, Q, H ], ALPHANUM => [L, M, Q, H], BINARY => [L, M, Q, H ], KANJI => [L, M, Q, H ]] // modules
1 => [[ 41, 34, 27, 17], [ 25, 20, 16, 10], [ 17, 14, 11, 7], [ 10, 8, 7, 4]], // 21
2 => [[ 77, 63, 48, 34], [ 47, 38, 29, 20], [ 32, 26, 20, 14], [ 20, 16, 12, 8]], // 25
3 => [[ 127, 101, 77, 58], [ 77, 61, 47, 35], [ 53, 42, 32, 24], [ 32, 26, 20, 15]], // 29
4 => [[ 187, 149, 111, 82], [ 114, 90, 67, 50], [ 78, 62, 46, 34], [ 48, 38, 28, 21]], // 33
5 => [[ 255, 202, 144, 106], [ 154, 122, 87, 64], [ 106, 84, 60, 44], [ 65, 52, 37, 27]], // 37
6 => [[ 322, 255, 178, 139], [ 195, 154, 108, 84], [ 134, 106, 74, 58], [ 82, 65, 45, 36]], // 41
7 => [[ 370, 293, 207, 154], [ 224, 178, 125, 93], [ 154, 122, 86, 64], [ 95, 75, 53, 39]], // 45
8 => [[ 461, 365, 259, 202], [ 279, 221, 157, 122], [ 192, 152, 108, 84], [ 118, 93, 66, 52]], // 49
9 => [[ 552, 432, 312, 235], [ 335, 262, 189, 143], [ 230, 180, 130, 98], [ 141, 111, 80, 60]], // 53
10 => [[ 652, 513, 364, 288], [ 395, 311, 221, 174], [ 271, 213, 151, 119], [ 167, 131, 93, 74]], // 57
11 => [[ 772, 604, 427, 331], [ 468, 366, 259, 200], [ 321, 251, 177, 137], [ 198, 155, 109, 85]], // 61
12 => [[ 883, 691, 489, 374], [ 535, 419, 296, 227], [ 367, 287, 203, 155], [ 226, 177, 125, 96]], // 65
13 => [[1022, 796, 580, 427], [ 619, 483, 352, 259], [ 425, 331, 241, 177], [ 262, 204, 149, 109]], // 69 NICE!
14 => [[1101, 871, 621, 468], [ 667, 528, 376, 283], [ 458, 362, 258, 194], [ 282, 223, 159, 120]], // 73
15 => [[1250, 991, 703, 530], [ 758, 600, 426, 321], [ 520, 412, 292, 220], [ 320, 254, 180, 136]], // 77
16 => [[1408, 1082, 775, 602], [ 854, 656, 470, 365], [ 586, 450, 322, 250], [ 361, 277, 198, 154]], // 81
17 => [[1548, 1212, 876, 674], [ 938, 734, 531, 408], [ 644, 504, 364, 280], [ 397, 310, 224, 173]], // 85
18 => [[1725, 1346, 948, 746], [1046, 816, 574, 452], [ 718, 560, 394, 310], [ 442, 345, 243, 191]], // 89
19 => [[1903, 1500, 1063, 813], [1153, 909, 644, 493], [ 792, 624, 442, 338], [ 488, 384, 272, 208]], // 93
20 => [[2061, 1600, 1159, 919], [1249, 970, 702, 557], [ 858, 666, 482, 382], [ 528, 410, 297, 235]], // 97
21 => [[2232, 1708, 1224, 969], [1352, 1035, 742, 587], [ 929, 711, 509, 403], [ 572, 438, 314, 248]], // 101
22 => [[2409, 1872, 1358, 1056], [1460, 1134, 823, 640], [1003, 779, 565, 439], [ 618, 480, 348, 270]], // 105
23 => [[2620, 2059, 1468, 1108], [1588, 1248, 890, 672], [1091, 857, 611, 461], [ 672, 528, 376, 284]], // 109
24 => [[2812, 2188, 1588, 1228], [1704, 1326, 963, 744], [1171, 911, 661, 511], [ 721, 561, 407, 315]], // 113
25 => [[3057, 2395, 1718, 1286], [1853, 1451, 1041, 779], [1273, 997, 715, 535], [ 784, 614, 440, 330]], // 117
26 => [[3283, 2544, 1804, 1425], [1990, 1542, 1094, 864], [1367, 1059, 751, 593], [ 842, 652, 462, 365]], // 121
27 => [[3517, 2701, 1933, 1501], [2132, 1637, 1172, 910], [1465, 1125, 805, 625], [ 902, 692, 496, 385]], // 125
28 => [[3669, 2857, 2085, 1581], [2223, 1732, 1263, 958], [1528, 1190, 868, 658], [ 940, 732, 534, 405]], // 129
29 => [[3909, 3035, 2181, 1677], [2369, 1839, 1322, 1016], [1628, 1264, 908, 698], [1002, 778, 559, 430]], // 133
30 => [[4158, 3289, 2358, 1782], [2520, 1994, 1429, 1080], [1732, 1370, 982, 742], [1066, 843, 604, 457]], // 137
31 => [[4417, 3486, 2473, 1897], [2677, 2113, 1499, 1150], [1840, 1452, 1030, 790], [1132, 894, 634, 486]], // 141
32 => [[4686, 3693, 2670, 2022], [2840, 2238, 1618, 1226], [1952, 1538, 1112, 842], [1201, 947, 684, 518]], // 145
33 => [[4965, 3909, 2805, 2157], [3009, 2369, 1700, 1307], [2068, 1628, 1168, 898], [1273, 1002, 719, 553]], // 149
34 => [[5253, 4134, 2949, 2301], [3183, 2506, 1787, 1394], [2188, 1722, 1228, 958], [1347, 1060, 756, 590]], // 153
35 => [[5529, 4343, 3081, 2361], [3351, 2632, 1867, 1431], [2303, 1809, 1283, 983], [1417, 1113, 790, 605]], // 157
36 => [[5836, 4588, 3244, 2524], [3537, 2780, 1966, 1530], [2431, 1911, 1351, 1051], [1496, 1176, 832, 647]], // 161
37 => [[6153, 4775, 3417, 2625], [3729, 2894, 2071, 1591], [2563, 1989, 1423, 1093], [1577, 1224, 876, 673]], // 165
38 => [[6479, 5039, 3599, 2735], [3927, 3054, 2181, 1658], [2699, 2099, 1499, 1139], [1661, 1292, 923, 701]], // 169
39 => [[6743, 5313, 3791, 2927], [4087, 3220, 2298, 1774], [2809, 2213, 1579, 1219], [1729, 1362, 972, 750]], // 173
40 => [[7089, 5596, 3993, 3057], [4296, 3391, 2420, 1852], [2953, 2331, 1663, 1273], [1817, 1435, 1024, 784]], // 177
];
/**
* ISO/IEC 18004:2000 Tables 7-11 - Number of symbol characters and input data capacity for versions 1 to 40
*
* @var int [][]
*/
public const MAX_BITS = [
// version => [L, M, Q, H ]
1 => [ 152, 128, 104, 72],
2 => [ 272, 224, 176, 128],
3 => [ 440, 352, 272, 208],
4 => [ 640, 512, 384, 288],
5 => [ 864, 688, 496, 368],
6 => [ 1088, 864, 608, 480],
7 => [ 1248, 992, 704, 528],
8 => [ 1552, 1232, 880, 688],
9 => [ 1856, 1456, 1056, 800],
10 => [ 2192, 1728, 1232, 976],
11 => [ 2592, 2032, 1440, 1120],
12 => [ 2960, 2320, 1648, 1264],
13 => [ 3424, 2672, 1952, 1440],
14 => [ 3688, 2920, 2088, 1576],
15 => [ 4184, 3320, 2360, 1784],
16 => [ 4712, 3624, 2600, 2024],
17 => [ 5176, 4056, 2936, 2264],
18 => [ 5768, 4504, 3176, 2504],
19 => [ 6360, 5016, 3560, 2728],
20 => [ 6888, 5352, 3880, 3080],
21 => [ 7456, 5712, 4096, 3248],
22 => [ 8048, 6256, 4544, 3536],
23 => [ 8752, 6880, 4912, 3712],
24 => [ 9392, 7312, 5312, 4112],
25 => [10208, 8000, 5744, 4304],
26 => [10960, 8496, 6032, 4768],
27 => [11744, 9024, 6464, 5024],
28 => [12248, 9544, 6968, 5288],
29 => [13048, 10136, 7288, 5608],
30 => [13880, 10984, 7880, 5960],
31 => [14744, 11640, 8264, 6344],
32 => [15640, 12328, 8920, 6760],
33 => [16568, 13048, 9368, 7208],
34 => [17528, 13800, 9848, 7688],
35 => [18448, 14496, 10288, 7888],
36 => [19472, 15312, 10832, 8432],
37 => [20528, 15936, 11408, 8768],
38 => [21616, 16816, 12016, 9136],
39 => [22496, 17728, 12656, 9776],
40 => [23648, 18672, 13328, 10208],
];
/**
* @see http://www.thonky.com/qr-code-tutorial/error-correction-table
*
* @var int [][][]
*/
public const RSBLOCKS = [
1 => [[ 1, 0, 26, 19], [ 1, 0, 26, 16], [ 1, 0, 26, 13], [ 1, 0, 26, 9]],
2 => [[ 1, 0, 44, 34], [ 1, 0, 44, 28], [ 1, 0, 44, 22], [ 1, 0, 44, 16]],
3 => [[ 1, 0, 70, 55], [ 1, 0, 70, 44], [ 2, 0, 35, 17], [ 2, 0, 35, 13]],
4 => [[ 1, 0, 100, 80], [ 2, 0, 50, 32], [ 2, 0, 50, 24], [ 4, 0, 25, 9]],
5 => [[ 1, 0, 134, 108], [ 2, 0, 67, 43], [ 2, 2, 33, 15], [ 2, 2, 33, 11]],
6 => [[ 2, 0, 86, 68], [ 4, 0, 43, 27], [ 4, 0, 43, 19], [ 4, 0, 43, 15]],
7 => [[ 2, 0, 98, 78], [ 4, 0, 49, 31], [ 2, 4, 32, 14], [ 4, 1, 39, 13]],
8 => [[ 2, 0, 121, 97], [ 2, 2, 60, 38], [ 4, 2, 40, 18], [ 4, 2, 40, 14]],
9 => [[ 2, 0, 146, 116], [ 3, 2, 58, 36], [ 4, 4, 36, 16], [ 4, 4, 36, 12]],
10 => [[ 2, 2, 86, 68], [ 4, 1, 69, 43], [ 6, 2, 43, 19], [ 6, 2, 43, 15]],
11 => [[ 4, 0, 101, 81], [ 1, 4, 80, 50], [ 4, 4, 50, 22], [ 3, 8, 36, 12]],
12 => [[ 2, 2, 116, 92], [ 6, 2, 58, 36], [ 4, 6, 46, 20], [ 7, 4, 42, 14]],
13 => [[ 4, 0, 133, 107], [ 8, 1, 59, 37], [ 8, 4, 44, 20], [12, 4, 33, 11]],
14 => [[ 3, 1, 145, 115], [ 4, 5, 64, 40], [11, 5, 36, 16], [11, 5, 36, 12]],
15 => [[ 5, 1, 109, 87], [ 5, 5, 65, 41], [ 5, 7, 54, 24], [11, 7, 36, 12]],
16 => [[ 5, 1, 122, 98], [ 7, 3, 73, 45], [15, 2, 43, 19], [ 3, 13, 45, 15]],
17 => [[ 1, 5, 135, 107], [10, 1, 74, 46], [ 1, 15, 50, 22], [ 2, 17, 42, 14]],
18 => [[ 5, 1, 150, 120], [ 9, 4, 69, 43], [17, 1, 50, 22], [ 2, 19, 42, 14]],
19 => [[ 3, 4, 141, 113], [ 3, 11, 70, 44], [17, 4, 47, 21], [ 9, 16, 39, 13]],
20 => [[ 3, 5, 135, 107], [ 3, 13, 67, 41], [15, 5, 54, 24], [15, 10, 43, 15]],
21 => [[ 4, 4, 144, 116], [17, 0, 68, 42], [17, 6, 50, 22], [19, 6, 46, 16]],
22 => [[ 2, 7, 139, 111], [17, 0, 74, 46], [ 7, 16, 54, 24], [34, 0, 37, 13]],
23 => [[ 4, 5, 151, 121], [ 4, 14, 75, 47], [11, 14, 54, 24], [16, 14, 45, 15]],
24 => [[ 6, 4, 147, 117], [ 6, 14, 73, 45], [11, 16, 54, 24], [30, 2, 46, 16]],
25 => [[ 8, 4, 132, 106], [ 8, 13, 75, 47], [ 7, 22, 54, 24], [22, 13, 45, 15]],
26 => [[10, 2, 142, 114], [19, 4, 74, 46], [28, 6, 50, 22], [33, 4, 46, 16]],
27 => [[ 8, 4, 152, 122], [22, 3, 73, 45], [ 8, 26, 53, 23], [12, 28, 45, 15]],
28 => [[ 3, 10, 147, 117], [ 3, 23, 73, 45], [ 4, 31, 54, 24], [11, 31, 45, 15]],
29 => [[ 7, 7, 146, 116], [21, 7, 73, 45], [ 1, 37, 53, 23], [19, 26, 45, 15]],
30 => [[ 5, 10, 145, 115], [19, 10, 75, 47], [15, 25, 54, 24], [23, 25, 45, 15]],
31 => [[13, 3, 145, 115], [ 2, 29, 74, 46], [42, 1, 54, 24], [23, 28, 45, 15]],
32 => [[17, 0, 145, 115], [10, 23, 74, 46], [10, 35, 54, 24], [19, 35, 45, 15]],
33 => [[17, 1, 145, 115], [14, 21, 74, 46], [29, 19, 54, 24], [11, 46, 45, 15]],
34 => [[13, 6, 145, 115], [14, 23, 74, 46], [44, 7, 54, 24], [59, 1, 46, 16]],
35 => [[12, 7, 151, 121], [12, 26, 75, 47], [39, 14, 54, 24], [22, 41, 45, 15]],
36 => [[ 6, 14, 151, 121], [ 6, 34, 75, 47], [46, 10, 54, 24], [ 2, 64, 45, 15]],
37 => [[17, 4, 152, 122], [29, 14, 74, 46], [49, 10, 54, 24], [24, 46, 45, 15]],
38 => [[ 4, 18, 152, 122], [13, 32, 74, 46], [48, 14, 54, 24], [42, 32, 45, 15]],
39 => [[20, 4, 147, 117], [40, 7, 75, 47], [43, 22, 54, 24], [10, 67, 45, 15]],
40 => [[19, 6, 148, 118], [18, 31, 75, 47], [34, 34, 54, 24], [20, 61, 45, 15]],
];
/**
* Sets the data string (internally called by the constructor)
*/
public function setData(string $data):QRDataInterface;
/**
* returns a fresh matrix object with the data written for the given $maskPattern
*/
public function initMatrix(int $maskPattern, ?bool $test = null):QRMatrix;
}

View File

@ -0,0 +1,779 @@
<?php
/**
* Class QRMatrix
*
* @filesource QRMatrix.php
* @created 15.11.2017
* @package chillerlan\QRCode\Data
* @author Smiley <smiley@chillerlan.net>
* @copyright 2017 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\QRCode;
use Closure;
use function array_fill, array_key_exists, array_push, array_unshift, count, floor, in_array, max, min, range;
/**
* Holds a numerical representation of the final QR Code;
* maps the ECC coded binary data and applies the mask pattern
*
* @see http://www.thonky.com/qr-code-tutorial/format-version-information
*/
final class QRMatrix{
/*
* special values
*/
/** @var int */
public const M_NULL = 0x00;
/** @var int */
public const M_LOGO = 0x14;
/** @var int */
public const M_LOGO_DARK = self::M_LOGO << 8;
/*
* light values
*/
/** @var int */
public const M_DATA = 0x04;
/** @var int */
public const M_FINDER = 0x06;
/** @var int */
public const M_SEPARATOR = 0x08;
/** @var int */
public const M_ALIGNMENT = 0x0a;
/** @var int */
public const M_TIMING = 0x0c;
/** @var int */
public const M_FORMAT = 0x0e;
/** @var int */
public const M_VERSION = 0x10;
/** @var int */
public const M_QUIETZONE = 0x12;
/*
* dark values
*/
/** @var int */
public const M_DARKMODULE = self::M_DARKMODULE_LIGHT << 8;
/** @var int */
public const M_DATA_DARK = self::M_DATA << 8;
/** @var int */
public const M_FINDER_DARK = self::M_FINDER << 8;
/** @var int */
public const M_ALIGNMENT_DARK = self::M_ALIGNMENT << 8;
/** @var int */
public const M_TIMING_DARK = self::M_TIMING << 8;
/** @var int */
public const M_FORMAT_DARK = self::M_FORMAT << 8;
/** @var int */
public const M_VERSION_DARK = self::M_VERSION << 8;
/** @var int */
public const M_FINDER_DOT = self::M_FINDER_DOT_LIGHT << 8;
/*
* values used for reversed reflectance
*/
/** @var int */
public const M_DARKMODULE_LIGHT = 0x02;
/** @var int */
public const M_FINDER_DOT_LIGHT = 0x16;
/** @var int */
public const M_SEPARATOR_DARK = self::M_SEPARATOR << 8;
/** @var int */
public const M_QUIETZONE_DARK = self::M_QUIETZONE << 8;
/**
* ISO/IEC 18004:2000 Annex E, Table E.1 - Row/column coordinates of center module of Alignment Patterns
*
* version -> pattern
*
* @var int[][]
*/
protected const alignmentPattern = [
1 => [],
2 => [6, 18],
3 => [6, 22],
4 => [6, 26],
5 => [6, 30],
6 => [6, 34],
7 => [6, 22, 38],
8 => [6, 24, 42],
9 => [6, 26, 46],
10 => [6, 28, 50],
11 => [6, 30, 54],
12 => [6, 32, 58],
13 => [6, 34, 62],
14 => [6, 26, 46, 66],
15 => [6, 26, 48, 70],
16 => [6, 26, 50, 74],
17 => [6, 30, 54, 78],
18 => [6, 30, 56, 82],
19 => [6, 30, 58, 86],
20 => [6, 34, 62, 90],
21 => [6, 28, 50, 72, 94],
22 => [6, 26, 50, 74, 98],
23 => [6, 30, 54, 78, 102],
24 => [6, 28, 54, 80, 106],
25 => [6, 32, 58, 84, 110],
26 => [6, 30, 58, 86, 114],
27 => [6, 34, 62, 90, 118],
28 => [6, 26, 50, 74, 98, 122],
29 => [6, 30, 54, 78, 102, 126],
30 => [6, 26, 52, 78, 104, 130],
31 => [6, 30, 56, 82, 108, 134],
32 => [6, 34, 60, 86, 112, 138],
33 => [6, 30, 58, 86, 114, 142],
34 => [6, 34, 62, 90, 118, 146],
35 => [6, 30, 54, 78, 102, 126, 150],
36 => [6, 24, 50, 76, 102, 128, 154],
37 => [6, 28, 54, 80, 106, 132, 158],
38 => [6, 32, 58, 84, 110, 136, 162],
39 => [6, 26, 54, 82, 110, 138, 166],
40 => [6, 30, 58, 86, 114, 142, 170],
];
/**
* ISO/IEC 18004:2000 Annex D, Table D.1 - Version information bit stream for each version
*
* no version pattern for QR Codes < 7
*
* @var int[]
*/
protected const versionPattern = [
7 => 0b000111110010010100,
8 => 0b001000010110111100,
9 => 0b001001101010011001,
10 => 0b001010010011010011,
11 => 0b001011101111110110,
12 => 0b001100011101100010,
13 => 0b001101100001000111,
14 => 0b001110011000001101,
15 => 0b001111100100101000,
16 => 0b010000101101111000,
17 => 0b010001010001011101,
18 => 0b010010101000010111,
19 => 0b010011010100110010,
20 => 0b010100100110100110,
21 => 0b010101011010000011,
22 => 0b010110100011001001,
23 => 0b010111011111101100,
24 => 0b011000111011000100,
25 => 0b011001000111100001,
26 => 0b011010111110101011,
27 => 0b011011000010001110,
28 => 0b011100110000011010,
29 => 0b011101001100111111,
30 => 0b011110110101110101,
31 => 0b011111001001010000,
32 => 0b100000100111010101,
33 => 0b100001011011110000,
34 => 0b100010100010111010,
35 => 0b100011011110011111,
36 => 0b100100101100001011,
37 => 0b100101010000101110,
38 => 0b100110101001100100,
39 => 0b100111010101000001,
40 => 0b101000110001101001,
];
/**
* ISO/IEC 18004:2000 Section 8.9 - Format Information
*
* ECC level -> mask pattern
*
* @var int[][]
*/
protected const formatPattern = [
[ // L
0b111011111000100,
0b111001011110011,
0b111110110101010,
0b111100010011101,
0b110011000101111,
0b110001100011000,
0b110110001000001,
0b110100101110110,
],
[ // M
0b101010000010010,
0b101000100100101,
0b101111001111100,
0b101101101001011,
0b100010111111001,
0b100000011001110,
0b100111110010111,
0b100101010100000,
],
[ // Q
0b011010101011111,
0b011000001101000,
0b011111100110001,
0b011101000000110,
0b010010010110100,
0b010000110000011,
0b010111011011010,
0b010101111101101,
],
[ // H
0b001011010001001,
0b001001110111110,
0b001110011100111,
0b001100111010000,
0b000011101100010,
0b000001001010101,
0b000110100001100,
0b000100000111011,
],
];
/**
* the current QR Code version number
*/
protected int $version;
/**
* the current ECC level
*/
protected int $eclevel;
/**
* the used mask pattern, set via QRMatrix::mapData()
*/
protected int $maskPattern = QRCode::MASK_PATTERN_AUTO;
/**
* the size (side length) of the matrix
*/
protected int $moduleCount;
/**
* the actual matrix data array
*
* @var int[][]
*/
protected array $matrix;
/**
* QRMatrix constructor.
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public function __construct(int $version, int $eclevel){
if(!in_array($version, range(1, 40), true)){
throw new QRCodeDataException('invalid QR Code version');
}
if(!array_key_exists($eclevel, QRCode::ECC_MODES)){
throw new QRCodeDataException('invalid ecc level');
}
$this->version = $version;
$this->eclevel = $eclevel;
$this->moduleCount = $this->version * 4 + 17;
$this->matrix = array_fill(0, $this->moduleCount, array_fill(0, $this->moduleCount, $this::M_NULL));
}
/**
* shortcut to initialize the matrix
*/
public function init(int $maskPattern, ?bool $test = null):QRMatrix{
return $this
->setFinderPattern()
->setSeparators()
->setAlignmentPattern()
->setTimingPattern()
->setVersionNumber($test)
->setFormatInfo($maskPattern, $test)
->setDarkModule()
;
}
/**
* Returns the data matrix, returns a pure boolean representation if $boolean is set to true
*
* @return int[][]|bool[][]
*/
public function matrix(bool $boolean = false):array{
if(!$boolean){
return $this->matrix;
}
$matrix = [];
foreach($this->matrix as $y => $row){
$matrix[$y] = [];
foreach($row as $x => $val){
$matrix[$y][$x] = ($val >> 8) > 0;
}
}
return $matrix;
}
/**
* Returns the current version number
*/
public function version():int{
return $this->version;
}
/**
* Returns the current ECC level
*/
public function eccLevel():int{
return $this->eclevel;
}
/**
* Returns the current mask pattern
*/
public function maskPattern():int{
return $this->maskPattern;
}
/**
* Returns the absoulute size of the matrix, including quiet zone (after setting it).
*
* size = version * 4 + 17 [ + 2 * quietzone size]
*/
public function size():int{
return $this->moduleCount;
}
/**
* Returns the value of the module at position [$x, $y]
*/
public function get(int $x, int $y):int{
return $this->matrix[$y][$x];
}
/**
* Sets the $M_TYPE value for the module at position [$x, $y]
*
* true => $M_TYPE << 8
* false => $M_TYPE
*/
public function set(int $x, int $y, bool $value, int $M_TYPE):QRMatrix{
$this->matrix[$y][$x] = $M_TYPE << ($value ? 8 : 0);
return $this;
}
/**
* Checks whether a module is true (dark) or false (light)
*
* true => $value >> 8 === $M_TYPE
* $value >> 8 > 0
*
* false => $value === $M_TYPE
* $value >> 8 === 0
*/
public function check(int $x, int $y):bool{
return ($this->matrix[$y][$x] >> 8) > 0;
}
/**
* Sets the "dark module", that is always on the same position 1x1px away from the bottom left finder
*/
public function setDarkModule():QRMatrix{
$this->set(8, 4 * $this->version + 9, true, $this::M_DARKMODULE_LIGHT);
return $this;
}
/**
* Draws the 7x7 finder patterns in the corners top left/right and bottom left
*
* ISO/IEC 18004:2000 Section 7.3.2
*/
public function setFinderPattern():QRMatrix{
$pos = [
[0, 0], // top left
[$this->moduleCount - 7, 0], // top right
[0, $this->moduleCount - 7], // bottom left
];
foreach($pos as $c){
for($y = 0; $y < 7; $y++){
for($x = 0; $x < 7; $x++){
// outer (dark) 7*7 square
if($x === 0 || $x === 6 || $y === 0 || $y === 6){
$this->set($c[0] + $y, $c[1] + $x, true, $this::M_FINDER);
}
// inner (light) 5*5 square
elseif($x === 1 || $x === 5 || $y === 1 || $y === 5){
$this->set($c[0] + $y, $c[1] + $x, false, $this::M_FINDER);
}
// 3*3 dot
else{
$this->set($c[0] + $y, $c[1] + $x, true, $this::M_FINDER_DOT_LIGHT);
}
}
}
}
return $this;
}
/**
* Draws the separator lines around the finder patterns
*
* ISO/IEC 18004:2000 Section 7.3.3
*/
public function setSeparators():QRMatrix{
$h = [
[7, 0],
[$this->moduleCount - 8, 0],
[7, $this->moduleCount - 8],
];
$v = [
[7, 7],
[$this->moduleCount - 1, 7],
[7, $this->moduleCount - 8],
];
for($c = 0; $c < 3; $c++){
for($i = 0; $i < 8; $i++){
$this->set($h[$c][0] , $h[$c][1] + $i, false, $this::M_SEPARATOR);
$this->set($v[$c][0] - $i, $v[$c][1] , false, $this::M_SEPARATOR);
}
}
return $this;
}
/**
* Draws the 5x5 alignment patterns
*
* ISO/IEC 18004:2000 Section 7.3.5
*/
public function setAlignmentPattern():QRMatrix{
foreach($this::alignmentPattern[$this->version] as $y){
foreach($this::alignmentPattern[$this->version] as $x){
// skip existing patterns
if($this->matrix[$y][$x] !== $this::M_NULL){
continue;
}
for($ry = -2; $ry <= 2; $ry++){
for($rx = -2; $rx <= 2; $rx++){
$v = ($ry === 0 && $rx === 0) || $ry === 2 || $ry === -2 || $rx === 2 || $rx === -2;
$this->set($x + $rx, $y + $ry, $v, $this::M_ALIGNMENT);
}
}
}
}
return $this;
}
/**
* Draws the timing pattern (h/v checkered line between the finder patterns)
*
* ISO/IEC 18004:2000 Section 7.3.4
*/
public function setTimingPattern():QRMatrix{
foreach(range(8, $this->moduleCount - 8 - 1) as $i){
if($this->matrix[6][$i] !== $this::M_NULL || $this->matrix[$i][6] !== $this::M_NULL){
continue;
}
$v = $i % 2 === 0;
$this->set($i, 6, $v, $this::M_TIMING); // h
$this->set(6, $i, $v, $this::M_TIMING); // v
}
return $this;
}
/**
* Draws the version information, 2x 3x6 pixel
*
* ISO/IEC 18004:2000 Section 8.10
*/
public function setVersionNumber(?bool $test = null):QRMatrix{
$bits = $this::versionPattern[$this->version] ?? false;
if($bits !== false){
for($i = 0; $i < 18; $i++){
$a = (int)floor($i / 3);
$b = $i % 3 + $this->moduleCount - 8 - 3;
$v = !$test && (($bits >> $i) & 1) === 1;
$this->set($b, $a, $v, $this::M_VERSION); // ne
$this->set($a, $b, $v, $this::M_VERSION); // sw
}
}
return $this;
}
/**
* Draws the format info along the finder patterns
*
* ISO/IEC 18004:2000 Section 8.9
*/
public function setFormatInfo(int $maskPattern, ?bool $test = null):QRMatrix{
$bits = $this::formatPattern[QRCode::ECC_MODES[$this->eclevel]][$maskPattern] ?? 0;
for($i = 0; $i < 15; $i++){
$v = !$test && (($bits >> $i) & 1) === 1;
if($i < 6){
$this->set(8, $i, $v, $this::M_FORMAT);
}
elseif($i < 8){
$this->set(8, $i + 1, $v, $this::M_FORMAT);
}
else{
$this->set(8, $this->moduleCount - 15 + $i, $v, $this::M_FORMAT);
}
if($i < 8){
$this->set($this->moduleCount - $i - 1, 8, $v, $this::M_FORMAT);
}
elseif($i < 9){
$this->set(15 - $i, 8, $v, $this::M_FORMAT);
}
else{
$this->set(15 - $i - 1, 8, $v, $this::M_FORMAT);
}
}
$this->set(8, $this->moduleCount - 8, !$test, $this::M_FORMAT);
return $this;
}
/**
* Draws the "quiet zone" of $size around the matrix
*
* ISO/IEC 18004:2000 Section 7.3.7
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public function setQuietZone(?int $size = null):QRMatrix{
if($this->matrix[$this->moduleCount - 1][$this->moduleCount - 1] === $this::M_NULL){
throw new QRCodeDataException('use only after writing data');
}
$size = $size !== null
? max(0, min($size, floor($this->moduleCount / 2)))
: 4;
for($y = 0; $y < $this->moduleCount; $y++){
for($i = 0; $i < $size; $i++){
array_unshift($this->matrix[$y], $this::M_QUIETZONE);
array_push($this->matrix[$y], $this::M_QUIETZONE);
}
}
$this->moduleCount += ($size * 2);
$r = array_fill(0, $this->moduleCount, $this::M_QUIETZONE);
for($i = 0; $i < $size; $i++){
array_unshift($this->matrix, $r);
array_push($this->matrix, $r);
}
return $this;
}
/**
* Clears a space of $width * $height in order to add a logo or text.
*
* Additionally, the logo space can be positioned within the QR Code - respecting the main functional patterns -
* using $startX and $startY. If either of these are null, the logo space will be centered in that direction.
* ECC level "H" (30%) is required.
*
* Please note that adding a logo space minimizes the error correction capacity of the QR Code and
* created images may become unreadable, especially when printed with a chance to receive damage.
* Please test thoroughly before using this feature in production.
*
* This method should be called from within an output module (after the matrix has been filled with data).
* Note that there is no restiction on how many times this method could be called on the same matrix instance.
*
* @link https://github.com/chillerlan/php-qrcode/issues/52
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public function setLogoSpace(int $width, int $height, ?int $startX = null, ?int $startY = null):QRMatrix{
// for logos we operate in ECC H (30%) only
if($this->eclevel !== QRCode::ECC_H){
throw new QRCodeDataException('ECC level "H" required to add logo space');
}
// we need uneven sizes to center the logo space, adjust if needed
if($startX === null && ($width % 2) === 0){
$width++;
}
if($startY === null && ($height % 2) === 0){
$height++;
}
// $this->moduleCount includes the quiet zone (if created), we need the QR size here
$length = $this->version * 4 + 17;
// throw if the logo space exceeds the maximum error correction capacity
if($width * $height > floor($length * $length * 0.2)){
throw new QRCodeDataException('logo space exceeds the maximum error correction capacity');
}
// quiet zone size
$qz = ($this->moduleCount - $length) / 2;
// skip quiet zone and the first 9 rows/columns (finder-, mode-, version- and timing patterns)
$start = $qz + 9;
// skip quiet zone
$end = $this->moduleCount - $qz;
// determine start coordinates
$startX = ($startX !== null ? $startX : ($length - $width) / 2) + $qz;
$startY = ($startY !== null ? $startY : ($length - $height) / 2) + $qz;
// clear the space
for($y = 0; $y < $this->moduleCount; $y++){
for($x = 0; $x < $this->moduleCount; $x++){
// out of bounds, skip
if($x < $start || $y < $start ||$x >= $end || $y >= $end){
continue;
}
// a match
if($x >= $startX && $x < ($startX + $width) && $y >= $startY && $y < ($startY + $height)){
$this->set($x, $y, false, $this::M_LOGO);
}
}
}
return $this;
}
/**
* Maps the binary $data array from QRDataInterface::maskECC() on the matrix,
* masking the data using $maskPattern (ISO/IEC 18004:2000 Section 8.8)
*
* @see \chillerlan\QRCode\Data\QRDataAbstract::maskECC()
*
* @param int[] $data
* @param int $maskPattern
*
* @return \chillerlan\QRCode\Data\QRMatrix
*/
public function mapData(array $data, int $maskPattern):QRMatrix{
$this->maskPattern = $maskPattern;
$byteCount = count($data);
$y = $this->moduleCount - 1;
$inc = -1;
$byteIndex = 0;
$bitIndex = 7;
$mask = $this->getMask($this->maskPattern);
for($i = $y; $i > 0; $i -= 2){
if($i === 6){
$i--;
}
while(true){
for($c = 0; $c < 2; $c++){
$x = $i - $c;
if($this->matrix[$y][$x] === $this::M_NULL){
$v = false;
if($byteIndex < $byteCount){
$v = (($data[$byteIndex] >> $bitIndex) & 1) === 1;
}
if($mask($x, $y) === 0){
$v = !$v;
}
$this->matrix[$y][$x] = $this::M_DATA << ($v ? 8 : 0);
$bitIndex--;
if($bitIndex === -1){
$byteIndex++;
$bitIndex = 7;
}
}
}
$y += $inc;
if($y < 0 || $this->moduleCount <= $y){
$y -= $inc;
$inc = -$inc;
break;
}
}
}
return $this;
}
/**
* ISO/IEC 18004:2000 Section 8.8.1
*
* Note that some versions of the QR code standard have had errors in the section about mask patterns.
* The information below has been corrected. (https://www.thonky.com/qr-code-tutorial/mask-patterns)
*
* @see \chillerlan\QRCode\QRMatrix::mapData()
*
* @internal
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
protected function getMask(int $maskPattern):Closure{
if((0b111 & $maskPattern) !== $maskPattern){
throw new QRCodeDataException('invalid mask pattern'); // @codeCoverageIgnore
}
return [
0b000 => fn($x, $y):int => ($x + $y) % 2,
0b001 => fn($x, $y):int => $y % 2,
0b010 => fn($x, $y):int => $x % 3,
0b011 => fn($x, $y):int => ($x + $y) % 3,
0b100 => fn($x, $y):int => ((int)($y / 2) + (int)($x / 3)) % 2,
0b101 => fn($x, $y):int => (($x * $y) % 2) + (($x * $y) % 3),
0b110 => fn($x, $y):int => ((($x * $y) % 2) + (($x * $y) % 3)) % 2,
0b111 => fn($x, $y):int => ((($x * $y) % 3) + (($x + $y) % 2)) % 2,
][$maskPattern];
}
}

View File

@ -0,0 +1,89 @@
<?php
/**
* Class BitBuffer
*
* @filesource BitBuffer.php
* @created 25.11.2015
* @package chillerlan\QRCode\Helpers
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Helpers;
use function count, floor;
/**
* Holds the raw binary data
*/
final class BitBuffer{
/**
* The buffer content
*
* @var int[]
*/
protected array $buffer = [];
/**
* Length of the content (bits)
*/
protected int $length = 0;
/**
* clears the buffer
*/
public function clear():BitBuffer{
$this->buffer = [];
$this->length = 0;
return $this;
}
/**
* appends a sequence of bits
*/
public function put(int $num, int $length):BitBuffer{
for($i = 0; $i < $length; $i++){
$this->putBit((($num >> ($length - $i - 1)) & 1) === 1);
}
return $this;
}
/**
* appends a single bit
*/
public function putBit(bool $bit):BitBuffer{
$bufIndex = floor($this->length / 8);
if(count($this->buffer) <= $bufIndex){
$this->buffer[] = 0;
}
if($bit === true){
$this->buffer[(int)$bufIndex] |= (0x80 >> ($this->length % 8));
}
$this->length++;
return $this;
}
/**
* returns the current buffer length
*/
public function getLength():int{
return $this->length;
}
/**
* returns the buffer content
*/
public function getBuffer():array{
return $this->buffer;
}
}

View File

@ -0,0 +1,178 @@
<?php
/**
* Class Polynomial
*
* @filesource Polynomial.php
* @created 25.11.2015
* @package chillerlan\QRCode\Helpers
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Helpers;
use chillerlan\QRCode\QRCodeException;
use function array_fill, count, sprintf;
/**
* Polynomial long division helpers
*
* @see http://www.thonky.com/qr-code-tutorial/error-correction-coding
*/
final class Polynomial{
/**
* @see http://www.thonky.com/qr-code-tutorial/log-antilog-table
*/
protected const table = [
[ 1, 0], [ 2, 0], [ 4, 1], [ 8, 25], [ 16, 2], [ 32, 50], [ 64, 26], [128, 198],
[ 29, 3], [ 58, 223], [116, 51], [232, 238], [205, 27], [135, 104], [ 19, 199], [ 38, 75],
[ 76, 4], [152, 100], [ 45, 224], [ 90, 14], [180, 52], [117, 141], [234, 239], [201, 129],
[143, 28], [ 3, 193], [ 6, 105], [ 12, 248], [ 24, 200], [ 48, 8], [ 96, 76], [192, 113],
[157, 5], [ 39, 138], [ 78, 101], [156, 47], [ 37, 225], [ 74, 36], [148, 15], [ 53, 33],
[106, 53], [212, 147], [181, 142], [119, 218], [238, 240], [193, 18], [159, 130], [ 35, 69],
[ 70, 29], [140, 181], [ 5, 194], [ 10, 125], [ 20, 106], [ 40, 39], [ 80, 249], [160, 185],
[ 93, 201], [186, 154], [105, 9], [210, 120], [185, 77], [111, 228], [222, 114], [161, 166],
[ 95, 6], [190, 191], [ 97, 139], [194, 98], [153, 102], [ 47, 221], [ 94, 48], [188, 253],
[101, 226], [202, 152], [137, 37], [ 15, 179], [ 30, 16], [ 60, 145], [120, 34], [240, 136],
[253, 54], [231, 208], [211, 148], [187, 206], [107, 143], [214, 150], [177, 219], [127, 189],
[254, 241], [225, 210], [223, 19], [163, 92], [ 91, 131], [182, 56], [113, 70], [226, 64],
[217, 30], [175, 66], [ 67, 182], [134, 163], [ 17, 195], [ 34, 72], [ 68, 126], [136, 110],
[ 13, 107], [ 26, 58], [ 52, 40], [104, 84], [208, 250], [189, 133], [103, 186], [206, 61],
[129, 202], [ 31, 94], [ 62, 155], [124, 159], [248, 10], [237, 21], [199, 121], [147, 43],
[ 59, 78], [118, 212], [236, 229], [197, 172], [151, 115], [ 51, 243], [102, 167], [204, 87],
[133, 7], [ 23, 112], [ 46, 192], [ 92, 247], [184, 140], [109, 128], [218, 99], [169, 13],
[ 79, 103], [158, 74], [ 33, 222], [ 66, 237], [132, 49], [ 21, 197], [ 42, 254], [ 84, 24],
[168, 227], [ 77, 165], [154, 153], [ 41, 119], [ 82, 38], [164, 184], [ 85, 180], [170, 124],
[ 73, 17], [146, 68], [ 57, 146], [114, 217], [228, 35], [213, 32], [183, 137], [115, 46],
[230, 55], [209, 63], [191, 209], [ 99, 91], [198, 149], [145, 188], [ 63, 207], [126, 205],
[252, 144], [229, 135], [215, 151], [179, 178], [123, 220], [246, 252], [241, 190], [255, 97],
[227, 242], [219, 86], [171, 211], [ 75, 171], [150, 20], [ 49, 42], [ 98, 93], [196, 158],
[149, 132], [ 55, 60], [110, 57], [220, 83], [165, 71], [ 87, 109], [174, 65], [ 65, 162],
[130, 31], [ 25, 45], [ 50, 67], [100, 216], [200, 183], [141, 123], [ 7, 164], [ 14, 118],
[ 28, 196], [ 56, 23], [112, 73], [224, 236], [221, 127], [167, 12], [ 83, 111], [166, 246],
[ 81, 108], [162, 161], [ 89, 59], [178, 82], [121, 41], [242, 157], [249, 85], [239, 170],
[195, 251], [155, 96], [ 43, 134], [ 86, 177], [172, 187], [ 69, 204], [138, 62], [ 9, 90],
[ 18, 203], [ 36, 89], [ 72, 95], [144, 176], [ 61, 156], [122, 169], [244, 160], [245, 81],
[247, 11], [243, 245], [251, 22], [235, 235], [203, 122], [139, 117], [ 11, 44], [ 22, 215],
[ 44, 79], [ 88, 174], [176, 213], [125, 233], [250, 230], [233, 231], [207, 173], [131, 232],
[ 27, 116], [ 54, 214], [108, 244], [216, 234], [173, 168], [ 71, 80], [142, 88], [ 1, 175],
];
/**
* @var int[]
*/
protected array $num = [];
/**
* Polynomial constructor.
*/
public function __construct(?array $num = null, ?int $shift = null){
$this->setNum($num ?? [1], $shift);
}
/**
*
*/
public function getNum():array{
return $this->num;
}
/**
* @param int[] $num
* @param int|null $shift
*
* @return \chillerlan\QRCode\Helpers\Polynomial
*/
public function setNum(array $num, ?int $shift = null):Polynomial{
$offset = 0;
$numCount = count($num);
while($offset < $numCount && $num[$offset] === 0){
$offset++;
}
$this->num = array_fill(0, $numCount - $offset + ($shift ?? 0), 0);
for($i = 0; $i < $numCount - $offset; $i++){
$this->num[$i] = $num[$i + $offset];
}
return $this;
}
/**
* @param int[] $e
*
* @return \chillerlan\QRCode\Helpers\Polynomial
*/
public function multiply(array $e):Polynomial{
$n = array_fill(0, count($this->num) + count($e) - 1, 0);
foreach($this->num as $i => $vi){
$vi = $this->glog($vi);
foreach($e as $j => $vj){
$n[$i + $j] ^= $this->gexp($vi + $this->glog($vj));
}
}
$this->setNum($n);
return $this;
}
/**
* @param int[] $e
*
* @return \chillerlan\QRCode\Helpers\Polynomial
*/
public function mod(array $e):Polynomial{
$n = $this->num;
if(count($n) - count($e) < 0){
return $this;
}
$ratio = $this->glog($n[0]) - $this->glog($e[0]);
foreach($e as $i => $v){
$n[$i] ^= $this->gexp($this->glog($v) + $ratio);
}
$this->setNum($n)->mod($e);
return $this;
}
/**
* @throws \chillerlan\QRCode\QRCodeException
*/
public function glog(int $n):int{
if($n < 1){
throw new QRCodeException(sprintf('log(%s)', $n));
}
return self::table[$n][1];
}
/**
*
*/
public function gexp(int $n):int{
if($n < 0){
$n += 255;
}
elseif($n >= 256){
$n -= 255;
}
return self::table[$n][0];
}
}

View File

@ -0,0 +1,17 @@
<?php
/**
* Class QRCodeOutputException
*
* @filesource QRCodeOutputException.php
* @created 09.12.2015
* @package chillerlan\QRCode\Output
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Output;
use chillerlan\QRCode\QRCodeException;
class QRCodeOutputException extends QRCodeException{}

View File

@ -0,0 +1,113 @@
<?php
/**
* Class QRFpdf
*
* https://github.com/chillerlan/php-qrcode/pull/49
*
* @filesource QRFpdf.php
* @created 03.06.2020
* @package chillerlan\QRCode\Output
* @author Maximilian Kresse
*
* @license MIT
*/
namespace chillerlan\QRCode\Output;
use chillerlan\QRCode\Data\QRMatrix;
use chillerlan\QRCode\QRCodeException;
use chillerlan\Settings\SettingsContainerInterface;
use FPDF;
use function array_values, class_exists, count, is_array;
/**
* QRFpdf output module (requires fpdf)
*
* @see https://github.com/Setasign/FPDF
* @see http://www.fpdf.org/
*/
class QRFpdf extends QROutputAbstract{
public function __construct(SettingsContainerInterface $options, QRMatrix $matrix){
if(!class_exists(FPDF::class)){
// @codeCoverageIgnoreStart
throw new QRCodeException(
'The QRFpdf output requires FPDF as dependency but the class "\FPDF" couldn\'t be found.'
);
// @codeCoverageIgnoreEnd
}
parent::__construct($options, $matrix);
}
/**
* @inheritDoc
*/
protected function setModuleValues():void{
foreach($this::DEFAULT_MODULE_VALUES as $M_TYPE => $defaultValue){
$v = $this->options->moduleValues[$M_TYPE] ?? null;
if(!is_array($v) || count($v) < 3){
$this->moduleValues[$M_TYPE] = $defaultValue
? [0, 0, 0]
: [255, 255, 255];
}
else{
$this->moduleValues[$M_TYPE] = array_values($v);
}
}
}
/**
* @inheritDoc
*
* @return string|\FPDF
*/
public function dump(?string $file = null){
$file ??= $this->options->cachefile;
$fpdf = new FPDF('P', $this->options->fpdfMeasureUnit, [$this->length, $this->length]);
$fpdf->AddPage();
$prevColor = null;
foreach($this->matrix->matrix() as $y => $row){
foreach($row as $x => $M_TYPE){
/** @var int $M_TYPE */
$color = $this->moduleValues[$M_TYPE];
if($prevColor === null || $prevColor !== $color){
/** @phan-suppress-next-line PhanParamTooFewUnpack */
$fpdf->SetFillColor(...$color);
$prevColor = $color;
}
$fpdf->Rect($x * $this->scale, $y * $this->scale, 1 * $this->scale, 1 * $this->scale, 'F');
}
}
if($this->options->returnResource){
return $fpdf;
}
$pdfData = $fpdf->Output('S');
if($file !== null){
$this->saveToFile($pdfData, $file);
}
if($this->options->imageBase64){
$pdfData = sprintf('data:application/pdf;base64,%s', base64_encode($pdfData));
}
return $pdfData;
}
}

View File

@ -0,0 +1,217 @@
<?php
/**
* Class QRImage
*
* @filesource QRImage.php
* @created 05.12.2015
* @package chillerlan\QRCode\Output
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*
* @noinspection PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode\Output;
use chillerlan\QRCode\Data\QRMatrix;
use chillerlan\QRCode\{QRCode, QRCodeException};
use chillerlan\Settings\SettingsContainerInterface;
use Exception;
use function array_values, base64_encode, call_user_func, count, extension_loaded, imagecolorallocate, imagecolortransparent,
imagecreatetruecolor, imagedestroy, imagefilledrectangle, imagegif, imagejpeg, imagepng, in_array,
is_array, ob_end_clean, ob_get_contents, ob_start, range, sprintf;
/**
* Converts the matrix into GD images, raw or base64 output (requires ext-gd)
*
* @see http://php.net/manual/book.image.php
*/
class QRImage extends QROutputAbstract{
/**
* GD image types that support transparency
*
* @var string[]
*/
protected const TRANSPARENCY_TYPES = [
QRCode::OUTPUT_IMAGE_PNG,
QRCode::OUTPUT_IMAGE_GIF,
];
protected string $defaultMode = QRCode::OUTPUT_IMAGE_PNG;
/**
* The GD image resource
*
* @see imagecreatetruecolor()
* @var resource|\GdImage
*
* @phan-suppress PhanUndeclaredTypeProperty
*/
protected $image;
/**
* @inheritDoc
*
* @throws \chillerlan\QRCode\QRCodeException
*/
public function __construct(SettingsContainerInterface $options, QRMatrix $matrix){
if(!extension_loaded('gd')){
throw new QRCodeException('ext-gd not loaded'); // @codeCoverageIgnore
}
parent::__construct($options, $matrix);
}
/**
* @inheritDoc
*/
protected function setModuleValues():void{
foreach($this::DEFAULT_MODULE_VALUES as $M_TYPE => $defaultValue){
$v = $this->options->moduleValues[$M_TYPE] ?? null;
if(!is_array($v) || count($v) < 3){
$this->moduleValues[$M_TYPE] = $defaultValue
? [0, 0, 0]
: [255, 255, 255];
}
else{
$this->moduleValues[$M_TYPE] = array_values($v);
}
}
}
/**
* @inheritDoc
*
* @return string|resource|\GdImage
*
* @phan-suppress PhanUndeclaredTypeReturnType, PhanTypeMismatchReturn
*/
public function dump(?string $file = null){
$file ??= $this->options->cachefile;
$this->image = imagecreatetruecolor($this->length, $this->length);
// avoid: Indirect modification of overloaded property $imageTransparencyBG has no effect
// https://stackoverflow.com/a/10455217
$tbg = $this->options->imageTransparencyBG;
/** @phan-suppress-next-line PhanParamTooFewInternalUnpack */
$background = imagecolorallocate($this->image, ...$tbg);
if((bool)$this->options->imageTransparent && in_array($this->options->outputType, $this::TRANSPARENCY_TYPES, true)){
imagecolortransparent($this->image, $background);
}
imagefilledrectangle($this->image, 0, 0, $this->length, $this->length, $background);
foreach($this->matrix->matrix() as $y => $row){
foreach($row as $x => $M_TYPE){
$this->setPixel($x, $y, $this->moduleValues[$M_TYPE]);
}
}
if($this->options->returnResource){
return $this->image;
}
$imageData = $this->dumpImage();
if($file !== null){
$this->saveToFile($imageData, $file);
}
if($this->options->imageBase64){
$imageData = sprintf('data:image/%s;base64,%s', $this->options->outputType, base64_encode($imageData));
}
return $imageData;
}
/**
* Creates a single QR pixel with the given settings
*/
protected function setPixel(int $x, int $y, array $rgb):void{
imagefilledrectangle(
$this->image,
$x * $this->scale,
$y * $this->scale,
($x + 1) * $this->scale,
($y + 1) * $this->scale,
/** @phan-suppress-next-line PhanParamTooFewInternalUnpack */
imagecolorallocate($this->image, ...$rgb)
);
}
/**
* Creates the final image by calling the desired GD output function
*
* @throws \chillerlan\QRCode\Output\QRCodeOutputException
*/
protected function dumpImage():string{
ob_start();
try{
call_user_func([$this, $this->outputMode ?? $this->defaultMode]);
}
// not going to cover edge cases
// @codeCoverageIgnoreStart
catch(Exception $e){
throw new QRCodeOutputException($e->getMessage());
}
// @codeCoverageIgnoreEnd
$imageData = ob_get_contents();
imagedestroy($this->image);
ob_end_clean();
return $imageData;
}
/**
* PNG output
*
* @return void
*/
protected function png():void{
imagepng(
$this->image,
null,
in_array($this->options->pngCompression, range(-1, 9), true)
? $this->options->pngCompression
: -1
);
}
/**
* Jiff - like... JitHub!
*
* @return void
*/
protected function gif():void{
imagegif($this->image);
}
/**
* JPG output
*
* @return void
*/
protected function jpg():void{
imagejpeg(
$this->image,
null,
in_array($this->options->jpegQuality, range(0, 100), true)
? $this->options->jpegQuality
: 85
);
}
}

View File

@ -0,0 +1,119 @@
<?php
/**
* Class QRImagick
*
* @filesource QRImagick.php
* @created 04.07.2018
* @package chillerlan\QRCode\Output
* @author smiley <smiley@chillerlan.net>
* @copyright 2018 smiley
* @license MIT
*
* @noinspection PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode\Output;
use chillerlan\QRCode\Data\QRMatrix;
use chillerlan\QRCode\QRCodeException;
use chillerlan\Settings\SettingsContainerInterface;
use Imagick, ImagickDraw, ImagickPixel;
use function extension_loaded, is_string;
/**
* ImageMagick output module (requires ext-imagick)
*
* @see http://php.net/manual/book.imagick.php
* @see http://phpimagick.com
*/
class QRImagick extends QROutputAbstract{
protected Imagick $imagick;
/**
* @inheritDoc
*/
public function __construct(SettingsContainerInterface $options, QRMatrix $matrix){
if(!extension_loaded('imagick')){
throw new QRCodeException('ext-imagick not loaded'); // @codeCoverageIgnore
}
parent::__construct($options, $matrix);
}
/**
* @inheritDoc
*/
protected function setModuleValues():void{
foreach($this::DEFAULT_MODULE_VALUES as $type => $defaultValue){
$v = $this->options->moduleValues[$type] ?? null;
if(!is_string($v)){
$this->moduleValues[$type] = $defaultValue
? new ImagickPixel($this->options->markupDark)
: new ImagickPixel($this->options->markupLight);
}
else{
$this->moduleValues[$type] = new ImagickPixel($v);
}
}
}
/**
* @inheritDoc
*
* @return string|\Imagick
*/
public function dump(?string $file = null){
$file ??= $this->options->cachefile;
$this->imagick = new Imagick;
$this->imagick->newImage(
$this->length,
$this->length,
new ImagickPixel($this->options->imagickBG ?? 'transparent'),
$this->options->imagickFormat
);
$this->drawImage();
if($this->options->returnResource){
return $this->imagick;
}
$imageData = $this->imagick->getImageBlob();
if($file !== null){
$this->saveToFile($imageData, $file);
}
return $imageData;
}
/**
* Creates the QR image via ImagickDraw
*/
protected function drawImage():void{
$draw = new ImagickDraw;
foreach($this->matrix->matrix() as $y => $row){
foreach($row as $x => $M_TYPE){
$draw->setStrokeColor($this->moduleValues[$M_TYPE]);
$draw->setFillColor($this->moduleValues[$M_TYPE]);
$draw->rectangle(
$x * $this->scale,
$y * $this->scale,
($x + 1) * $this->scale,
($y + 1) * $this->scale
);
}
}
$this->imagick->drawImage($draw);
}
}

View File

@ -0,0 +1,162 @@
<?php
/**
* Class QRMarkup
*
* @filesource QRMarkup.php
* @created 17.12.2016
* @package chillerlan\QRCode\Output
* @author Smiley <smiley@chillerlan.net>
* @copyright 2016 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Output;
use chillerlan\QRCode\QRCode;
use function is_string, sprintf, strip_tags, trim;
/**
* Converts the matrix into markup types: HTML, SVG, ...
*/
class QRMarkup extends QROutputAbstract{
protected string $defaultMode = QRCode::OUTPUT_MARKUP_SVG;
/**
* @see \sprintf()
*/
protected string $svgHeader = '<svg xmlns="http://www.w3.org/2000/svg" class="qr-svg %1$s" '.
'style="width: 100%%; height: auto;" viewBox="0 0 %2$d %2$d">';
/**
* @inheritDoc
*/
protected function setModuleValues():void{
foreach($this::DEFAULT_MODULE_VALUES as $M_TYPE => $defaultValue){
$v = $this->options->moduleValues[$M_TYPE] ?? null;
if(!is_string($v)){
$this->moduleValues[$M_TYPE] = $defaultValue
? $this->options->markupDark
: $this->options->markupLight;
}
else{
$this->moduleValues[$M_TYPE] = trim(strip_tags($v), '\'"');
}
}
}
/**
* HTML output
*/
protected function html(?string $file = null):string{
$html = empty($this->options->cssClass)
? '<div>'
: '<div class="'.$this->options->cssClass.'">';
$html .= $this->options->eol;
foreach($this->matrix->matrix() as $row){
$html .= '<div>';
foreach($row as $M_TYPE){
$html .= '<span style="background: '.$this->moduleValues[$M_TYPE].';"></span>';
}
$html .= '</div>'.$this->options->eol;
}
$html .= '</div>'.$this->options->eol;
if($file !== null){
/** @noinspection HtmlRequiredLangAttribute */
return sprintf(
'<!DOCTYPE html><html><head><meta charset="UTF-8"><title>QR Code</title></head><body>%s</body></html>',
$this->options->eol.$html
);
}
return $html;
}
/**
* SVG output
*
* @see https://github.com/codemasher/php-qrcode/pull/5
*/
protected function svg(?string $file = null):string{
$matrix = $this->matrix->matrix();
$svg = sprintf($this->svgHeader, $this->options->cssClass, $this->options->svgViewBoxSize ?? $this->moduleCount)
.$this->options->eol
.'<defs>'.$this->options->svgDefs.'</defs>'
.$this->options->eol;
foreach($this->moduleValues as $M_TYPE => $value){
$path = '';
foreach($matrix as $y => $row){
//we'll combine active blocks within a single row as a lightweight compression technique
$start = null;
$count = 0;
foreach($row as $x => $module){
if($module === $M_TYPE){
$count++;
if($start === null){
$start = $x;
}
if(isset($row[$x + 1])){
continue;
}
}
if($count > 0){
$len = $count;
$start ??= 0; // avoid type coercion in sprintf() - phan happy
$path .= sprintf('M%s %s h%s v1 h-%sZ ', $start, $y, $len, $len);
// reset count
$count = 0;
$start = null;
}
}
}
if(!empty($path)){
$svg .= sprintf(
'<path class="qr-%s %s" stroke="transparent" fill="%s" fill-opacity="%s" d="%s" />',
$M_TYPE, $this->options->cssClass, $value, $this->options->svgOpacity, $path
);
}
}
// close svg
$svg .= '</svg>'.$this->options->eol;
// if saving to file, append the correct headers
if($file !== null){
return '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">'.
$this->options->eol.$svg;
}
if($this->options->imageBase64){
$svg = sprintf('data:image/svg+xml;base64,%s', base64_encode($svg));
}
return $svg;
}
}

View File

@ -0,0 +1,130 @@
<?php
/**
* Class QROutputAbstract
*
* @filesource QROutputAbstract.php
* @created 09.12.2015
* @package chillerlan\QRCode\Output
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Output;
use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\Data\QRMatrix;
use chillerlan\Settings\SettingsContainerInterface;
use function call_user_func_array, dirname, file_put_contents, get_called_class, in_array, is_writable, sprintf;
/**
* common output abstract
*/
abstract class QROutputAbstract implements QROutputInterface{
/**
* the current size of the QR matrix
*
* @see \chillerlan\QRCode\Data\QRMatrix::size()
*/
protected int $moduleCount;
/**
* the current output mode
*
* @see \chillerlan\QRCode\QROptions::$outputType
*/
protected string $outputMode;
/**
* the default output mode of the current output module
*/
protected string $defaultMode;
/**
* the current scaling for a QR pixel
*
* @see \chillerlan\QRCode\QROptions::$scale
*/
protected int $scale;
/**
* the side length of the QR image (modules * scale)
*/
protected int $length;
/**
* an (optional) array of color values for the several QR matrix parts
*/
protected array $moduleValues;
/**
* the (filled) data matrix object
*/
protected QRMatrix $matrix;
/**
* @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\QRCode\QROptions
*/
protected SettingsContainerInterface $options;
/**
* QROutputAbstract constructor.
*/
public function __construct(SettingsContainerInterface $options, QRMatrix $matrix){
$this->options = $options;
$this->matrix = $matrix;
$this->moduleCount = $this->matrix->size();
$this->scale = $this->options->scale;
$this->length = $this->moduleCount * $this->scale;
$class = get_called_class();
if(isset(QRCode::OUTPUT_MODES[$class]) && in_array($this->options->outputType, QRCode::OUTPUT_MODES[$class])){
$this->outputMode = $this->options->outputType;
}
$this->setModuleValues();
}
/**
* Sets the initial module values (clean-up & defaults)
*/
abstract protected function setModuleValues():void;
/**
* saves the qr data to a file
*
* @see file_put_contents()
* @see \chillerlan\QRCode\QROptions::cachefile
*
* @throws \chillerlan\QRCode\Output\QRCodeOutputException
*/
protected function saveToFile(string $data, string $file):bool{
if(!is_writable(dirname($file))){
throw new QRCodeOutputException(sprintf('Could not write data to cache file: %s', $file));
}
return (bool)file_put_contents($file, $data);
}
/**
* @inheritDoc
*/
public function dump(?string $file = null){
$file ??= $this->options->cachefile;
// call the built-in output method with the optional file path as parameter
// to make the called method aware if a cache file was given
$data = call_user_func_array([$this, $this->outputMode ?? $this->defaultMode], [$file]);
if($file !== null){
$this->saveToFile($data, $file);
}
return $data;
}
}

View File

@ -0,0 +1,57 @@
<?php
/**
* Interface QROutputInterface,
*
* @filesource QROutputInterface.php
* @created 02.12.2015
* @package chillerlan\QRCode\Output
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Output;
use chillerlan\QRCode\Data\QRMatrix;
/**
* Converts the data matrix into readable output
*/
interface QROutputInterface{
public const DEFAULT_MODULE_VALUES = [
// light
QRMatrix::M_NULL => false,
QRMatrix::M_DARKMODULE_LIGHT => false,
QRMatrix::M_DATA => false,
QRMatrix::M_FINDER => false,
QRMatrix::M_SEPARATOR => false,
QRMatrix::M_ALIGNMENT => false,
QRMatrix::M_TIMING => false,
QRMatrix::M_FORMAT => false,
QRMatrix::M_VERSION => false,
QRMatrix::M_QUIETZONE => false,
QRMatrix::M_LOGO => false,
QRMatrix::M_FINDER_DOT_LIGHT => false,
// dark
QRMatrix::M_DARKMODULE => true,
QRMatrix::M_DATA_DARK => true,
QRMatrix::M_FINDER_DARK => true,
QRMatrix::M_SEPARATOR_DARK => true,
QRMatrix::M_ALIGNMENT_DARK => true,
QRMatrix::M_TIMING_DARK => true,
QRMatrix::M_FORMAT_DARK => true,
QRMatrix::M_VERSION_DARK => true,
QRMatrix::M_QUIETZONE_DARK => true,
QRMatrix::M_LOGO_DARK => true,
QRMatrix::M_FINDER_DOT => true,
];
/**
* generates the output, optionally dumps it to a file, and returns it
*
* @return mixed
*/
public function dump(?string $file = null);
}

View File

@ -0,0 +1,76 @@
<?php
/**
* Class QRString
*
* @filesource QRString.php
* @created 05.12.2015
* @package chillerlan\QRCode\Output
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*
* @noinspection PhpUnusedParameterInspection
* @noinspection PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode\Output;
use chillerlan\QRCode\QRCode;
use function implode, is_string, json_encode;
/**
* Converts the matrix data into string types
*/
class QRString extends QROutputAbstract{
protected string $defaultMode = QRCode::OUTPUT_STRING_TEXT;
/**
* @inheritDoc
*/
protected function setModuleValues():void{
foreach($this::DEFAULT_MODULE_VALUES as $M_TYPE => $defaultValue){
$v = $this->options->moduleValues[$M_TYPE] ?? null;
if(!is_string($v)){
$this->moduleValues[$M_TYPE] = $defaultValue
? $this->options->textDark
: $this->options->textLight;
}
else{
$this->moduleValues[$M_TYPE] = $v;
}
}
}
/**
* string output
*/
protected function text(?string $file = null):string{
$str = [];
foreach($this->matrix->matrix() as $row){
$r = [];
foreach($row as $M_TYPE){
$r[] = $this->moduleValues[$M_TYPE];
}
$str[] = implode('', $r);
}
return implode($this->options->eol, $str);
}
/**
* JSON output
*/
protected function json(?string $file = null):string{
return json_encode($this->matrix->matrix());
}
}

313
vendor/chillerlan/php-qrcode/src/QRCode.php vendored Executable file
View File

@ -0,0 +1,313 @@
<?php
/**
* Class QRCode
*
* @filesource QRCode.php
* @created 26.11.2015
* @package chillerlan\QRCode
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode;
use chillerlan\QRCode\Data\{
AlphaNum, Byte, Kanji, MaskPatternTester, Number, QRCodeDataException, QRDataInterface, QRMatrix
};
use chillerlan\QRCode\Output\{
QRCodeOutputException, QRFpdf, QRImage, QRImagick, QRMarkup, QROutputInterface, QRString
};
use chillerlan\Settings\SettingsContainerInterface;
use function call_user_func_array, class_exists, in_array, ord, strlen, strtolower, str_split;
/**
* Turns a text string into a Model 2 QR Code
*
* @see https://github.com/kazuhikoarase/qrcode-generator/tree/master/php
* @see http://www.qrcode.com/en/codes/model12.html
* @see https://www.swisseduc.ch/informatik/theoretische_informatik/qr_codes/docs/qr_standard.pdf
* @see https://en.wikipedia.org/wiki/QR_code
* @see http://www.thonky.com/qr-code-tutorial/
*/
class QRCode{
/** @var int */
public const VERSION_AUTO = -1;
/** @var int */
public const MASK_PATTERN_AUTO = -1;
// ISO/IEC 18004:2000 Table 2
/** @var int */
public const DATA_NUMBER = 0b0001;
/** @var int */
public const DATA_ALPHANUM = 0b0010;
/** @var int */
public const DATA_BYTE = 0b0100;
/** @var int */
public const DATA_KANJI = 0b1000;
/**
* References to the keys of the following tables:
*
* @see \chillerlan\QRCode\Data\QRDataInterface::MAX_LENGTH
*
* @var int[]
*/
public const DATA_MODES = [
self::DATA_NUMBER => 0,
self::DATA_ALPHANUM => 1,
self::DATA_BYTE => 2,
self::DATA_KANJI => 3,
];
// ISO/IEC 18004:2000 Tables 12, 25
/** @var int */
public const ECC_L = 0b01; // 7%.
/** @var int */
public const ECC_M = 0b00; // 15%.
/** @var int */
public const ECC_Q = 0b11; // 25%.
/** @var int */
public const ECC_H = 0b10; // 30%.
/**
* References to the keys of the following tables:
*
* @see \chillerlan\QRCode\Data\QRDataInterface::MAX_BITS
* @see \chillerlan\QRCode\Data\QRDataInterface::RSBLOCKS
* @see \chillerlan\QRCode\Data\QRMatrix::formatPattern
*
* @var int[]
*/
public const ECC_MODES = [
self::ECC_L => 0,
self::ECC_M => 1,
self::ECC_Q => 2,
self::ECC_H => 3,
];
/** @var string */
public const OUTPUT_MARKUP_HTML = 'html';
/** @var string */
public const OUTPUT_MARKUP_SVG = 'svg';
/** @var string */
public const OUTPUT_IMAGE_PNG = 'png';
/** @var string */
public const OUTPUT_IMAGE_JPG = 'jpg';
/** @var string */
public const OUTPUT_IMAGE_GIF = 'gif';
/** @var string */
public const OUTPUT_STRING_JSON = 'json';
/** @var string */
public const OUTPUT_STRING_TEXT = 'text';
/** @var string */
public const OUTPUT_IMAGICK = 'imagick';
/** @var string */
public const OUTPUT_FPDF = 'fpdf';
/** @var string */
public const OUTPUT_CUSTOM = 'custom';
/**
* Map of built-in output modules => capabilities
*
* @var string[][]
*/
public const OUTPUT_MODES = [
QRMarkup::class => [
self::OUTPUT_MARKUP_SVG,
self::OUTPUT_MARKUP_HTML,
],
QRImage::class => [
self::OUTPUT_IMAGE_PNG,
self::OUTPUT_IMAGE_GIF,
self::OUTPUT_IMAGE_JPG,
],
QRString::class => [
self::OUTPUT_STRING_JSON,
self::OUTPUT_STRING_TEXT,
],
QRImagick::class => [
self::OUTPUT_IMAGICK,
],
QRFpdf::class => [
self::OUTPUT_FPDF
]
];
/**
* Map of data mode => interface
*
* @var string[]
*/
protected const DATA_INTERFACES = [
'number' => Number::class,
'alphanum' => AlphaNum::class,
'kanji' => Kanji::class,
'byte' => Byte::class,
];
/**
* The settings container
*
* @var \chillerlan\QRCode\QROptions|\chillerlan\Settings\SettingsContainerInterface
*/
protected SettingsContainerInterface $options;
/**
* The selected data interface (Number, AlphaNum, Kanji, Byte)
*/
protected QRDataInterface $dataInterface;
/**
* QRCode constructor.
*
* Sets the options instance, determines the current mb-encoding and sets it to UTF-8
*/
public function __construct(?SettingsContainerInterface $options = null){
$this->options = $options ?? new QROptions;
}
/**
* Renders a QR Code for the given $data and QROptions
*
* @return mixed
*/
public function render(string $data, ?string $file = null){
return $this->initOutputInterface($data)->dump($file);
}
/**
* Returns a QRMatrix object for the given $data and current QROptions
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public function getMatrix(string $data):QRMatrix{
if(empty($data)){
throw new QRCodeDataException('QRCode::getMatrix() No data given.');
}
$this->dataInterface = $this->initDataInterface($data);
$maskPattern = $this->options->maskPattern === $this::MASK_PATTERN_AUTO
? (new MaskPatternTester($this->dataInterface))->getBestMaskPattern()
: $this->options->maskPattern;
$matrix = $this->dataInterface->initMatrix($maskPattern);
if((bool)$this->options->addQuietzone){
$matrix->setQuietZone($this->options->quietzoneSize);
}
return $matrix;
}
/**
* returns a fresh QRDataInterface for the given $data
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public function initDataInterface(string $data):QRDataInterface{
// allow forcing the data mode
// see https://github.com/chillerlan/php-qrcode/issues/39
$interface = $this::DATA_INTERFACES[strtolower($this->options->dataModeOverride)] ?? null;
if($interface !== null){
return new $interface($this->options, $data);
}
foreach($this::DATA_INTERFACES as $mode => $dataInterface){
if(call_user_func_array([$this, 'is'.$mode], [$data])){
return new $dataInterface($this->options, $data);
}
}
throw new QRCodeDataException('invalid data type'); // @codeCoverageIgnore
}
/**
* returns a fresh (built-in) QROutputInterface
*
* @throws \chillerlan\QRCode\Output\QRCodeOutputException
*/
protected function initOutputInterface(string $data):QROutputInterface{
if($this->options->outputType === $this::OUTPUT_CUSTOM && class_exists($this->options->outputInterface)){
/** @phan-suppress-next-line PhanTypeExpectedObjectOrClassName */
return new $this->options->outputInterface($this->options, $this->getMatrix($data));
}
foreach($this::OUTPUT_MODES as $outputInterface => $modes){
if(in_array($this->options->outputType, $modes, true) && class_exists($outputInterface)){
return new $outputInterface($this->options, $this->getMatrix($data));
}
}
throw new QRCodeOutputException('invalid output type');
}
/**
* checks if a string qualifies as numeric
*/
public function isNumber(string $string):bool{
return $this->checkString($string, QRDataInterface::CHAR_MAP_NUMBER);
}
/**
* checks if a string qualifies as alphanumeric
*/
public function isAlphaNum(string $string):bool{
return $this->checkString($string, QRDataInterface::CHAR_MAP_ALPHANUM);
}
/**
* checks is a given $string matches the characters of a given $charmap, returns false on the first invalid occurence.
*/
protected function checkString(string $string, array $charmap):bool{
foreach(str_split($string) as $chr){
if(!isset($charmap[$chr])){
return false;
}
}
return true;
}
/**
* checks if a string qualifies as Kanji
*/
public function isKanji(string $string):bool{
$i = 0;
$len = strlen($string);
while($i + 1 < $len){
$c = ((0xff & ord($string[$i])) << 8) | (0xff & ord($string[$i + 1]));
if(!($c >= 0x8140 && $c <= 0x9FFC) && !($c >= 0xE040 && $c <= 0xEBBF)){
return false;
}
$i += 2;
}
return $i >= $len;
}
/**
* a dummy
*/
public function isByte(string $data):bool{
return $data !== '';
}
}

View File

@ -0,0 +1,20 @@
<?php
/**
* Class QRCodeException
*
* @filesource QRCodeException.php
* @created 27.11.2015
* @package chillerlan\QRCode
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode;
use Exception;
/**
* An exception container
*/
class QRCodeException extends Exception{}

View File

@ -0,0 +1,54 @@
<?php
/**
* Class QROptions
*
* @filesource QROptions.php
* @created 08.12.2015
* @package chillerlan\QRCode
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode;
use chillerlan\Settings\SettingsContainerAbstract;
/**
* The QRCode settings container
*
* @property int $version
* @property int $versionMin
* @property int $versionMax
* @property int $eccLevel
* @property int $maskPattern
* @property bool $addQuietzone
* @property int $quietzoneSize
* @property string|null $dataModeOverride
* @property string $outputType
* @property string|null $outputInterface
* @property string|null $cachefile
* @property string $eol
* @property int $scale
* @property string $cssClass
* @property float $svgOpacity
* @property string $svgDefs
* @property int $svgViewBoxSize
* @property string $textDark
* @property string $textLight
* @property string $markupDark
* @property string $markupLight
* @property bool $returnResource
* @property bool $imageBase64
* @property bool $imageTransparent
* @property array $imageTransparencyBG
* @property int $pngCompression
* @property int $jpegQuality
* @property string $imagickFormat
* @property string|null $imagickBG
* @property string $fpdfMeasureUnit
* @property array|null $moduleValues
*/
class QROptions extends SettingsContainerAbstract{
use QROptionsTrait;
}

View File

@ -0,0 +1,341 @@
<?php
/**
* Trait QROptionsTrait
*
* @filesource QROptionsTrait.php
* @created 10.03.2018
* @package chillerlan\QRCode
* @author smiley <smiley@chillerlan.net>
* @copyright 2018 smiley
* @license MIT
*
* @noinspection PhpUnused
*/
namespace chillerlan\QRCode;
use function array_values, count, in_array, is_numeric, max, min, sprintf, strtolower;
/**
* The QRCode plug-in settings & setter functionality
*/
trait QROptionsTrait{
/**
* QR Code version number
*
* [1 ... 40] or QRCode::VERSION_AUTO
*/
protected int $version = QRCode::VERSION_AUTO;
/**
* Minimum QR version
*
* if $version = QRCode::VERSION_AUTO
*/
protected int $versionMin = 1;
/**
* Maximum QR version
*/
protected int $versionMax = 40;
/**
* Error correct level
*
* QRCode::ECC_X where X is:
*
* - L => 7%
* - M => 15%
* - Q => 25%
* - H => 30%
*/
protected int $eccLevel = QRCode::ECC_L;
/**
* Mask Pattern to use
*
* [0...7] or QRCode::MASK_PATTERN_AUTO
*/
protected int $maskPattern = QRCode::MASK_PATTERN_AUTO;
/**
* Add a "quiet zone" (margin) according to the QR code spec
*/
protected bool $addQuietzone = true;
/**
* Size of the quiet zone
*
* internally clamped to [0 ... $moduleCount / 2], defaults to 4 modules
*/
protected int $quietzoneSize = 4;
/**
* Use this to circumvent the data mode detection and force the usage of the given mode.
*
* valid modes are: Number, AlphaNum, Kanji, Byte (case insensitive)
*
* @see https://github.com/chillerlan/php-qrcode/issues/39
* @see https://github.com/chillerlan/php-qrcode/issues/97 (changed default value to '')
*/
protected string $dataModeOverride = '';
/**
* The output type
*
* - QRCode::OUTPUT_MARKUP_XXXX where XXXX = HTML, SVG
* - QRCode::OUTPUT_IMAGE_XXX where XXX = PNG, GIF, JPG
* - QRCode::OUTPUT_STRING_XXXX where XXXX = TEXT, JSON
* - QRCode::OUTPUT_CUSTOM
*/
protected string $outputType = QRCode::OUTPUT_IMAGE_PNG;
/**
* the FQCN of the custom QROutputInterface if $outputType is set to QRCode::OUTPUT_CUSTOM
*/
protected ?string $outputInterface = null;
/**
* /path/to/cache.file
*/
protected ?string $cachefile = null;
/**
* newline string [HTML, SVG, TEXT]
*/
protected string $eol = PHP_EOL;
/**
* size of a QR code pixel [SVG, IMAGE_*], HTML via CSS
*/
protected int $scale = 5;
/**
* a common css class
*/
protected string $cssClass = '';
/**
* SVG opacity
*/
protected float $svgOpacity = 1.0;
/**
* anything between <defs>
*
* @see https://developer.mozilla.org/docs/Web/SVG/Element/defs
*/
protected string $svgDefs = '<style>rect{shape-rendering:crispEdges}</style>';
/**
* SVG viewBox size. a single integer number which defines width/height of the viewBox attribute.
*
* viewBox="0 0 x x"
*
* @see https://css-tricks.com/scale-svg/#article-header-id-3
*/
protected ?int $svgViewBoxSize = null;
/**
* string substitute for dark
*/
protected string $textDark = '██';
/**
* string substitute for light
*/
protected string $textLight = '░░';
/**
* markup substitute for dark (CSS value)
*/
protected string $markupDark = '#000';
/**
* markup substitute for light (CSS value)
*/
protected string $markupLight = '#fff';
/**
* Return the image resource instead of a render if applicable.
* This option overrides other output options, such as $cachefile and $imageBase64.
*
* Supported by the following modules:
*
* - QRImage: resource (PHP < 8), GdImage
* - QRImagick: Imagick
* - QRFpdf: FPDF
*
* @see \chillerlan\QRCode\Output\QROutputInterface::dump()
*
* @var bool
*/
protected bool $returnResource = false;
/**
* toggle base64 or raw image data
*/
protected bool $imageBase64 = true;
/**
* toggle transparency, not supported by jpg
*/
protected bool $imageTransparent = true;
/**
* @see imagecolortransparent()
*
* [R, G, B]
*/
protected array $imageTransparencyBG = [255, 255, 255];
/**
* @see imagepng()
*/
protected int $pngCompression = -1;
/**
* @see imagejpeg()
*/
protected int $jpegQuality = 85;
/**
* Imagick output format
*
* @see \Imagick::setType()
*/
protected string $imagickFormat = 'png';
/**
* Imagick background color (defaults to "transparent")
*
* @see \ImagickPixel::__construct()
*/
protected ?string $imagickBG = null;
/**
* Measurement unit for FPDF output: pt, mm, cm, in (defaults to "pt")
*
* @see \FPDF::__construct()
*/
protected string $fpdfMeasureUnit = 'pt';
/**
* Module values map
*
* - HTML, IMAGICK: #ABCDEF, cssname, rgb(), rgba()...
* - IMAGE: [63, 127, 255] // R, G, B
*/
protected ?array $moduleValues = null;
/**
* clamp min/max version number
*/
protected function setMinMaxVersion(int $versionMin, int $versionMax):void{
$min = max(1, min(40, $versionMin));
$max = max(1, min(40, $versionMax));
$this->versionMin = min($min, $max);
$this->versionMax = max($min, $max);
}
/**
* sets the minimum version number
*/
protected function set_versionMin(int $version):void{
$this->setMinMaxVersion($version, $this->versionMax);
}
/**
* sets the maximum version number
*/
protected function set_versionMax(int $version):void{
$this->setMinMaxVersion($this->versionMin, $version);
}
/**
* sets the error correction level
*
* @throws \chillerlan\QRCode\QRCodeException
*/
protected function set_eccLevel(int $eccLevel):void{
if(!isset(QRCode::ECC_MODES[$eccLevel])){
throw new QRCodeException(sprintf('Invalid error correct level: %s', $eccLevel));
}
$this->eccLevel = $eccLevel;
}
/**
* sets/clamps the mask pattern
*/
protected function set_maskPattern(int $maskPattern):void{
if($maskPattern !== QRCode::MASK_PATTERN_AUTO){
$this->maskPattern = max(0, min(7, $maskPattern));
}
}
/**
* sets the transparency background color
*
* @throws \chillerlan\QRCode\QRCodeException
*/
protected function set_imageTransparencyBG(array $imageTransparencyBG):void{
// invalid value - set to white as default
if(count($imageTransparencyBG) < 3){
$this->imageTransparencyBG = [255, 255, 255];
return;
}
foreach($imageTransparencyBG as $k => $v){
// cut off exceeding items
if($k > 2){
break;
}
if(!is_numeric($v)){
throw new QRCodeException('Invalid RGB value.');
}
// clamp the values
$this->imageTransparencyBG[$k] = max(0, min(255, (int)$v));
}
// use the array values to not run into errors with the spread operator (...$arr)
$this->imageTransparencyBG = array_values($this->imageTransparencyBG);
}
/**
* sets/clamps the version number
*/
protected function set_version(int $version):void{
if($version !== QRCode::VERSION_AUTO){
$this->version = max(1, min(40, $version));
}
}
/**
* sets the FPDF measurement unit
*
* @codeCoverageIgnore
*/
protected function set_fpdfMeasureUnit(string $unit):void{
$unit = strtolower($unit);
if(in_array($unit, ['cm', 'in', 'mm', 'pt'], true)){
$this->fpdfMeasureUnit = $unit;
}
// @todo throw or ignore silently?
}
}

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018 Smiley <smiley@chillerlan.net>
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,167 @@
# chillerlan/php-settings-container
A container class for settings objects - decouple configuration logic from your application! Not a DI container.
- [`SettingsContainerInterface`](https://github.com/chillerlan/php-settings-container/blob/main/src/SettingsContainerInterface.php) provides immutable properties with magic getter & setter and some fancy.
[![PHP Version Support][php-badge]][php]
[![version][packagist-badge]][packagist]
[![license][license-badge]][license]
[![Continuous Integration][gh-action-badge]][gh-action]
[![Coverage][coverage-badge]][coverage]
[![Codacy][codacy-badge]][codacy]
[![Packagist downloads][downloads-badge]][downloads]
[php-badge]: https://img.shields.io/packagist/php-v/chillerlan/php-settings-container?logo=php&color=8892BF
[php]: https://www.php.net/supported-versions.php
[packagist-badge]: https://img.shields.io/packagist/v/chillerlan/php-settings-container.svg?logo=packagist
[packagist]: https://packagist.org/packages/chillerlan/php-settings-container
[license-badge]: https://img.shields.io/github/license/chillerlan/php-settings-container.svg
[license]: https://github.com/chillerlan/php-settings-container/blob/main/LICENSE
[coverage-badge]: https://img.shields.io/codecov/c/github/chillerlan/php-settings-container.svg?logo=codecov
[coverage]: https://codecov.io/github/chillerlan/php-settings-container
[codacy-badge]: https://img.shields.io/codacy/grade/bd2467799e2943d2853ce3ebad5af490/main?logo=codacy
[codacy]: https://www.codacy.com/gh/chillerlan/php-settings-container/dashboard?branch=main
[downloads-badge]: https://img.shields.io/packagist/dt/chillerlan/php-settings-container.svg?logo=packagist
[downloads]: https://packagist.org/packages/chillerlan/php-settings-container/stats
[gh-action-badge]: https://img.shields.io/github/actions/workflow/status/chillerlan/php-settings-container/ci.yml?branch=main&logo=github
[gh-action]: https://github.com/chillerlan/php-settings-container/actions/workflows/ci.yml?query=branch%3Amain
## Documentation
### Installation
**requires [composer](https://getcomposer.org)**
*composer.json* (note: replace `dev-main` with a [version constraint](https://getcomposer.org/doc/articles/versions.md#writing-version-constraints), e.g. `^3.0` - see [releases](https://github.com/chillerlan/php-settings-container/releases) for valid versions)
```json
{
"require": {
"php": "^8.1",
"chillerlan/php-settings-container": "dev-main"
}
}
```
Profit!
## Usage
The `SettingsContainerInterface` (wrapped in`SettingsContainerAbstract`) provides plug-in functionality for immutable object properties and adds some fancy, like loading/saving JSON, arrays etc.
It takes an `iterable` as the only constructor argument and calls a method with the trait's name on invocation (`MyTrait::MyTrait()`) for each used trait.
A PHPStan ruleset to exclude errors generated by accessing magic properties on `SettingsContainerInterface` can be found in `rules-magic-access.neon`.
### Simple usage
```php
class MyContainer extends SettingsContainerAbstract{
protected string $foo;
protected string $bar;
}
```
```php
// use it just like a \stdClass (except the properties are fixed)
$container = new MyContainer;
$container->foo = 'what';
$container->bar = 'foo';
// which is equivalent to
$container = new MyContainer(['bar' => 'foo', 'foo' => 'what']);
// ...or try
$container->fromJSON('{"foo": "what", "bar": "foo"}');
// fetch all properties as array
$container->toArray(); // -> ['foo' => 'what', 'bar' => 'foo']
// or JSON
$container->toJSON(); // -> {"foo": "what", "bar": "foo"}
// JSON via JsonSerializable
$json = json_encode($container); // -> {"foo": "what", "bar": "foo"}
//non-existing properties will be ignored:
$container->nope = 'what';
var_dump($container->nope); // -> null
```
### Advanced usage
```php
// from library 1
trait SomeOptions{
protected string $foo;
protected string $what;
// this method will be called in SettingsContainerAbstract::construct()
// after the properties have been set
protected function SomeOptions():void{
// just some constructor stuff...
$this->foo = strtoupper($this->foo);
}
/*
* special prefixed magic setters & getters
*/
// this method will be called from __set() when property $what is set
protected function set_what(string $value):void{
$this->what = md5($value);
}
// this method is called on __get() for the property $what
protected function get_what():string{
return 'hash: '.$this->what;
}
}
// from library 2
trait MoreOptions{
protected string $bar = 'whatever'; // provide default values
}
```
```php
$commonOptions = [
// SomeOptions
'foo' => 'whatever',
// MoreOptions
'bar' => 'nothing',
];
// now plug the several library options together to a single object
$container = new class ($commonOptions) extends SettingsContainerAbstract{
use SomeOptions, MoreOptions;
};
var_dump($container->foo); // -> WHATEVER (constructor ran strtoupper on the value)
var_dump($container->bar); // -> nothing
$container->what = 'some value';
var_dump($container->what); // -> hash: 5946210c9e93ae37891dfe96c3e39614 (custom getter added "hash: ")
```
### API
#### [`SettingsContainerAbstract`](https://github.com/chillerlan/php-settings-container/blob/main/src/SettingsContainerAbstract.php)
| method | return | info |
|--------------------------------------------|------------------------------|---------------------------------------------------------------------------------------------------------------------|
| `__construct(iterable $properties = null)` | - | calls `construct()` internally after the properties have been set |
| (protected) `construct()` | void | calls a method with trait name as replacement constructor for each used trait |
| `__get(string $property)` | mixed | calls `$this->{'get_'.$property}()` if such a method exists |
| `__set(string $property, $value)` | void | calls `$this->{'set_'.$property}($value)` if such a method exists |
| `__isset(string $property)` | bool | |
| `__unset(string $property)` | void | |
| `__toString()` | string | a JSON string |
| `toArray()` | array | |
| `fromIterable(iterable $properties)` | `SettingsContainerInterface` | |
| `toJSON(int $jsonOptions = null)` | string | accepts [JSON options constants](http://php.net/manual/json.constants.php) |
| `fromJSON(string $json)` | `SettingsContainerInterface` | |
| `jsonSerialize()` | mixed | implements the [`JsonSerializable`](https://www.php.net/manual/en/jsonserializable.jsonserialize.php) interface |
| `serialize()` | string | implements the [`Serializable`](https://www.php.net/manual/en/serializable.serialize.php) interface |
| `unserialize(string $data)` | void | implements the [`Serializable`](https://www.php.net/manual/en/serializable.unserialize.php) interface |
| `__serialize()` | array | implements the [`Serializable`](https://www.php.net/manual/en/language.oop5.magic.php#object.serialize) interface |
| `__unserialize(array $data)` | void | implements the [`Serializable`](https://www.php.net/manual/en/language.oop5.magic.php#object.unserialize) interface |
## Disclaimer
This might be either an utterly genius or completely stupid idea - you decide. However, i like it and it works.
Also, this is not a dependency injection container. Stop using DI containers FFS.

View File

@ -0,0 +1,52 @@
{
"name": "chillerlan/php-settings-container",
"description": "A container class for immutable settings objects. Not a DI container.",
"homepage": "https://github.com/chillerlan/php-settings-container",
"license": "MIT",
"type": "library",
"minimum-stability": "stable",
"keywords": [
"helper", "container", "settings", "configuration"
],
"authors": [
{
"name": "Smiley",
"email": "smiley@chillerlan.net",
"homepage": "https://github.com/codemasher"
}
],
"support": {
"issues": "https://github.com/chillerlan/php-settings-container/issues",
"source": "https://github.com/chillerlan/php-settings-container"
},
"require": {
"php": "^8.1",
"ext-json": "*"
},
"require-dev": {
"phpmd/phpmd": "^2.15",
"phpstan/phpstan": "^1.11",
"phpstan/phpstan-deprecation-rules": "^1.2",
"phpunit/phpunit": "^10.5",
"squizlabs/php_codesniffer": "^3.10"
},
"autoload": {
"psr-4": {
"chillerlan\\Settings\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"chillerlan\\SettingsTest\\": "tests"
}
},
"scripts": {
"phpunit": "@php vendor/bin/phpunit",
"phpstan": "@php vendor/bin/phpstan"
},
"config": {
"lock": false,
"sort-packages": true,
"platform-check": true
}
}

View File

@ -0,0 +1,4 @@
parameters:
ignoreErrors:
# yes, these are magic
- message: "#^Access to an undefined property chillerlan\\\\Settings\\\\SettingsContainerInterface\\:\\:\\$[\\w]+\\.$#"

View File

@ -0,0 +1,252 @@
<?php
/**
* Class SettingsContainerAbstract
*
* @created 28.08.2018
* @author Smiley <smiley@chillerlan.net>
* @copyright 2018 Smiley
* @license MIT
*/
declare(strict_types=1);
namespace chillerlan\Settings;
use InvalidArgumentException, JsonException, ReflectionClass, ReflectionProperty;
use function array_keys, get_object_vars, is_object, json_decode, json_encode,
json_last_error_msg, method_exists, property_exists, serialize, unserialize;
use const JSON_THROW_ON_ERROR;
abstract class SettingsContainerAbstract implements SettingsContainerInterface{
/**
* SettingsContainerAbstract constructor.
*
* @phpstan-param array<string, mixed> $properties
*/
public function __construct(iterable|null $properties = null){
if(!empty($properties)){
$this->fromIterable($properties);
}
$this->construct();
}
/**
* calls a method with trait name as replacement constructor for each used trait
* (remember pre-php5 classname constructors? yeah, basically this.)
*/
protected function construct():void{
$traits = (new ReflectionClass($this))->getTraits();
foreach($traits as $trait){
$method = $trait->getShortName();
if(method_exists($this, $method)){
$this->{$method}();
}
}
}
/**
* @inheritdoc
*/
public function __get(string $property):mixed{
if(!property_exists($this, $property) || $this->isPrivate($property)){
return null;
}
$method = 'get_'.$property;
if(method_exists($this, $method)){
return $this->{$method}();
}
return $this->{$property};
}
/**
* @inheritdoc
*/
public function __set(string $property, mixed $value):void{
if(!property_exists($this, $property) || $this->isPrivate($property)){
return;
}
$method = 'set_'.$property;
if(method_exists($this, $method)){
$this->{$method}($value);
return;
}
$this->{$property} = $value;
}
/**
* @inheritdoc
*/
public function __isset(string $property):bool{
return isset($this->{$property}) && !$this->isPrivate($property);
}
/**
* @internal Checks if a property is private
*/
protected function isPrivate(string $property):bool{
return (new ReflectionProperty($this, $property))->isPrivate();
}
/**
* @inheritdoc
*/
public function __unset(string $property):void{
if($this->__isset($property)){
unset($this->{$property});
}
}
/**
* @inheritdoc
*/
public function __toString():string{
return $this->toJSON();
}
/**
* @inheritdoc
*/
public function toArray():array{
$properties = [];
foreach(array_keys(get_object_vars($this)) as $key){
$properties[$key] = $this->__get($key);
}
return $properties;
}
/**
* @inheritdoc
*/
public function fromIterable(iterable $properties):static{
foreach($properties as $key => $value){
$this->__set($key, $value);
}
return $this;
}
/**
* @inheritdoc
*/
public function toJSON(int|null $jsonOptions = null):string{
$json = json_encode($this, ($jsonOptions ?? 0));
if($json === false){
throw new JsonException(json_last_error_msg());
}
return $json;
}
/**
* @inheritdoc
*/
public function fromJSON(string $json):static{
/** @phpstan-var array<string, mixed> $data */
$data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
return $this->fromIterable($data);
}
/**
* @inheritdoc
* @return array<string, mixed>
*/
public function jsonSerialize():array{
return $this->toArray();
}
/**
* Returns a serialized string representation of the object in its current state (except static/readonly properties)
*
* @inheritdoc
* @see \chillerlan\Settings\SettingsContainerInterface::toArray()
*/
public function serialize():string{
return serialize($this);
}
/**
* Restores the data (except static/readonly properties) from the given serialized object to the current instance
*
* @inheritdoc
* @see \chillerlan\Settings\SettingsContainerInterface::fromIterable()
*/
public function unserialize(string $data):void{
$obj = unserialize($data);
if($obj === false || !is_object($obj)){
throw new InvalidArgumentException('The given serialized string is invalid');
}
$reflection = new ReflectionClass($obj);
if(!$reflection->isInstance($this)){
throw new InvalidArgumentException('The unserialized object does not match the class of this container');
}
$properties = $reflection->getProperties(~(ReflectionProperty::IS_STATIC | ReflectionProperty::IS_READONLY));
foreach($properties as $reflectionProperty){
$this->{$reflectionProperty->name} = $reflectionProperty->getValue($obj);
}
}
/**
* Returns a serialized string representation of the object in its current state (except static/readonly properties)
*
* @inheritdoc
* @see \chillerlan\Settings\SettingsContainerInterface::toArray()
*/
public function __serialize():array{
$properties = (new ReflectionClass($this))
->getProperties(~(ReflectionProperty::IS_STATIC | ReflectionProperty::IS_READONLY))
;
$data = [];
foreach($properties as $reflectionProperty){
$data[$reflectionProperty->name] = $reflectionProperty->getValue($this);
}
return $data;
}
/**
* Restores the data from the given array to the current instance
*
* @inheritdoc
* @see \chillerlan\Settings\SettingsContainerInterface::fromIterable()
*
* @param array<string, mixed> $data
*/
public function __unserialize(array $data):void{
foreach($data as $key => $value){
$this->{$key} = $value;
}
}
}

View File

@ -0,0 +1,86 @@
<?php
/**
* Interface SettingsContainerInterface
*
* @created 28.08.2018
* @author Smiley <smiley@chillerlan.net>
* @copyright 2018 Smiley
* @license MIT
*/
declare(strict_types=1);
namespace chillerlan\Settings;
use JsonSerializable, Serializable;
/**
* a generic container with magic getter and setter
*/
interface SettingsContainerInterface extends JsonSerializable, Serializable{
/**
* Retrieve the value of $property
*
* @return mixed|null
*/
public function __get(string $property):mixed;
/**
* Set $property to $value while avoiding private and non-existing properties
*/
public function __set(string $property, mixed $value):void;
/**
* Checks if $property is set (aka. not null), excluding private properties
*/
public function __isset(string $property):bool;
/**
* Unsets $property while avoiding private and non-existing properties
*/
public function __unset(string $property):void;
/**
* @see \chillerlan\Settings\SettingsContainerInterface::toJSON()
*/
public function __toString():string;
/**
* Returns an array representation of the settings object
*
* The values will be run through the magic __get(), which may also call custom getters.
*
* @return array<string, mixed>
*/
public function toArray():array;
/**
* Sets properties from a given iterable
*
* The values will be run through the magic __set(), which may also call custom setters.
*
* @phpstan-param array<string, mixed> $properties
*/
public function fromIterable(iterable $properties):static;
/**
* Returns a JSON representation of the settings object
*
* @see \json_encode()
* @see \chillerlan\Settings\SettingsContainerInterface::toArray()
*
* @throws \JsonException
*/
public function toJSON(int|null $jsonOptions = null):string;
/**
* Sets properties from a given JSON string
*
* @see \chillerlan\Settings\SettingsContainerInterface::fromIterable()
*
* @throws \Exception
* @throws \JsonException
*/
public function fromJSON(string $json):static;
}

View File

@ -6,6 +6,8 @@ $vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'chillerlan\\Settings\\' => array($vendorDir . '/chillerlan/php-settings-container/src'),
'chillerlan\\QRCode\\' => array($vendorDir . '/chillerlan/php-qrcode/src'),
'Twig\\' => array($vendorDir . '/twig/twig/src'),
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'),

View File

@ -20,6 +20,11 @@ class ComposerStaticInit071586d19f5409de22b3235d85d8476c
);
public static $prefixLengthsPsr4 = array (
'c' =>
array (
'chillerlan\\Settings\\' => 20,
'chillerlan\\QRCode\\' => 18,
),
'T' =>
array (
'Twig\\' => 5,
@ -50,6 +55,14 @@ class ComposerStaticInit071586d19f5409de22b3235d85d8476c
);
public static $prefixDirsPsr4 = array (
'chillerlan\\Settings\\' =>
array (
0 => __DIR__ . '/..' . '/chillerlan/php-settings-container/src',
),
'chillerlan\\QRCode\\' =>
array (
0 => __DIR__ . '/..' . '/chillerlan/php-qrcode/src',
),
'Twig\\' =>
array (
0 => __DIR__ . '/..' . '/twig/twig/src',

View File

@ -1,5 +1,154 @@
{
"packages": [
{
"name": "chillerlan/php-qrcode",
"version": "4.4.2",
"version_normalized": "4.4.2.0",
"source": {
"type": "git",
"url": "https://github.com/chillerlan/php-qrcode.git",
"reference": "345ed8e4ffb56e6b3fcd9f42e3970b9026fa6ce4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/345ed8e4ffb56e6b3fcd9f42e3970b9026fa6ce4",
"reference": "345ed8e4ffb56e6b3fcd9f42e3970b9026fa6ce4",
"shasum": ""
},
"require": {
"chillerlan/php-settings-container": "^2.1.6 || ^3.2.1",
"ext-mbstring": "*",
"php": "^7.4 || ^8.0"
},
"require-dev": {
"phan/phan": "^5.4.5",
"phpmd/phpmd": "^2.15",
"phpunit/phpunit": "^9.6",
"setasign/fpdf": "^1.8.2",
"squizlabs/php_codesniffer": "^3.11"
},
"suggest": {
"chillerlan/php-authenticator": "Yet another Google authenticator! Also creates URIs for mobile apps.",
"setasign/fpdf": "Required to use the QR FPDF output.",
"simple-icons/simple-icons": "SVG icons that you can use to embed as logos in the QR Code"
},
"time": "2024-11-15T15:36:24+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"chillerlan\\QRCode\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Kazuhiko Arase",
"homepage": "https://github.com/kazuhikoarase"
},
{
"name": "Smiley",
"email": "smiley@chillerlan.net",
"homepage": "https://github.com/codemasher"
},
{
"name": "Contributors",
"homepage": "https://github.com/chillerlan/php-qrcode/graphs/contributors"
}
],
"description": "A QR code generator with a user friendly API. PHP 7.4+",
"homepage": "https://github.com/chillerlan/php-qrcode",
"keywords": [
"phpqrcode",
"qr",
"qr code",
"qrcode",
"qrcode-generator"
],
"support": {
"issues": "https://github.com/chillerlan/php-qrcode/issues",
"source": "https://github.com/chillerlan/php-qrcode/tree/4.4.2"
},
"funding": [
{
"url": "https://ko-fi.com/codemasher",
"type": "ko_fi"
}
],
"install-path": "../chillerlan/php-qrcode"
},
{
"name": "chillerlan/php-settings-container",
"version": "3.2.1",
"version_normalized": "3.2.1.0",
"source": {
"type": "git",
"url": "https://github.com/chillerlan/php-settings-container.git",
"reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/95ed3e9676a1d47cab2e3174d19b43f5dbf52681",
"reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": "^8.1"
},
"require-dev": {
"phpmd/phpmd": "^2.15",
"phpstan/phpstan": "^1.11",
"phpstan/phpstan-deprecation-rules": "^1.2",
"phpunit/phpunit": "^10.5",
"squizlabs/php_codesniffer": "^3.10"
},
"time": "2024-07-16T11:13:48+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"chillerlan\\Settings\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Smiley",
"email": "smiley@chillerlan.net",
"homepage": "https://github.com/codemasher"
}
],
"description": "A container class for immutable settings objects. Not a DI container.",
"homepage": "https://github.com/chillerlan/php-settings-container",
"keywords": [
"Settings",
"configuration",
"container",
"helper"
],
"support": {
"issues": "https://github.com/chillerlan/php-settings-container/issues",
"source": "https://github.com/chillerlan/php-settings-container"
},
"funding": [
{
"url": "https://www.paypal.com/donate?hosted_button_id=WLYUNAT9ZTJZ4",
"type": "custom"
},
{
"url": "https://ko-fi.com/codemasher",
"type": "ko_fi"
}
],
"install-path": "../chillerlan/php-settings-container"
},
{
"name": "guzzlehttp/psr7",
"version": "2.8.0",

View File

@ -19,6 +19,24 @@
'aliases' => array(),
'dev_requirement' => false,
),
'chillerlan/php-qrcode' => array(
'pretty_version' => '4.4.2',
'version' => '4.4.2.0',
'reference' => '345ed8e4ffb56e6b3fcd9f42e3970b9026fa6ce4',
'type' => 'library',
'install_path' => __DIR__ . '/../chillerlan/php-qrcode',
'aliases' => array(),
'dev_requirement' => false,
),
'chillerlan/php-settings-container' => array(
'pretty_version' => '3.2.1',
'version' => '3.2.1.0',
'reference' => '95ed3e9676a1d47cab2e3174d19b43f5dbf52681',
'type' => 'library',
'install_path' => __DIR__ . '/../chillerlan/php-settings-container',
'aliases' => array(),
'dev_requirement' => false,
),
'guzzlehttp/psr7' => array(
'pretty_version' => '2.8.0',
'version' => '2.8.0.0',