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.
This commit is contained in:
Vendored
+118
-38
@@ -1,12 +1,23 @@
|
|||||||
// Preset system + random style button for the qr code generation forms (Fase 3, priority 2).
|
// 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 () {
|
||||||
function csrfToken() {
|
function csrfToken() {
|
||||||
var meta = document.querySelector('meta[name="csrf-token"]');
|
var meta = document.querySelector('meta[name="csrf-token"]');
|
||||||
return meta ? meta.getAttribute('content') : '';
|
return meta ? meta.getAttribute('content') : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function setColor(id, hex) {
|
function scopeOf(el) {
|
||||||
var input = document.getElementById(id);
|
return el.closest('.tab-pane') || el.closest('form') || document;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setColor(scope, id, hex) {
|
||||||
|
var input = scope.querySelector('#' + id);
|
||||||
if (!input) {
|
if (!input) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -27,12 +38,35 @@
|
|||||||
return '#' + ('000000' + value).slice(-6);
|
return '#' + ('000000' + value).slice(-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadPresets() {
|
function updateStylePreview(scope) {
|
||||||
var select = document.getElementById('preset_select');
|
var swatch = scope.querySelector('#style_preview_swatch');
|
||||||
if (!select) {
|
var text = scope.querySelector('#style_preview_text');
|
||||||
|
if (!swatch || !text) {
|
||||||
return;
|
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')
|
fetch('presets.php?action=list')
|
||||||
.then(function (response) { return response.json(); })
|
.then(function (response) { return response.json(); })
|
||||||
.then(function (json) {
|
.then(function (json) {
|
||||||
@@ -54,43 +88,69 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
loadPresets();
|
document.querySelectorAll('#preset_select').forEach(loadPresetsInto);
|
||||||
|
|
||||||
var randomBtn = document.getElementById('random_style_btn');
|
document.querySelectorAll('#style_preview').forEach(function (row) {
|
||||||
if (randomBtn) {
|
updateStylePreview(scopeOf(row));
|
||||||
randomBtn.addEventListener('click', function () {
|
|
||||||
setColor('foreground', randomHexColor());
|
|
||||||
setColor('background', randomHexColor());
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
var presetSelect = document.getElementById('preset_select');
|
document.querySelectorAll('#foreground, #background').forEach(function (input) {
|
||||||
if (presetSelect) {
|
input.addEventListener('change', function () {
|
||||||
|
updateStylePreview(scopeOf(input));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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 () {
|
presetSelect.addEventListener('change', function () {
|
||||||
var option = presetSelect.options[presetSelect.selectedIndex];
|
var option = presetSelect.options[presetSelect.selectedIndex];
|
||||||
if (!option.value) {
|
if (!option.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setColor('foreground', option.dataset.foreground);
|
var scope = scopeOf(presetSelect);
|
||||||
setColor('background', option.dataset.background);
|
|
||||||
|
|
||||||
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) {
|
if (levelSelect) {
|
||||||
levelSelect.value = option.dataset.level;
|
levelSelect.value = option.dataset.level;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sizeSelect = document.getElementById('size');
|
var sizeSelect = scope.querySelector('#size');
|
||||||
if (sizeSelect) {
|
if (sizeSelect) {
|
||||||
sizeSelect.value = option.dataset.size;
|
sizeSelect.value = option.dataset.size;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var saveBtn = document.getElementById('preset_save_btn');
|
updateStylePreview(scope);
|
||||||
if (saveBtn) {
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('#preset_save_btn').forEach(function (saveBtn) {
|
||||||
saveBtn.addEventListener('click', function () {
|
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();
|
var name = nameInput.value.trim();
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
@@ -98,13 +158,18 @@
|
|||||||
return;
|
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();
|
var body = new URLSearchParams();
|
||||||
body.set('action', 'save');
|
body.set('action', 'save');
|
||||||
body.set('name', name);
|
body.set('name', name);
|
||||||
body.set('foreground', document.getElementById('foreground').value);
|
body.set('foreground', foreground);
|
||||||
body.set('background', document.getElementById('background').value);
|
body.set('background', background);
|
||||||
body.set('level', document.querySelector('select[name="level"]').value);
|
body.set('level', level);
|
||||||
body.set('size', document.getElementById('size').value);
|
body.set('size', size);
|
||||||
|
|
||||||
fetch('presets.php', {
|
fetch('presets.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -118,24 +183,33 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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');
|
var option = document.createElement('option');
|
||||||
option.value = json.data.id;
|
option.value = json.data.id;
|
||||||
option.textContent = json.data.name;
|
option.textContent = json.data.name;
|
||||||
option.dataset.foreground = document.getElementById('foreground').value;
|
option.dataset.foreground = foreground;
|
||||||
option.dataset.background = document.getElementById('background').value;
|
option.dataset.background = background;
|
||||||
option.dataset.level = document.querySelector('select[name="level"]').value;
|
option.dataset.level = level;
|
||||||
option.dataset.size = document.getElementById('size').value;
|
option.dataset.size = size;
|
||||||
presetSelect.appendChild(option);
|
select.appendChild(option);
|
||||||
presetSelect.value = option.value;
|
|
||||||
|
if (select === scope.querySelector('#preset_select')) {
|
||||||
|
select.value = option.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
nameInput.value = '';
|
nameInput.value = '';
|
||||||
})
|
})
|
||||||
.catch(function () { alert('Could not save preset (network error).'); });
|
.catch(function () { alert('Could not save preset (network error).'); });
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
|
||||||
var deleteBtn = document.getElementById('preset_delete_btn');
|
document.querySelectorAll('#preset_delete_btn').forEach(function (deleteBtn) {
|
||||||
if (deleteBtn) {
|
|
||||||
deleteBtn.addEventListener('click', function () {
|
deleteBtn.addEventListener('click', function () {
|
||||||
|
var scope = scopeOf(deleteBtn);
|
||||||
|
var presetSelect = scope.querySelector('#preset_select');
|
||||||
var option = presetSelect.options[presetSelect.selectedIndex];
|
var option = presetSelect.options[presetSelect.selectedIndex];
|
||||||
if (!option.value) {
|
if (!option.value) {
|
||||||
return;
|
return;
|
||||||
@@ -157,13 +231,19 @@
|
|||||||
.then(function (response) { return response.json(); })
|
.then(function (response) { return response.json(); })
|
||||||
.then(function (json) {
|
.then(function (json) {
|
||||||
if (json.status === 200) {
|
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 {
|
} else {
|
||||||
alert('Could not delete preset: ' + json.data);
|
alert('Could not delete preset: ' + json.data);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function () { alert('Could not delete preset (network error).'); });
|
.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);
|
$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(
|
if(
|
||||||
isset($_POST["foreground"]) &&
|
isset($_POST["foreground"]) &&
|
||||||
isset($_POST["background"]) &&
|
isset($_POST["background"]) &&
|
||||||
@@ -45,9 +45,18 @@ if($_SERVER["REQUEST_METHOD"] === "POST" && !isset($_POST["edit"])) {
|
|||||||
isset($_POST["filename"]) &&
|
isset($_POST["filename"]) &&
|
||||||
isset($_POST["format"]) &&
|
isset($_POST["format"]) &&
|
||||||
isset($_POST["id_owner"])
|
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);
|
$dynamic_qrcode_instance->addQrcode($_POST);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
<div class="col-sm-12 mb-2">
|
<div class="col-sm-12 mb-2">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6 col-md-3">
|
<div class="col-6 col-md-3">
|
||||||
|
<label> </label>
|
||||||
<button type="button" id="random_style_btn" class="btn btn-outline-secondary btn-block">
|
<button type="button" id="random_style_btn" class="btn btn-outline-secondary btn-block">
|
||||||
<i class="fa fa-dice"></i> Random style
|
<i class="fa fa-dice"></i> Random style
|
||||||
</button>
|
</button>
|
||||||
@@ -80,6 +81,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
</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>
|
<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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -146,7 +184,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="id_owner">Owner *</label>
|
<label for="id_owner">Owner *</label>
|
||||||
<select name="id_owner" class="form-control">
|
<select name="id_owner" class="form-control">
|
||||||
<option value="" selected>All</option>
|
<option value="">All (shared with every admin)</option>
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
require_once BASE_PATH . '/lib/Users/Users.php';
|
require_once BASE_PATH . '/lib/Users/Users.php';
|
||||||
@@ -154,8 +192,9 @@
|
|||||||
$users = $users_instance->getAllUsers();
|
$users = $users_instance->getAllUsers();
|
||||||
|
|
||||||
foreach ($users as $user) {
|
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 } ?>
|
<?php } ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -105,4 +105,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div><!-- /.card -->
|
</div><!-- /.card -->
|
||||||
|
|
||||||
|
<script src="dist/js/qrcode-style-tools.js?nocache=<?php print rand();?>"></script>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -58,6 +58,7 @@ if (QRCODE_GENERATOR === "internal-chillerlan.qrcode") {
|
|||||||
<div class="col-sm-12 mb-2">
|
<div class="col-sm-12 mb-2">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6 col-md-3">
|
<div class="col-6 col-md-3">
|
||||||
|
<label> </label>
|
||||||
<button type="button" id="random_style_btn" class="btn btn-outline-secondary btn-block">
|
<button type="button" id="random_style_btn" class="btn btn-outline-secondary btn-block">
|
||||||
<i class="fa fa-dice"></i> Random style
|
<i class="fa fa-dice"></i> Random style
|
||||||
</button>
|
</button>
|
||||||
@@ -84,10 +85,25 @@ if (QRCODE_GENERATOR === "internal-chillerlan.qrcode") {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
</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
|
<!-- Its use is not recommended. Read the documentation
|
||||||
<div class="form-group">
|
<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>
|
<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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -140,7 +185,7 @@ if (QRCODE_GENERATOR === "internal-chillerlan.qrcode") {
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="id_owner">Owner *</label>
|
<label for="id_owner">Owner *</label>
|
||||||
<select name="id_owner" class="form-control">
|
<select name="id_owner" class="form-control">
|
||||||
<option value="" selected>All</option>
|
<option value="">All (shared with every admin)</option>
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
require_once BASE_PATH . '/lib/Users/Users.php';
|
require_once BASE_PATH . '/lib/Users/Users.php';
|
||||||
@@ -148,8 +193,9 @@ if (QRCODE_GENERATOR === "internal-chillerlan.qrcode") {
|
|||||||
$users = $users_instance->getAllUsers();
|
$users = $users_instance->getAllUsers();
|
||||||
|
|
||||||
foreach ($users as $user) {
|
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 } ?>
|
<?php } ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<div class="col-6 col-md-3">
|
<div class="col-6 col-md-3">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Amount *</label>
|
<label>Amount</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="number" name="amount" value="" placeholder="" class="form-control" step="0.0001">
|
<input type="number" name="amount" value="" placeholder="" class="form-control" step="0.0001">
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
<label>Encryption *</label>
|
<label>Encryption *</label>
|
||||||
<select name="encryption" class="form-control">
|
<select name="encryption" class="form-control">
|
||||||
<option value="WPA" Selected>WPA/WPA2</option>
|
<option value="WPA" Selected>WPA/WPA2</option>
|
||||||
|
<option value="WPA3">WPA3</option>
|
||||||
<option value="WEP">WEP</option>
|
<option value="WEP">WEP</option>
|
||||||
<option value="">None</option>
|
<option value="">None</option>
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -117,6 +117,45 @@ function paginationLinks($current_page, $total_pages, $base_url) {
|
|||||||
return $html;
|
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() {
|
function base_url() {
|
||||||
require_once(__DIR__ . '/../config/environment.php');
|
require_once(__DIR__ . '/../config/environment.php');
|
||||||
if (defined('BASE_URL') && BASE_URL !== null) {
|
if (defined('BASE_URL') && BASE_URL !== null) {
|
||||||
|
|||||||
@@ -72,11 +72,29 @@ class Qrcode {
|
|||||||
return $format;
|
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
|
* 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.
|
* 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);
|
$text = trim((string) $text);
|
||||||
$loaders = ['png' => 'imagecreatefrompng', 'jpg' => 'imagecreatefromjpeg', 'jpeg' => 'imagecreatefromjpeg', 'gif' => 'imagecreatefromgif'];
|
$loaders = ['png' => 'imagecreatefrompng', 'jpg' => 'imagecreatefromjpeg', 'jpeg' => 'imagecreatefromjpeg', 'gif' => 'imagecreatefromgif'];
|
||||||
$savers = ['png' => 'imagepng', 'jpg' => 'imagejpeg', 'jpeg' => 'imagejpeg', 'gif' => 'imagegif'];
|
$savers = ['png' => 'imagepng', 'jpg' => 'imagejpeg', 'jpeg' => 'imagejpeg', 'gif' => 'imagegif'];
|
||||||
@@ -85,6 +103,9 @@ class Qrcode {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$fontFile = self::resolveFrameFont($fontKey);
|
||||||
|
$fontSize = min(max((int) $fontSize, 8), 60);
|
||||||
|
|
||||||
$source = @$loaders[$format]($path);
|
$source = @$loaders[$format]($path);
|
||||||
if ($source === false) {
|
if ($source === false) {
|
||||||
return;
|
return;
|
||||||
@@ -92,7 +113,7 @@ class Qrcode {
|
|||||||
|
|
||||||
$width = imagesx($source);
|
$width = imagesx($source);
|
||||||
$height = imagesy($source);
|
$height = imagesy($source);
|
||||||
$padding = 30;
|
$padding = $fontFile !== null ? $fontSize + 20 : 30;
|
||||||
|
|
||||||
$canvas = imagecreatetruecolor($width, $height + $padding);
|
$canvas = imagecreatetruecolor($width, $height + $padding);
|
||||||
$white = imagecolorallocate($canvas, 255, 255, 255);
|
$white = imagecolorallocate($canvas, 255, 255, 255);
|
||||||
@@ -100,11 +121,20 @@ class Qrcode {
|
|||||||
imagefill($canvas, 0, 0, $white);
|
imagefill($canvas, 0, 0, $white);
|
||||||
imagecopy($canvas, $source, 0, 0, 0, 0, $width, $height);
|
imagecopy($canvas, $source, 0, 0, 0, 0, $width, $height);
|
||||||
|
|
||||||
|
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;
|
$font = 5;
|
||||||
$text_width = imagefontwidth($font) * strlen($text);
|
$text_width = imagefontwidth($font) * strlen($text);
|
||||||
$x = max(0, (int) (($width - $text_width) / 2));
|
$x = max(0, (int) (($width - $text_width) / 2));
|
||||||
$y = $height + (int) (($padding - imagefontheight($font)) / 2);
|
$y = $height + (int) (($padding - imagefontheight($font)) / 2);
|
||||||
imagestring($canvas, $font, $x, $y, $text, $black);
|
imagestring($canvas, $font, $x, $y, $text, $black);
|
||||||
|
}
|
||||||
|
|
||||||
$savers[$format]($canvas, $path);
|
$savers[$format]($canvas, $path);
|
||||||
|
|
||||||
@@ -112,6 +142,70 @@ class Qrcode {
|
|||||||
imagedestroy($canvas);
|
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) {
|
public function getQrcode($id) {
|
||||||
$db = getDbInstance();
|
$db = getDbInstance();
|
||||||
|
|
||||||
@@ -397,7 +491,12 @@ class Qrcode {
|
|||||||
throw new \RuntimeException($e->getMessage());
|
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
|
// If you want you can customi<e qr code with logo
|
||||||
//$this->addLogo($data_to_db['qrcode'], $options['optionlogo']);
|
//$this->addLogo($data_to_db['qrcode'], $options['optionlogo']);
|
||||||
|
|||||||
+108
-4
@@ -57,11 +57,29 @@ class Qrcode {
|
|||||||
return $format;
|
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
|
* 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.
|
* 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);
|
$text = trim((string) $text);
|
||||||
$loaders = ['png' => 'imagecreatefrompng', 'jpg' => 'imagecreatefromjpeg', 'jpeg' => 'imagecreatefromjpeg', 'gif' => 'imagecreatefromgif'];
|
$loaders = ['png' => 'imagecreatefrompng', 'jpg' => 'imagecreatefromjpeg', 'jpeg' => 'imagecreatefromjpeg', 'gif' => 'imagecreatefromgif'];
|
||||||
$savers = ['png' => 'imagepng', 'jpg' => 'imagejpeg', 'jpeg' => 'imagejpeg', 'gif' => 'imagegif'];
|
$savers = ['png' => 'imagepng', 'jpg' => 'imagejpeg', 'jpeg' => 'imagejpeg', 'gif' => 'imagegif'];
|
||||||
@@ -70,6 +88,9 @@ class Qrcode {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$fontFile = self::resolveFrameFont($fontKey);
|
||||||
|
$fontSize = min(max((int) $fontSize, 8), 60);
|
||||||
|
|
||||||
$source = @$loaders[$format]($path);
|
$source = @$loaders[$format]($path);
|
||||||
if ($source === false) {
|
if ($source === false) {
|
||||||
return;
|
return;
|
||||||
@@ -77,7 +98,7 @@ class Qrcode {
|
|||||||
|
|
||||||
$width = imagesx($source);
|
$width = imagesx($source);
|
||||||
$height = imagesy($source);
|
$height = imagesy($source);
|
||||||
$padding = 30;
|
$padding = $fontFile !== null ? $fontSize + 20 : 30;
|
||||||
|
|
||||||
$canvas = imagecreatetruecolor($width, $height + $padding);
|
$canvas = imagecreatetruecolor($width, $height + $padding);
|
||||||
$white = imagecolorallocate($canvas, 255, 255, 255);
|
$white = imagecolorallocate($canvas, 255, 255, 255);
|
||||||
@@ -85,11 +106,20 @@ class Qrcode {
|
|||||||
imagefill($canvas, 0, 0, $white);
|
imagefill($canvas, 0, 0, $white);
|
||||||
imagecopy($canvas, $source, 0, 0, 0, 0, $width, $height);
|
imagecopy($canvas, $source, 0, 0, 0, 0, $width, $height);
|
||||||
|
|
||||||
|
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;
|
$font = 5;
|
||||||
$text_width = imagefontwidth($font) * strlen($text);
|
$text_width = imagefontwidth($font) * strlen($text);
|
||||||
$x = max(0, (int) (($width - $text_width) / 2));
|
$x = max(0, (int) (($width - $text_width) / 2));
|
||||||
$y = $height + (int) (($padding - imagefontheight($font)) / 2);
|
$y = $height + (int) (($padding - imagefontheight($font)) / 2);
|
||||||
imagestring($canvas, $font, $x, $y, $text, $black);
|
imagestring($canvas, $font, $x, $y, $text, $black);
|
||||||
|
}
|
||||||
|
|
||||||
$savers[$format]($canvas, $path);
|
$savers[$format]($canvas, $path);
|
||||||
|
|
||||||
@@ -97,6 +127,70 @@ class Qrcode {
|
|||||||
imagedestroy($canvas);
|
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) {
|
public function getQrcode($id) {
|
||||||
$db = getDbInstance();
|
$db = getDbInstance();
|
||||||
|
|
||||||
@@ -173,7 +267,12 @@ class Qrcode {
|
|||||||
$this->failure($e->getMessage());
|
$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
|
// If you want you can customi<e qr code with logo
|
||||||
//$this->addLogo($data_to_db['qrcode'], $options['optionlogo']);
|
//$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.'];
|
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();
|
$db = getDbInstance();
|
||||||
$last_id = $db->insert($this->table, $data_to_db);
|
$last_id = $db->insert($this->table, $data_to_db);
|
||||||
|
|||||||
@@ -330,22 +330,49 @@ class StaticQrcode {
|
|||||||
/**
|
/**
|
||||||
* create a qr code of type "bitcoin"
|
* create a qr code of type "bitcoin"
|
||||||
* @string address -> required
|
* @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 label
|
||||||
* @string message
|
* @string message
|
||||||
*/
|
*/
|
||||||
public function bitcoinQrcode($address, $amount, $label, $message)
|
public function bitcoinQrcode($address, $amount, $label, $message)
|
||||||
{
|
{
|
||||||
if($address != NULL && $amount != NULL){
|
$address = trim((string) $address);
|
||||||
$this->sData = 'bitcoin:'.$address.'?amount='.$amount.'&label='.$label.'&message='.$message;
|
$amount = trim((string) $amount);
|
||||||
$this->sContent = '<strong>BTC address:</strong> '.$address.'<br>'.'<strong>Amount:</strong> '.$amount.'<br>';
|
$label = trim((string) $label);
|
||||||
$this->sContent .= '<strong>Label:</strong> '.$label.'<br>'.'<strong>Message:</strong> '.$message;
|
$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");
|
$this->addQrcode("bitcoin");
|
||||||
}
|
}
|
||||||
else
|
|
||||||
$this->requiredFieldsError();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* create a qr code of type "2FA"
|
* create a qr code of type "2FA"
|
||||||
@@ -473,6 +500,9 @@ class StaticQrcode {
|
|||||||
$input_data["foreground"] = $_POST['foreground'];
|
$input_data["foreground"] = $_POST['foreground'];
|
||||||
$input_data["background"] = $_POST['background'];
|
$input_data["background"] = $_POST['background'];
|
||||||
$input_data["frame_text"] = $_POST['frame_text'] ?? '';
|
$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);
|
$data_to_qrcode = urlencode($this->sData);
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,15 @@ if($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["edit"])) {
|
|||||||
$static_qrcode_instance->editQrcode($_POST);
|
$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']){
|
switch($_GET['type']){
|
||||||
case 'text': $static_qrcode_instance->textQrcode($_POST['text']);
|
case 'text': $static_qrcode_instance->textQrcode($_POST['text']);
|
||||||
break;
|
break;
|
||||||
|
|||||||
Reference in New Issue
Block a user