Add embedding via 404 page
This commit is contained in:
5 files changed
+201
-92
No files matched your search
@@ -0,0 +1,88 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<!--
|
||||||
|
Embed endpoint for OnlyQR.
|
||||||
|
|
||||||
|
Static hosts (GitHub/Gitea/Codeberg Pages) serve this file for every unknown path,
|
||||||
|
so https://onlyqr.0n8.de/<text> renders a QR code for <text> and nothing else.
|
||||||
|
Meant to be embedded via <iframe>. (Note: the HTTP status is 404, which iframes ignore.)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
/<text> QR code for <text> (percent-decoded)
|
||||||
|
/<text>?color=ff0000 custom colour (hex without '#', or any CSS colour name)
|
||||||
|
|
||||||
|
Recommended: build the path with encodeURIComponent(text) so that '?', '#', '%' and
|
||||||
|
'/' inside the text survive unchanged. Unknown query parameters are kept as part of
|
||||||
|
the text, so /https://example.com/page?id=5&color=ff0000 encodes
|
||||||
|
"https://example.com/page?id=5" in red.
|
||||||
|
-->
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<meta name="robots" content="noindex" />
|
||||||
|
<title>QR Code</title>
|
||||||
|
<style>
|
||||||
|
html, body { margin: 0; padding: 0; width: 100%; height: 100%; background: transparent; overflow: hidden; }
|
||||||
|
#qrcode { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
|
||||||
|
#qrcode svg { width: 100%; height: 100%; display: block; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="qrcode"></div>
|
||||||
|
|
||||||
|
<!-- Absolute paths: this page is served under arbitrary (possibly nested) paths. -->
|
||||||
|
<script src="/qrcode.js"></script>
|
||||||
|
<script src="/qr-svg.js"></script>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var DEFAULT_COLOR = '#004c93';
|
||||||
|
|
||||||
|
function safeDecode(s) {
|
||||||
|
try { return decodeURIComponent(s); } catch (e) { return s; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseColor(value) {
|
||||||
|
if (!value) return DEFAULT_COLOR;
|
||||||
|
value = safeDecode(value).trim();
|
||||||
|
if (/^[0-9a-f]{3}([0-9a-f]{3})?([0-9a-f]{2})?$/i.test(value)) value = '#' + value;
|
||||||
|
return (window.CSS && CSS.supports('color', value)) ? value : DEFAULT_COLOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split the query string: "color" is ours, everything else belongs to the text.
|
||||||
|
var color = null;
|
||||||
|
var rest = [];
|
||||||
|
location.search.replace(/^\?/, '').split('&').forEach(function(part) {
|
||||||
|
if (!part) return;
|
||||||
|
if (/^color=/i.test(part)) color = part.slice(6);
|
||||||
|
else rest.push(part);
|
||||||
|
});
|
||||||
|
|
||||||
|
var raw = location.pathname.replace(/^\//, '') +
|
||||||
|
(rest.length ? '?' + rest.join('&') : '') +
|
||||||
|
location.hash;
|
||||||
|
var text = safeDecode(raw);
|
||||||
|
|
||||||
|
if (!text) return;
|
||||||
|
document.title = text;
|
||||||
|
|
||||||
|
var el = document.getElementById('qrcode');
|
||||||
|
new QRCode(el, { text: text, colorDark: parseColor(color) });
|
||||||
|
|
||||||
|
var svg = el.querySelector('svg');
|
||||||
|
if (svg) {
|
||||||
|
svg = optimizeQRSVG(svg);
|
||||||
|
// Let the SVG scale to the iframe instead of using fixed pixel dimensions.
|
||||||
|
svg.removeAttribute('width');
|
||||||
|
svg.removeAttribute('height');
|
||||||
|
svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
|
||||||
|
svg.setAttribute('shape-rendering', 'crispEdges');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -4,6 +4,20 @@ A simplified, SVG-only QR code generator based on QRCode.js. This lightweight li
|
|||||||
|
|
||||||
Try it out at [OnlyQR Demo](https://onlyqr.0n8.de).
|
Try it out at [OnlyQR Demo](https://onlyqr.0n8.de).
|
||||||
|
|
||||||
|
## Embedding via URL
|
||||||
|
|
||||||
|
Everything after the domain becomes the QR code content, and the page shows only the QR code, scaled to fit:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<iframe src="https://onlyqr.0n8.de/hello-world" width="200" height="200" style="border:0"></iframe>
|
||||||
|
<iframe src="https://onlyqr.0n8.de/https%3A%2F%2Fexample.com?color=ff0000" width="200" height="200" style="border:0"></iframe>
|
||||||
|
```
|
||||||
|
|
||||||
|
- `?color=` takes a hex value without `#` (e.g. `ff0000`) or a CSS colour name. The default is `004c93`.
|
||||||
|
- Use `encodeURIComponent(text)` for the path, so that `?`, `#`, `%` and `/` in your text come through unchanged.
|
||||||
|
- Any other query parameters are treated as part of the text.
|
||||||
|
- This is handled by `404.html` on the static host, so it only works in iframes (not `<img src>`), and the HTTP status is 404.
|
||||||
|
|
||||||
## Changes from Original QRCode.js
|
## Changes from Original QRCode.js
|
||||||
|
|
||||||
- Removed Canvas and Table rendering methods
|
- Removed Canvas and Table rendering methods
|
||||||
|
|||||||
@@ -9,97 +9,6 @@
|
|||||||
|
|
||||||
var debounceTimer;
|
var debounceTimer;
|
||||||
|
|
||||||
function optimizeSVG(svg) {
|
|
||||||
// Parse the existing SVG to extract the QR data
|
|
||||||
const uses = svg.querySelectorAll('use');
|
|
||||||
const qrData = [];
|
|
||||||
let maxX = 0, maxY = 0;
|
|
||||||
|
|
||||||
// Extract positions from use elements
|
|
||||||
uses.forEach(use => {
|
|
||||||
const x = parseInt(use.getAttribute('x') || '0');
|
|
||||||
const y = parseInt(use.getAttribute('y') || '0');
|
|
||||||
if (!qrData[y]) qrData[y] = [];
|
|
||||||
qrData[y][x] = true;
|
|
||||||
maxX = Math.max(maxX, x);
|
|
||||||
maxY = Math.max(maxY, y);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get the color from the original template
|
|
||||||
const template = svg.querySelector('#template, rect[id]');
|
|
||||||
const color = template ? template.getAttribute('fill') || '#000' : '#000';
|
|
||||||
|
|
||||||
// Create optimized SVG
|
|
||||||
const optimizedSVG = createOptimizedSVG(qrData, maxX + 1, maxY + 1, color);
|
|
||||||
|
|
||||||
// Replace the old SVG with the optimized one
|
|
||||||
svg.parentNode.replaceChild(optimizedSVG, svg);
|
|
||||||
|
|
||||||
return optimizedSVG;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createOptimizedSVG(qrData, width, height, color) {
|
|
||||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
||||||
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
||||||
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
|
||||||
svg.setAttribute('width', width * 10);
|
|
||||||
svg.setAttribute('height', height * 10);
|
|
||||||
|
|
||||||
// Create defs section with reusable cell (no fill - will inherit)
|
|
||||||
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
|
|
||||||
const cellTemplate = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
||||||
cellTemplate.setAttribute('id', 'cell');
|
|
||||||
cellTemplate.setAttribute('width', '1');
|
|
||||||
cellTemplate.setAttribute('height', '1');
|
|
||||||
defs.appendChild(cellTemplate);
|
|
||||||
svg.appendChild(defs);
|
|
||||||
|
|
||||||
// Create main group with fill color
|
|
||||||
const mainGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
||||||
mainGroup.setAttribute('fill', color);
|
|
||||||
svg.appendChild(mainGroup);
|
|
||||||
|
|
||||||
// Create optimized structure
|
|
||||||
for (let y = 0; y < height; y++) {
|
|
||||||
if (!qrData[y]) continue;
|
|
||||||
|
|
||||||
// Find consecutive cells and group them
|
|
||||||
let x = 0;
|
|
||||||
while (x < width) {
|
|
||||||
if (qrData[y][x]) {
|
|
||||||
// Count consecutive cells
|
|
||||||
let consecutiveCount = 1;
|
|
||||||
while (x + consecutiveCount < width && qrData[y][x + consecutiveCount]) {
|
|
||||||
consecutiveCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (consecutiveCount > 1) {
|
|
||||||
// Use a single rect for multiple consecutive cells (inherits fill from group)
|
|
||||||
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
||||||
rect.setAttribute('x', x);
|
|
||||||
rect.setAttribute('y', y);
|
|
||||||
rect.setAttribute('width', consecutiveCount);
|
|
||||||
rect.setAttribute('height', '1');
|
|
||||||
mainGroup.appendChild(rect);
|
|
||||||
} else {
|
|
||||||
// Use individual use elements for single cells (inherits fill from group)
|
|
||||||
const use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
|
||||||
use.setAttribute('href', '#cell');
|
|
||||||
use.setAttribute('x', x);
|
|
||||||
use.setAttribute('y', y);
|
|
||||||
mainGroup.appendChild(use);
|
|
||||||
}
|
|
||||||
|
|
||||||
x += consecutiveCount;
|
|
||||||
} else {
|
|
||||||
x++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return svg;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeCode() {
|
function makeCode() {
|
||||||
var elText = document.getElementById("text");
|
var elText = document.getElementById("text");
|
||||||
var elColor = document.getElementById("color");
|
var elColor = document.getElementById("color");
|
||||||
@@ -112,7 +21,7 @@
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const svg = document.querySelector('#qrcode svg');
|
const svg = document.querySelector('#qrcode svg');
|
||||||
if (svg) {
|
if (svg) {
|
||||||
optimizeSVG(svg);
|
optimizeQRSVG(svg);
|
||||||
}
|
}
|
||||||
}, 10);
|
}, 10);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
|
|
||||||
<script src="jquery.min.js"></script>
|
<script src="jquery.min.js"></script>
|
||||||
<script src="qrcode.js"></script>
|
<script src="qrcode.js"></script>
|
||||||
|
<script src="qr-svg.js"></script>
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
/* Shared SVG optimisation used by the generator UI (index.html) and the embed page (404.html). */
|
||||||
|
(function(global) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function optimizeSVG(svg) {
|
||||||
|
// Parse the existing SVG to extract the QR data
|
||||||
|
const uses = svg.querySelectorAll('use');
|
||||||
|
const qrData = [];
|
||||||
|
let maxX = 0, maxY = 0;
|
||||||
|
|
||||||
|
// Extract positions from use elements
|
||||||
|
uses.forEach(use => {
|
||||||
|
const x = parseInt(use.getAttribute('x') || '0');
|
||||||
|
const y = parseInt(use.getAttribute('y') || '0');
|
||||||
|
if (!qrData[y]) qrData[y] = [];
|
||||||
|
qrData[y][x] = true;
|
||||||
|
maxX = Math.max(maxX, x);
|
||||||
|
maxY = Math.max(maxY, y);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the color from the original template
|
||||||
|
const template = svg.querySelector('#template, rect[id]');
|
||||||
|
const color = template ? template.getAttribute('fill') || '#000' : '#000';
|
||||||
|
|
||||||
|
// Create optimized SVG
|
||||||
|
const optimizedSVG = createOptimizedSVG(qrData, maxX + 1, maxY + 1, color);
|
||||||
|
|
||||||
|
// Replace the old SVG with the optimized one
|
||||||
|
svg.parentNode.replaceChild(optimizedSVG, svg);
|
||||||
|
|
||||||
|
return optimizedSVG;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createOptimizedSVG(qrData, width, height, color) {
|
||||||
|
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||||
|
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
||||||
|
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
||||||
|
svg.setAttribute('width', width * 10);
|
||||||
|
svg.setAttribute('height', height * 10);
|
||||||
|
|
||||||
|
// Create defs section with reusable cell (no fill - will inherit)
|
||||||
|
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
|
||||||
|
const cellTemplate = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
||||||
|
cellTemplate.setAttribute('id', 'cell');
|
||||||
|
cellTemplate.setAttribute('width', '1');
|
||||||
|
cellTemplate.setAttribute('height', '1');
|
||||||
|
defs.appendChild(cellTemplate);
|
||||||
|
svg.appendChild(defs);
|
||||||
|
|
||||||
|
// Create main group with fill color
|
||||||
|
const mainGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||||
|
mainGroup.setAttribute('fill', color);
|
||||||
|
svg.appendChild(mainGroup);
|
||||||
|
|
||||||
|
// Create optimized structure
|
||||||
|
for (let y = 0; y < height; y++) {
|
||||||
|
if (!qrData[y]) continue;
|
||||||
|
|
||||||
|
// Find consecutive cells and group them
|
||||||
|
let x = 0;
|
||||||
|
while (x < width) {
|
||||||
|
if (qrData[y][x]) {
|
||||||
|
// Count consecutive cells
|
||||||
|
let consecutiveCount = 1;
|
||||||
|
while (x + consecutiveCount < width && qrData[y][x + consecutiveCount]) {
|
||||||
|
consecutiveCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (consecutiveCount > 1) {
|
||||||
|
// Use a single rect for multiple consecutive cells (inherits fill from group)
|
||||||
|
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
||||||
|
rect.setAttribute('x', x);
|
||||||
|
rect.setAttribute('y', y);
|
||||||
|
rect.setAttribute('width', consecutiveCount);
|
||||||
|
rect.setAttribute('height', '1');
|
||||||
|
mainGroup.appendChild(rect);
|
||||||
|
} else {
|
||||||
|
// Use individual use elements for single cells (inherits fill from group)
|
||||||
|
const use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
||||||
|
use.setAttribute('href', '#cell');
|
||||||
|
use.setAttribute('x', x);
|
||||||
|
use.setAttribute('y', y);
|
||||||
|
mainGroup.appendChild(use);
|
||||||
|
}
|
||||||
|
|
||||||
|
x += consecutiveCount;
|
||||||
|
} else {
|
||||||
|
x++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
global.optimizeQRSVG = optimizeSVG;
|
||||||
|
})(window);
|
||||||
Reference in new issue
Block a user