Fase 3 feedback round: style preview, icon upload, font choice, optional bitcoin amount, WPA3
- Default the Owner select to the creator's own account instead of "All", so
superadmin-created codes no longer silently become visible to every admin
(the underlying NULL-fallback sharing behavior for an explicit "All" choice
is unchanged).
- Add a live color/precision/size preview swatch next to the preset picker.
- Let the frame text use a chosen DejaVu font + font size instead of a fixed
GD bitmap font.
- Add an optional self-uploaded icon rendered above the qr code (not embedded
in it, so scanability is unaffected).
- Make the Bitcoin qr amount optional; a standing wallet address is useful
without forcing a one-off amount per code.
- Add a WPA3 option to the WiFi qr encryption select.
- Fix a real bug surfaced while testing the preview/style JS: qrcode_options.php
was included once per static qr type (16 times on one page) and each
inclusion re-executed <script src="qrcode-style-tools.js">, so every button
click fired once per type - e.g. saving one preset wrote 16 duplicate rows,
and every tab except the first ("Text") had dead random-style/preset
buttons since only the first DOM match ever got a listener. Moved the
script include to load once per page and rewrote the JS to scope every
lookup to the triggering element's own tab-pane/form instead of relying on
getElementById's first-match behavior.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0196bLhZhuxwK3MkHuKLe5WB
This commit is contained in:
Vendored
+121
-41
@@ -1,12 +1,23 @@
|
||||
// Preset system + random style button for the qr code generation forms (Fase 3, priority 2).
|
||||
//
|
||||
// The static qr "add" page stacks all qr-type forms (text/email/.../wifi/bitcoin/...) into
|
||||
// the DOM at once as Bootstrap tab-panes, and every one of them repeats the same element ids
|
||||
// (foreground, background, size, random_style_btn, ...). getElementById/getElementsBy* only
|
||||
// ever finds the *first* of those (the "Text" tab), so every helper below is scoped to the
|
||||
// specific tab-pane/form the triggering element lives in, and wired up via querySelectorAll
|
||||
// so every tab gets working listeners, not just the first one.
|
||||
(function () {
|
||||
function csrfToken() {
|
||||
var meta = document.querySelector('meta[name="csrf-token"]');
|
||||
return meta ? meta.getAttribute('content') : '';
|
||||
}
|
||||
|
||||
function setColor(id, hex) {
|
||||
var input = document.getElementById(id);
|
||||
function scopeOf(el) {
|
||||
return el.closest('.tab-pane') || el.closest('form') || document;
|
||||
}
|
||||
|
||||
function setColor(scope, id, hex) {
|
||||
var input = scope.querySelector('#' + id);
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
@@ -27,12 +38,35 @@
|
||||
return '#' + ('000000' + value).slice(-6);
|
||||
}
|
||||
|
||||
function loadPresets() {
|
||||
var select = document.getElementById('preset_select');
|
||||
if (!select) {
|
||||
function updateStylePreview(scope) {
|
||||
var swatch = scope.querySelector('#style_preview_swatch');
|
||||
var text = scope.querySelector('#style_preview_text');
|
||||
if (!swatch || !text) {
|
||||
return;
|
||||
}
|
||||
|
||||
var foreground = scope.querySelector('#foreground');
|
||||
var background = scope.querySelector('#background');
|
||||
var levelSelect = scope.querySelector('select[name="level"]');
|
||||
var sizeSelect = scope.querySelector('#size');
|
||||
|
||||
var fg = foreground ? foreground.value : '#000000';
|
||||
var bg = background ? background.value : '#ffffff';
|
||||
|
||||
swatch.style.background = bg;
|
||||
swatch.style.borderColor = fg;
|
||||
|
||||
var parts = [];
|
||||
if (levelSelect) {
|
||||
parts.push('Precision: ' + levelSelect.value);
|
||||
}
|
||||
if (sizeSelect) {
|
||||
parts.push('Size: ' + sizeSelect.value + 'px');
|
||||
}
|
||||
text.textContent = parts.join(' · ');
|
||||
}
|
||||
|
||||
function loadPresetsInto(select) {
|
||||
fetch('presets.php?action=list')
|
||||
.then(function (response) { return response.json(); })
|
||||
.then(function (json) {
|
||||
@@ -54,43 +88,69 @@
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
loadPresets();
|
||||
document.querySelectorAll('#preset_select').forEach(loadPresetsInto);
|
||||
|
||||
var randomBtn = document.getElementById('random_style_btn');
|
||||
if (randomBtn) {
|
||||
randomBtn.addEventListener('click', function () {
|
||||
setColor('foreground', randomHexColor());
|
||||
setColor('background', randomHexColor());
|
||||
document.querySelectorAll('#style_preview').forEach(function (row) {
|
||||
updateStylePreview(scopeOf(row));
|
||||
});
|
||||
|
||||
document.querySelectorAll('#foreground, #background').forEach(function (input) {
|
||||
input.addEventListener('change', function () {
|
||||
updateStylePreview(scopeOf(input));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var presetSelect = document.getElementById('preset_select');
|
||||
if (presetSelect) {
|
||||
document.querySelectorAll('select[name="level"]').forEach(function (select) {
|
||||
select.addEventListener('change', function () {
|
||||
updateStylePreview(scopeOf(select));
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('#size').forEach(function (select) {
|
||||
select.addEventListener('change', function () {
|
||||
updateStylePreview(scopeOf(select));
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('#random_style_btn').forEach(function (randomBtn) {
|
||||
randomBtn.addEventListener('click', function () {
|
||||
var scope = scopeOf(randomBtn);
|
||||
setColor(scope, 'foreground', randomHexColor());
|
||||
setColor(scope, 'background', randomHexColor());
|
||||
updateStylePreview(scope);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('#preset_select').forEach(function (presetSelect) {
|
||||
presetSelect.addEventListener('change', function () {
|
||||
var option = presetSelect.options[presetSelect.selectedIndex];
|
||||
if (!option.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
setColor('foreground', option.dataset.foreground);
|
||||
setColor('background', option.dataset.background);
|
||||
var scope = scopeOf(presetSelect);
|
||||
|
||||
var levelSelect = document.querySelector('select[name="level"]');
|
||||
setColor(scope, 'foreground', option.dataset.foreground);
|
||||
setColor(scope, 'background', option.dataset.background);
|
||||
|
||||
var levelSelect = scope.querySelector('select[name="level"]');
|
||||
if (levelSelect) {
|
||||
levelSelect.value = option.dataset.level;
|
||||
}
|
||||
|
||||
var sizeSelect = document.getElementById('size');
|
||||
var sizeSelect = scope.querySelector('#size');
|
||||
if (sizeSelect) {
|
||||
sizeSelect.value = option.dataset.size;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var saveBtn = document.getElementById('preset_save_btn');
|
||||
if (saveBtn) {
|
||||
updateStylePreview(scope);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('#preset_save_btn').forEach(function (saveBtn) {
|
||||
saveBtn.addEventListener('click', function () {
|
||||
var nameInput = document.getElementById('preset_name');
|
||||
var scope = scopeOf(saveBtn);
|
||||
var nameInput = scope.querySelector('#preset_name');
|
||||
var name = nameInput.value.trim();
|
||||
|
||||
if (!name) {
|
||||
@@ -98,13 +158,18 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var foreground = scope.querySelector('#foreground').value;
|
||||
var background = scope.querySelector('#background').value;
|
||||
var level = scope.querySelector('select[name="level"]').value;
|
||||
var size = scope.querySelector('#size').value;
|
||||
|
||||
var body = new URLSearchParams();
|
||||
body.set('action', 'save');
|
||||
body.set('name', name);
|
||||
body.set('foreground', document.getElementById('foreground').value);
|
||||
body.set('background', document.getElementById('background').value);
|
||||
body.set('level', document.querySelector('select[name="level"]').value);
|
||||
body.set('size', document.getElementById('size').value);
|
||||
body.set('foreground', foreground);
|
||||
body.set('background', background);
|
||||
body.set('level', level);
|
||||
body.set('size', size);
|
||||
|
||||
fetch('presets.php', {
|
||||
method: 'POST',
|
||||
@@ -118,24 +183,33 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var option = document.createElement('option');
|
||||
option.value = json.data.id;
|
||||
option.textContent = json.data.name;
|
||||
option.dataset.foreground = document.getElementById('foreground').value;
|
||||
option.dataset.background = document.getElementById('background').value;
|
||||
option.dataset.level = document.querySelector('select[name="level"]').value;
|
||||
option.dataset.size = document.getElementById('size').value;
|
||||
presetSelect.appendChild(option);
|
||||
presetSelect.value = option.value;
|
||||
// The preset list is shared across every tab, so mirror the new
|
||||
// option into every preset_select on the page, not just this one.
|
||||
document.querySelectorAll('#preset_select').forEach(function (select) {
|
||||
var option = document.createElement('option');
|
||||
option.value = json.data.id;
|
||||
option.textContent = json.data.name;
|
||||
option.dataset.foreground = foreground;
|
||||
option.dataset.background = background;
|
||||
option.dataset.level = level;
|
||||
option.dataset.size = size;
|
||||
select.appendChild(option);
|
||||
|
||||
if (select === scope.querySelector('#preset_select')) {
|
||||
select.value = option.value;
|
||||
}
|
||||
});
|
||||
|
||||
nameInput.value = '';
|
||||
})
|
||||
.catch(function () { alert('Could not save preset (network error).'); });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var deleteBtn = document.getElementById('preset_delete_btn');
|
||||
if (deleteBtn) {
|
||||
document.querySelectorAll('#preset_delete_btn').forEach(function (deleteBtn) {
|
||||
deleteBtn.addEventListener('click', function () {
|
||||
var scope = scopeOf(deleteBtn);
|
||||
var presetSelect = scope.querySelector('#preset_select');
|
||||
var option = presetSelect.options[presetSelect.selectedIndex];
|
||||
if (!option.value) {
|
||||
return;
|
||||
@@ -157,13 +231,19 @@
|
||||
.then(function (response) { return response.json(); })
|
||||
.then(function (json) {
|
||||
if (json.status === 200) {
|
||||
option.remove();
|
||||
// Remove the matching option from every preset_select on the page.
|
||||
document.querySelectorAll('#preset_select').forEach(function (select) {
|
||||
var match = select.querySelector('option[value="' + option.value + '"]');
|
||||
if (match) {
|
||||
match.remove();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
alert('Could not delete preset: ' + json.data);
|
||||
}
|
||||
})
|
||||
.catch(function () { alert('Could not delete preset (network error).'); });
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
+11
-2
@@ -37,7 +37,7 @@ if($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["edit"])) {
|
||||
$dynamic_qrcode_instance->editQrcode($_POST);
|
||||
}
|
||||
|
||||
if($_SERVER["REQUEST_METHOD"] === "POST" && !isset($_POST["edit"])) {
|
||||
if($_SERVER["REQUEST_METHOD"] === "POST" && !isset($_POST["edit"]) && !isset($_POST["del_id"])) {
|
||||
if(
|
||||
isset($_POST["foreground"]) &&
|
||||
isset($_POST["background"]) &&
|
||||
@@ -45,8 +45,17 @@ if($_SERVER["REQUEST_METHOD"] === "POST" && !isset($_POST["edit"])) {
|
||||
isset($_POST["filename"]) &&
|
||||
isset($_POST["format"]) &&
|
||||
isset($_POST["id_owner"])
|
||||
)
|
||||
) {
|
||||
$icon_upload = qr_handle_icon_upload('icon');
|
||||
if (!$icon_upload['ok']) {
|
||||
$_SESSION['failure'] = $icon_upload['error'];
|
||||
header('Location: ' . basename(__FILE__));
|
||||
exit;
|
||||
}
|
||||
$_POST['icon_tmp_path'] = $icon_upload['path'];
|
||||
|
||||
$dynamic_qrcode_instance->addQrcode($_POST);
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
<div class="col-sm-12 mb-2">
|
||||
<div class="row">
|
||||
<div class="col-6 col-md-3">
|
||||
<label> </label>
|
||||
<button type="button" id="random_style_btn" class="btn btn-outline-secondary btn-block">
|
||||
<i class="fa fa-dice"></i> Random style
|
||||
</button>
|
||||
@@ -80,6 +81,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-3">
|
||||
<label>Style preview</label>
|
||||
<div id="style_preview" class="d-flex align-items-center">
|
||||
<span id="style_preview_swatch" style="display:inline-block;width:38px;height:38px;border:3px solid #000;background:#fff;border-radius:4px;"></span>
|
||||
<small id="style_preview_text" class="ml-2 text-muted"></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -136,6 +145,35 @@
|
||||
<small class="form-text text-muted">Optional label rendered below the code. Only applies to PNG/JPEG/GIF, not SVG/EPS.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="form-group">
|
||||
<label for="frame_font">Frame font</label>
|
||||
<select name="frame_font" id="frame_font" class="form-control">
|
||||
<option value="sans" selected>Sans</option>
|
||||
<option value="sans-bold">Sans Bold</option>
|
||||
<option value="serif">Serif</option>
|
||||
<option value="serif-bold">Serif Bold</option>
|
||||
<option value="mono">Monospace</option>
|
||||
<option value="mono-bold">Monospace Bold</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="form-group">
|
||||
<label for="frame_font_size">Frame font size</label>
|
||||
<input type="number" name="frame_font_size" id="frame_font_size" value="16" min="8" max="60" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-4">
|
||||
<div class="form-group">
|
||||
<label for="icon">Icon above QR code</label>
|
||||
<input type="file" name="icon" id="icon" accept="image/png,image/jpeg,image/gif" class="form-control-file">
|
||||
<small class="form-text text-muted">Optional. PNG/JPEG/GIF, max 1MB. Shown above the code (not embedded in it). Only applies to PNG/JPEG/GIF output.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -146,7 +184,7 @@
|
||||
<div class="form-group">
|
||||
<label for="id_owner">Owner *</label>
|
||||
<select name="id_owner" class="form-control">
|
||||
<option value="" selected>All</option>
|
||||
<option value="">All (shared with every admin)</option>
|
||||
<?php
|
||||
|
||||
require_once BASE_PATH . '/lib/Users/Users.php';
|
||||
@@ -154,8 +192,9 @@
|
||||
$users = $users_instance->getAllUsers();
|
||||
|
||||
foreach ($users as $user) {
|
||||
$is_self = (int) $user["id"] === (int) $_SESSION["user_id"];
|
||||
?>
|
||||
<option value="<?php echo $user["id"];?>"><?php echo $user["username"];?></option>
|
||||
<option value="<?php echo $user["id"];?>" <?php echo $is_self ? 'selected' : ''; ?>><?php echo $user["username"];?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -105,4 +105,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /.card -->
|
||||
|
||||
<script src="dist/js/qrcode-style-tools.js?nocache=<?php print rand();?>"></script>
|
||||
</fieldset>
|
||||
@@ -58,6 +58,7 @@ if (QRCODE_GENERATOR === "internal-chillerlan.qrcode") {
|
||||
<div class="col-sm-12 mb-2">
|
||||
<div class="row">
|
||||
<div class="col-6 col-md-3">
|
||||
<label> </label>
|
||||
<button type="button" id="random_style_btn" class="btn btn-outline-secondary btn-block">
|
||||
<i class="fa fa-dice"></i> Random style
|
||||
</button>
|
||||
@@ -84,10 +85,25 @@ if (QRCODE_GENERATOR === "internal-chillerlan.qrcode") {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-3">
|
||||
<label>Style preview</label>
|
||||
<div id="style_preview" class="d-flex align-items-center">
|
||||
<span id="style_preview_swatch" style="display:inline-block;width:38px;height:38px;border:3px solid #000;background:#fff;border-radius:4px;"></span>
|
||||
<small id="style_preview_text" class="ml-2 text-muted"></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="dist/js/qrcode-style-tools.js?nocache=<?php print rand();?>"></script>
|
||||
<!--
|
||||
Note: no <script src="dist/js/qrcode-style-tools.js"> tag here on purpose. This
|
||||
partial is included once per qr type on the static "add" page (form_static_add.php),
|
||||
which stacks all types into the DOM as tab-panes; including the script here would load
|
||||
and execute it once per type, each execution re-attaching its own listeners to every
|
||||
button on the page. The script is included exactly once by the pages that use this
|
||||
partial (form_static_add.php and form_dynamic_add.php).
|
||||
-->
|
||||
|
||||
<!-- Its use is not recommended. Read the documentation
|
||||
<div class="form-group">
|
||||
@@ -130,6 +146,35 @@ if (QRCODE_GENERATOR === "internal-chillerlan.qrcode") {
|
||||
<small class="form-text text-muted">Optional label rendered below the code. Only applies to PNG/JPEG/GIF, not SVG/EPS.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="form-group">
|
||||
<label for="frame_font">Frame font</label>
|
||||
<select name="frame_font" id="frame_font" class="form-control">
|
||||
<option value="sans" selected>Sans</option>
|
||||
<option value="sans-bold">Sans Bold</option>
|
||||
<option value="serif">Serif</option>
|
||||
<option value="serif-bold">Serif Bold</option>
|
||||
<option value="mono">Monospace</option>
|
||||
<option value="mono-bold">Monospace Bold</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="form-group">
|
||||
<label for="frame_font_size">Frame font size</label>
|
||||
<input type="number" name="frame_font_size" id="frame_font_size" value="16" min="8" max="60" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-4">
|
||||
<div class="form-group">
|
||||
<label for="icon">Icon above QR code</label>
|
||||
<input type="file" name="icon" id="icon" accept="image/png,image/jpeg,image/gif" class="form-control-file">
|
||||
<small class="form-text text-muted">Optional. PNG/JPEG/GIF, max 1MB. Shown above the code (not embedded in it). Only applies to PNG/JPEG/GIF output.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -140,7 +185,7 @@ if (QRCODE_GENERATOR === "internal-chillerlan.qrcode") {
|
||||
<div class="form-group">
|
||||
<label for="id_owner">Owner *</label>
|
||||
<select name="id_owner" class="form-control">
|
||||
<option value="" selected>All</option>
|
||||
<option value="">All (shared with every admin)</option>
|
||||
<?php
|
||||
|
||||
require_once BASE_PATH . '/lib/Users/Users.php';
|
||||
@@ -148,8 +193,9 @@ if (QRCODE_GENERATOR === "internal-chillerlan.qrcode") {
|
||||
$users = $users_instance->getAllUsers();
|
||||
|
||||
foreach ($users as $user) {
|
||||
$is_self = (int) $user["id"] === (int) $_SESSION["user_id"];
|
||||
?>
|
||||
<option value="<?php echo $user["id"];?>"><?php echo $user["username"];?></option>
|
||||
<option value="<?php echo $user["id"];?>" <?php echo $is_self ? 'selected' : ''; ?>><?php echo $user["username"];?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="form-group">
|
||||
<label>Amount *</label>
|
||||
<label>Amount</label>
|
||||
<div class="input-group">
|
||||
<input type="number" name="amount" value="" placeholder="" class="form-control" step="0.0001">
|
||||
<div class="input-group-append">
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<label>Encryption *</label>
|
||||
<select name="encryption" class="form-control">
|
||||
<option value="WPA" Selected>WPA/WPA2</option>
|
||||
<option value="WPA3">WPA3</option>
|
||||
<option value="WEP">WEP</option>
|
||||
<option value="">None</option>
|
||||
</select>
|
||||
|
||||
@@ -117,6 +117,45 @@ function paginationLinks($current_page, $total_pages, $base_url) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the optional "icon above the qr code" upload. Validates the actual image
|
||||
* type (not just the extension/mime the browser claims) and stores the file outside
|
||||
* the document root, next to the generated qr codes.
|
||||
*/
|
||||
function qr_handle_icon_upload($fileKey = 'icon') {
|
||||
if (!isset($_FILES[$fileKey]) || $_FILES[$fileKey]['error'] === UPLOAD_ERR_NO_FILE) {
|
||||
return ['ok' => true, 'path' => null];
|
||||
}
|
||||
|
||||
if ($_FILES[$fileKey]['error'] !== UPLOAD_ERR_OK) {
|
||||
return ['ok' => false, 'error' => 'Icon upload failed.'];
|
||||
}
|
||||
|
||||
if ($_FILES[$fileKey]['size'] > 1024 * 1024) {
|
||||
return ['ok' => false, 'error' => 'Icon must be smaller than 1MB.'];
|
||||
}
|
||||
|
||||
$info = @getimagesize($_FILES[$fileKey]['tmp_name']);
|
||||
$allowed = [IMAGETYPE_PNG => 'png', IMAGETYPE_JPEG => 'jpg', IMAGETYPE_GIF => 'gif'];
|
||||
|
||||
if ($info === false || !isset($allowed[$info[2]])) {
|
||||
return ['ok' => false, 'error' => 'Icon must be a PNG, JPEG or GIF image.'];
|
||||
}
|
||||
|
||||
$dir = SAVED_QRCODE_DIRECTORY . 'icons/';
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0750, true);
|
||||
}
|
||||
|
||||
$destination = $dir . bin2hex(random_bytes(16)) . '.' . $allowed[$info[2]];
|
||||
|
||||
if (!move_uploaded_file($_FILES[$fileKey]['tmp_name'], $destination)) {
|
||||
return ['ok' => false, 'error' => 'Could not store the uploaded icon.'];
|
||||
}
|
||||
|
||||
return ['ok' => true, 'path' => $destination];
|
||||
}
|
||||
|
||||
function base_url() {
|
||||
require_once(__DIR__ . '/../config/environment.php');
|
||||
if (defined('BASE_URL') && BASE_URL !== null) {
|
||||
|
||||
@@ -72,11 +72,29 @@ class Qrcode {
|
||||
return $format;
|
||||
}
|
||||
|
||||
const FRAME_FONT_DIR = '/usr/share/fonts/truetype/dejavu/';
|
||||
|
||||
const ALLOWED_FRAME_FONTS = [
|
||||
'sans' => 'DejaVuSans.ttf',
|
||||
'sans-bold' => 'DejaVuSans-Bold.ttf',
|
||||
'serif' => 'DejaVuSerif.ttf',
|
||||
'serif-bold' => 'DejaVuSerif-Bold.ttf',
|
||||
'mono' => 'DejaVuSansMono.ttf',
|
||||
'mono-bold' => 'DejaVuSansMono-Bold.ttf',
|
||||
];
|
||||
|
||||
private static function resolveFrameFont($fontKey) {
|
||||
$file = self::ALLOWED_FRAME_FONTS[$fontKey] ?? self::ALLOWED_FRAME_FONTS['sans'];
|
||||
$path = self::FRAME_FONT_DIR . $file;
|
||||
|
||||
return is_file($path) ? $path : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an optional text label below the qr code. Only supported for raster
|
||||
* formats (png/jpg/jpeg/gif) via GD; a no-op for svg/svgbw/eps.
|
||||
*/
|
||||
private function addFrameText($path, $format, $text) {
|
||||
private function addFrameText($path, $format, $text, $fontKey = 'sans', $fontSize = 16) {
|
||||
$text = trim((string) $text);
|
||||
$loaders = ['png' => 'imagecreatefrompng', 'jpg' => 'imagecreatefromjpeg', 'jpeg' => 'imagecreatefromjpeg', 'gif' => 'imagecreatefromgif'];
|
||||
$savers = ['png' => 'imagepng', 'jpg' => 'imagejpeg', 'jpeg' => 'imagejpeg', 'gif' => 'imagegif'];
|
||||
@@ -85,6 +103,9 @@ class Qrcode {
|
||||
return;
|
||||
}
|
||||
|
||||
$fontFile = self::resolveFrameFont($fontKey);
|
||||
$fontSize = min(max((int) $fontSize, 8), 60);
|
||||
|
||||
$source = @$loaders[$format]($path);
|
||||
if ($source === false) {
|
||||
return;
|
||||
@@ -92,7 +113,7 @@ class Qrcode {
|
||||
|
||||
$width = imagesx($source);
|
||||
$height = imagesy($source);
|
||||
$padding = 30;
|
||||
$padding = $fontFile !== null ? $fontSize + 20 : 30;
|
||||
|
||||
$canvas = imagecreatetruecolor($width, $height + $padding);
|
||||
$white = imagecolorallocate($canvas, 255, 255, 255);
|
||||
@@ -100,11 +121,20 @@ class Qrcode {
|
||||
imagefill($canvas, 0, 0, $white);
|
||||
imagecopy($canvas, $source, 0, 0, 0, 0, $width, $height);
|
||||
|
||||
$font = 5;
|
||||
$text_width = imagefontwidth($font) * strlen($text);
|
||||
$x = max(0, (int) (($width - $text_width) / 2));
|
||||
$y = $height + (int) (($padding - imagefontheight($font)) / 2);
|
||||
imagestring($canvas, $font, $x, $y, $text, $black);
|
||||
if ($fontFile !== null && function_exists('imagettftext')) {
|
||||
$bbox = imagettfbbox($fontSize, 0, $fontFile, $text);
|
||||
$text_width = abs($bbox[2] - $bbox[0]);
|
||||
$text_height = abs($bbox[1] - $bbox[7]);
|
||||
$x = max(0, (int) (($width - $text_width) / 2));
|
||||
$y = $height + (int) (($padding + $text_height) / 2);
|
||||
imagettftext($canvas, $fontSize, 0, $x, $y, $black, $fontFile, $text);
|
||||
} else {
|
||||
$font = 5;
|
||||
$text_width = imagefontwidth($font) * strlen($text);
|
||||
$x = max(0, (int) (($width - $text_width) / 2));
|
||||
$y = $height + (int) (($padding - imagefontheight($font)) / 2);
|
||||
imagestring($canvas, $font, $x, $y, $text, $black);
|
||||
}
|
||||
|
||||
$savers[$format]($canvas, $path);
|
||||
|
||||
@@ -112,6 +142,70 @@ class Qrcode {
|
||||
imagedestroy($canvas);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an optional user-uploaded icon above the qr code (not embedded inside
|
||||
* it, so scanability is unaffected). Only supported for raster formats via GD.
|
||||
*/
|
||||
private function addTopIcon($path, $format, $iconPath) {
|
||||
$loaders = ['png' => 'imagecreatefrompng', 'jpg' => 'imagecreatefromjpeg', 'jpeg' => 'imagecreatefromjpeg', 'gif' => 'imagecreatefromgif'];
|
||||
$savers = ['png' => 'imagepng', 'jpg' => 'imagejpeg', 'jpeg' => 'imagejpeg', 'gif' => 'imagegif'];
|
||||
|
||||
if (!$iconPath || !is_file($iconPath) || !isset($loaders[$format]) || !is_file($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$iconInfo = @getimagesize($iconPath);
|
||||
$iconLoaders = [IMAGETYPE_PNG => 'imagecreatefrompng', IMAGETYPE_JPEG => 'imagecreatefromjpeg', IMAGETYPE_GIF => 'imagecreatefromgif'];
|
||||
|
||||
if ($iconInfo === false || !isset($iconLoaders[$iconInfo[2]])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$source = @$loaders[$format]($path);
|
||||
$icon = @$iconLoaders[$iconInfo[2]]($iconPath);
|
||||
|
||||
if ($source === false || $icon === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$width = imagesx($source);
|
||||
$height = imagesy($source);
|
||||
|
||||
$iconWidth = imagesx($icon);
|
||||
$iconHeight = imagesy($icon);
|
||||
|
||||
$maxIconHeight = (int) ($height * 0.25);
|
||||
$scale = min($maxIconHeight / $iconHeight, ($width * 0.6) / $iconWidth, 1);
|
||||
$targetWidth = max(1, (int) ($iconWidth * $scale));
|
||||
$targetHeight = max(1, (int) ($iconHeight * $scale));
|
||||
|
||||
$margin = 15;
|
||||
$topPadding = $targetHeight + ($margin * 2);
|
||||
|
||||
$canvas = imagecreatetruecolor($width, $height + $topPadding);
|
||||
$white = imagecolorallocate($canvas, 255, 255, 255);
|
||||
imagefill($canvas, 0, 0, $white);
|
||||
|
||||
$resizedIcon = imagecreatetruecolor($targetWidth, $targetHeight);
|
||||
imagealphablending($resizedIcon, false);
|
||||
imagesavealpha($resizedIcon, true);
|
||||
$transparent = imagecolorallocatealpha($resizedIcon, 0, 0, 0, 127);
|
||||
imagefill($resizedIcon, 0, 0, $transparent);
|
||||
imagealphablending($icon, true);
|
||||
imagecopyresampled($resizedIcon, $icon, 0, 0, 0, 0, $targetWidth, $targetHeight, $iconWidth, $iconHeight);
|
||||
|
||||
$x = (int) (($width - $targetWidth) / 2);
|
||||
imagecopy($canvas, $resizedIcon, $x, $margin, 0, 0, $targetWidth, $targetHeight);
|
||||
imagecopy($canvas, $source, 0, $topPadding, 0, 0, $width, $height);
|
||||
|
||||
$savers[$format]($canvas, $path);
|
||||
|
||||
imagedestroy($source);
|
||||
imagedestroy($icon);
|
||||
imagedestroy($resizedIcon);
|
||||
imagedestroy($canvas);
|
||||
}
|
||||
|
||||
public function getQrcode($id) {
|
||||
$db = getDbInstance();
|
||||
|
||||
@@ -397,7 +491,12 @@ class Qrcode {
|
||||
throw new \RuntimeException($e->getMessage());
|
||||
}
|
||||
|
||||
$this->addFrameText($filename, $fileExt, $input_data['frame_text'] ?? '');
|
||||
$this->addTopIcon($filename, $fileExt, $input_data['icon_tmp_path'] ?? null);
|
||||
$this->addFrameText($filename, $fileExt, $input_data['frame_text'] ?? '', $input_data['frame_font'] ?? 'sans', $input_data['frame_font_size'] ?? 16);
|
||||
|
||||
if (!empty($input_data['icon_tmp_path'])) {
|
||||
@unlink($input_data['icon_tmp_path']);
|
||||
}
|
||||
|
||||
// If you want you can customi<e qr code with logo
|
||||
//$this->addLogo($data_to_db['qrcode'], $options['optionlogo']);
|
||||
|
||||
+114
-10
@@ -57,11 +57,29 @@ class Qrcode {
|
||||
return $format;
|
||||
}
|
||||
|
||||
const FRAME_FONT_DIR = '/usr/share/fonts/truetype/dejavu/';
|
||||
|
||||
const ALLOWED_FRAME_FONTS = [
|
||||
'sans' => 'DejaVuSans.ttf',
|
||||
'sans-bold' => 'DejaVuSans-Bold.ttf',
|
||||
'serif' => 'DejaVuSerif.ttf',
|
||||
'serif-bold' => 'DejaVuSerif-Bold.ttf',
|
||||
'mono' => 'DejaVuSansMono.ttf',
|
||||
'mono-bold' => 'DejaVuSansMono-Bold.ttf',
|
||||
];
|
||||
|
||||
private static function resolveFrameFont($fontKey) {
|
||||
$file = self::ALLOWED_FRAME_FONTS[$fontKey] ?? self::ALLOWED_FRAME_FONTS['sans'];
|
||||
$path = self::FRAME_FONT_DIR . $file;
|
||||
|
||||
return is_file($path) ? $path : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an optional text label below the qr code. Only supported for raster
|
||||
* formats (png/jpg/jpeg/gif) via GD; a no-op for svg/svgbw/eps.
|
||||
*/
|
||||
private function addFrameText($path, $format, $text) {
|
||||
private function addFrameText($path, $format, $text, $fontKey = 'sans', $fontSize = 16) {
|
||||
$text = trim((string) $text);
|
||||
$loaders = ['png' => 'imagecreatefrompng', 'jpg' => 'imagecreatefromjpeg', 'jpeg' => 'imagecreatefromjpeg', 'gif' => 'imagecreatefromgif'];
|
||||
$savers = ['png' => 'imagepng', 'jpg' => 'imagejpeg', 'jpeg' => 'imagejpeg', 'gif' => 'imagegif'];
|
||||
@@ -70,6 +88,9 @@ class Qrcode {
|
||||
return;
|
||||
}
|
||||
|
||||
$fontFile = self::resolveFrameFont($fontKey);
|
||||
$fontSize = min(max((int) $fontSize, 8), 60);
|
||||
|
||||
$source = @$loaders[$format]($path);
|
||||
if ($source === false) {
|
||||
return;
|
||||
@@ -77,7 +98,7 @@ class Qrcode {
|
||||
|
||||
$width = imagesx($source);
|
||||
$height = imagesy($source);
|
||||
$padding = 30;
|
||||
$padding = $fontFile !== null ? $fontSize + 20 : 30;
|
||||
|
||||
$canvas = imagecreatetruecolor($width, $height + $padding);
|
||||
$white = imagecolorallocate($canvas, 255, 255, 255);
|
||||
@@ -85,11 +106,20 @@ class Qrcode {
|
||||
imagefill($canvas, 0, 0, $white);
|
||||
imagecopy($canvas, $source, 0, 0, 0, 0, $width, $height);
|
||||
|
||||
$font = 5;
|
||||
$text_width = imagefontwidth($font) * strlen($text);
|
||||
$x = max(0, (int) (($width - $text_width) / 2));
|
||||
$y = $height + (int) (($padding - imagefontheight($font)) / 2);
|
||||
imagestring($canvas, $font, $x, $y, $text, $black);
|
||||
if ($fontFile !== null && function_exists('imagettftext')) {
|
||||
$bbox = imagettfbbox($fontSize, 0, $fontFile, $text);
|
||||
$text_width = abs($bbox[2] - $bbox[0]);
|
||||
$text_height = abs($bbox[1] - $bbox[7]);
|
||||
$x = max(0, (int) (($width - $text_width) / 2));
|
||||
$y = $height + (int) (($padding + $text_height) / 2);
|
||||
imagettftext($canvas, $fontSize, 0, $x, $y, $black, $fontFile, $text);
|
||||
} else {
|
||||
$font = 5;
|
||||
$text_width = imagefontwidth($font) * strlen($text);
|
||||
$x = max(0, (int) (($width - $text_width) / 2));
|
||||
$y = $height + (int) (($padding - imagefontheight($font)) / 2);
|
||||
imagestring($canvas, $font, $x, $y, $text, $black);
|
||||
}
|
||||
|
||||
$savers[$format]($canvas, $path);
|
||||
|
||||
@@ -97,6 +127,70 @@ class Qrcode {
|
||||
imagedestroy($canvas);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an optional user-uploaded icon above the qr code (not embedded inside
|
||||
* it, so scanability is unaffected). Only supported for raster formats via GD.
|
||||
*/
|
||||
private function addTopIcon($path, $format, $iconPath) {
|
||||
$loaders = ['png' => 'imagecreatefrompng', 'jpg' => 'imagecreatefromjpeg', 'jpeg' => 'imagecreatefromjpeg', 'gif' => 'imagecreatefromgif'];
|
||||
$savers = ['png' => 'imagepng', 'jpg' => 'imagejpeg', 'jpeg' => 'imagejpeg', 'gif' => 'imagegif'];
|
||||
|
||||
if (!$iconPath || !is_file($iconPath) || !isset($loaders[$format]) || !is_file($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$iconInfo = @getimagesize($iconPath);
|
||||
$iconLoaders = [IMAGETYPE_PNG => 'imagecreatefrompng', IMAGETYPE_JPEG => 'imagecreatefromjpeg', IMAGETYPE_GIF => 'imagecreatefromgif'];
|
||||
|
||||
if ($iconInfo === false || !isset($iconLoaders[$iconInfo[2]])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$source = @$loaders[$format]($path);
|
||||
$icon = @$iconLoaders[$iconInfo[2]]($iconPath);
|
||||
|
||||
if ($source === false || $icon === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$width = imagesx($source);
|
||||
$height = imagesy($source);
|
||||
|
||||
$iconWidth = imagesx($icon);
|
||||
$iconHeight = imagesy($icon);
|
||||
|
||||
$maxIconHeight = (int) ($height * 0.25);
|
||||
$scale = min($maxIconHeight / $iconHeight, ($width * 0.6) / $iconWidth, 1);
|
||||
$targetWidth = max(1, (int) ($iconWidth * $scale));
|
||||
$targetHeight = max(1, (int) ($iconHeight * $scale));
|
||||
|
||||
$margin = 15;
|
||||
$topPadding = $targetHeight + ($margin * 2);
|
||||
|
||||
$canvas = imagecreatetruecolor($width, $height + $topPadding);
|
||||
$white = imagecolorallocate($canvas, 255, 255, 255);
|
||||
imagefill($canvas, 0, 0, $white);
|
||||
|
||||
$resizedIcon = imagecreatetruecolor($targetWidth, $targetHeight);
|
||||
imagealphablending($resizedIcon, false);
|
||||
imagesavealpha($resizedIcon, true);
|
||||
$transparent = imagecolorallocatealpha($resizedIcon, 0, 0, 0, 127);
|
||||
imagefill($resizedIcon, 0, 0, $transparent);
|
||||
imagealphablending($icon, true);
|
||||
imagecopyresampled($resizedIcon, $icon, 0, 0, 0, 0, $targetWidth, $targetHeight, $iconWidth, $iconHeight);
|
||||
|
||||
$x = (int) (($width - $targetWidth) / 2);
|
||||
imagecopy($canvas, $resizedIcon, $x, $margin, 0, 0, $targetWidth, $targetHeight);
|
||||
imagecopy($canvas, $source, 0, $topPadding, 0, 0, $width, $height);
|
||||
|
||||
$savers[$format]($canvas, $path);
|
||||
|
||||
imagedestroy($source);
|
||||
imagedestroy($icon);
|
||||
imagedestroy($resizedIcon);
|
||||
imagedestroy($canvas);
|
||||
}
|
||||
|
||||
public function getQrcode($id) {
|
||||
$db = getDbInstance();
|
||||
|
||||
@@ -108,7 +202,7 @@ class Qrcode {
|
||||
else
|
||||
$this->failure("Qrcode not found");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set option for qr code like:
|
||||
* Error Correction Level, size (default = 100), foreground, background
|
||||
@@ -173,7 +267,12 @@ class Qrcode {
|
||||
$this->failure($e->getMessage());
|
||||
}
|
||||
|
||||
$this->addFrameText($filename, $data_to_db['format'], $input_data['frame_text'] ?? '');
|
||||
$this->addTopIcon($filename, $data_to_db['format'], $input_data['icon_tmp_path'] ?? null);
|
||||
$this->addFrameText($filename, $data_to_db['format'], $input_data['frame_text'] ?? '', $input_data['frame_font'] ?? 'sans', $input_data['frame_font_size'] ?? 16);
|
||||
|
||||
if (!empty($input_data['icon_tmp_path'])) {
|
||||
@unlink($input_data['icon_tmp_path']);
|
||||
}
|
||||
|
||||
// If you want you can customi<e qr code with logo
|
||||
//$this->addLogo($data_to_db['qrcode'], $options['optionlogo']);
|
||||
@@ -245,7 +344,12 @@ class Qrcode {
|
||||
return ['ok' => false, 'error' => 'Could not write the qr code file.'];
|
||||
}
|
||||
|
||||
$this->addFrameText($path, $format, $input_data['frame_text'] ?? '');
|
||||
$this->addTopIcon($path, $format, $input_data['icon_tmp_path'] ?? null);
|
||||
$this->addFrameText($path, $format, $input_data['frame_text'] ?? '', $input_data['frame_font'] ?? 'sans', $input_data['frame_font_size'] ?? 16);
|
||||
|
||||
if (!empty($input_data['icon_tmp_path'])) {
|
||||
@unlink($input_data['icon_tmp_path']);
|
||||
}
|
||||
|
||||
$db = getDbInstance();
|
||||
$last_id = $db->insert($this->table, $data_to_db);
|
||||
|
||||
@@ -330,21 +330,48 @@ class StaticQrcode {
|
||||
/**
|
||||
* create a qr code of type "bitcoin"
|
||||
* @string address -> required
|
||||
* @int amount -> required
|
||||
* @string amount -> optional (a bitcoin address is useful as a standing QR code,
|
||||
* not just for one specific payment amount)
|
||||
* @string label
|
||||
* @string message
|
||||
*/
|
||||
public function bitcoinQrcode($address, $amount, $label, $message)
|
||||
{
|
||||
if($address != NULL && $amount != NULL){
|
||||
$this->sData = 'bitcoin:'.$address.'?amount='.$amount.'&label='.$label.'&message='.$message;
|
||||
$this->sContent = '<strong>BTC address:</strong> '.$address.'<br>'.'<strong>Amount:</strong> '.$amount.'<br>';
|
||||
$this->sContent .= '<strong>Label:</strong> '.$label.'<br>'.'<strong>Message:</strong> '.$message;
|
||||
|
||||
$this->addQrcode("bitcoin");
|
||||
}
|
||||
else
|
||||
$address = trim((string) $address);
|
||||
$amount = trim((string) $amount);
|
||||
$label = trim((string) $label);
|
||||
$message = trim((string) $message);
|
||||
|
||||
if ($address === '') {
|
||||
$this->requiredFieldsError();
|
||||
return;
|
||||
}
|
||||
|
||||
$params = [];
|
||||
if ($amount !== '') {
|
||||
$params[] = 'amount=' . rawurlencode($amount);
|
||||
}
|
||||
if ($label !== '') {
|
||||
$params[] = 'label=' . rawurlencode($label);
|
||||
}
|
||||
if ($message !== '') {
|
||||
$params[] = 'message=' . rawurlencode($message);
|
||||
}
|
||||
|
||||
$this->sData = 'bitcoin:' . $address . ($params ? '?' . implode('&', $params) : '');
|
||||
|
||||
$this->sContent = '<strong>BTC address:</strong> ' . $address . '<br>';
|
||||
if ($amount !== '') {
|
||||
$this->sContent .= '<strong>Amount:</strong> ' . $amount . '<br>';
|
||||
}
|
||||
if ($label !== '') {
|
||||
$this->sContent .= '<strong>Label:</strong> ' . $label . '<br>';
|
||||
}
|
||||
if ($message !== '') {
|
||||
$this->sContent .= '<strong>Message:</strong> ' . $message;
|
||||
}
|
||||
|
||||
$this->addQrcode("bitcoin");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -473,6 +500,9 @@ class StaticQrcode {
|
||||
$input_data["foreground"] = $_POST['foreground'];
|
||||
$input_data["background"] = $_POST['background'];
|
||||
$input_data["frame_text"] = $_POST['frame_text'] ?? '';
|
||||
$input_data["frame_font"] = $_POST['frame_font'] ?? 'sans';
|
||||
$input_data["frame_font_size"] = $_POST['frame_font_size'] ?? 16;
|
||||
$input_data["icon_tmp_path"] = $_POST['icon_tmp_path'] ?? null;
|
||||
|
||||
$data_to_qrcode = urlencode($this->sData);
|
||||
|
||||
|
||||
@@ -34,7 +34,15 @@ if($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["edit"])) {
|
||||
$static_qrcode_instance->editQrcode($_POST);
|
||||
}
|
||||
|
||||
if($_SERVER["REQUEST_METHOD"] === "POST" && !isset($_POST["edit"])) {
|
||||
if($_SERVER["REQUEST_METHOD"] === "POST" && !isset($_POST["edit"]) && !isset($_POST["del_id"])) {
|
||||
$icon_upload = qr_handle_icon_upload('icon');
|
||||
if (!$icon_upload['ok']) {
|
||||
$_SESSION['failure'] = $icon_upload['error'];
|
||||
header('Location: ' . basename(__FILE__) . '?type=' . urlencode($_GET['type'] ?? ''));
|
||||
exit;
|
||||
}
|
||||
$_POST['icon_tmp_path'] = $icon_upload['path'];
|
||||
|
||||
switch($_GET['type']){
|
||||
case 'text': $static_qrcode_instance->textQrcode($_POST['text']);
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user