Barcode fix

This commit is contained in:
Timo Knuth
2026-04-17 23:24:22 +02:00
parent 5894f4619d
commit aa2628834b
2 changed files with 114 additions and 23 deletions

View File

@@ -62,6 +62,43 @@ const getFrameOptionsForContentType = (contentType: string) => {
}
};
// Injects a caption <text> element below a barcode SVG and expands its height/viewBox.
// Used so the "scanner app" hint is baked into the downloaded SVG.
function addBarcodeCaptionToSvg(svgElement: SVGElement, caption: string): string {
const cloned = svgElement.cloneNode(true) as SVGElement;
const NS = 'http://www.w3.org/2000/svg';
const widthAttr = cloned.getAttribute('width');
const heightAttr = cloned.getAttribute('height');
const width = widthAttr ? parseFloat(widthAttr) : 200;
const height = heightAttr ? parseFloat(heightAttr) : 100;
const extraHeight = 18;
cloned.setAttribute('height', String(height + extraHeight));
const viewBox = cloned.getAttribute('viewBox');
if (viewBox) {
const parts = viewBox.split(/\s+/);
if (parts.length === 4) {
cloned.setAttribute(
'viewBox',
`${parts[0]} ${parts[1]} ${parts[2]} ${parseFloat(parts[3]) + extraHeight}`
);
}
}
const text = document.createElementNS(NS, 'text');
text.setAttribute('x', String(width / 2));
text.setAttribute('y', String(height + 12));
text.setAttribute('text-anchor', 'middle');
text.setAttribute('font-size', '9');
text.setAttribute('font-family', 'Arial, Helvetica, sans-serif');
text.setAttribute('fill', '#666666');
text.textContent = caption;
cloned.appendChild(text);
return new XMLSerializer().serializeToString(cloned);
}
export default function CreatePage() {
const router = useRouter();
const { t } = useTranslation();
@@ -212,7 +249,9 @@ export default function CreatePage() {
if (frameType === 'none') {
const svgElement = qrRef.current.querySelector('svg');
if (svgElement) {
const svgData = new XMLSerializer().serializeToString(svgElement);
const svgData = contentType === 'BARCODE'
? addBarcodeCaptionToSvg(svgElement, 'Scan: iPhone -> Barcode Scanner App | Android -> Google Lens')
: new XMLSerializer().serializeToString(svgElement);
const blob = new Blob([svgData], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
@@ -651,11 +690,16 @@ export default function CreatePage() {
<>
{isDynamic ? (
<>
<div className="rounded-lg bg-blue-50 border border-blue-200 p-3 text-sm text-blue-800">
<strong>How dynamic barcodes work:</strong> The barcode encodes a short redirect URL
(e.g. <span className="font-mono text-xs">qrmaster.net/r/</span>). When scanned with a
smartphone camera, it opens the browser and redirects to your destination which you
can update anytime. Works with smartphone cameras, not POS laser scanners.
<div className="rounded-lg bg-blue-50 border border-blue-200 p-3 text-sm text-blue-800 space-y-2">
<p>
<strong>How dynamic barcodes work:</strong> The barcode encodes a short redirect URL
(e.g. <span className="font-mono text-xs">qrmaster.net/r/</span>) that you can update anytime.
</p>
<p className="rounded border border-amber-300 bg-amber-50 p-2 text-xs text-amber-900">
<strong>📱 Scanner tip:</strong> Use a <strong>barcode scanner app</strong> on iPhone
(iOS Camera doesn't auto-open links from barcodes). Android Google Lens / Camera works
out of the box. Print min. 5&nbsp;cm wide for reliable scanning.
</p>
</div>
<Input
label="Destination URL"
@@ -1038,13 +1082,12 @@ export default function CreatePage() {
<CardTitle>{t('create.preview')}</CardTitle>
</CardHeader>
<CardContent className="text-center">
<div id="create-qr-preview" className="flex justify-center mb-4">
<div id="create-qr-preview" className="flex justify-center mb-4 w-full min-w-0 overflow-hidden">
{/* WRAPPER FOR REF AND FRAME */}
<div
ref={qrRef}
className="relative bg-white rounded-xl p-4 flex flex-col items-center justify-center transition-all duration-300"
className="relative bg-white rounded-xl p-4 flex flex-col items-center justify-center transition-all duration-300 w-full max-w-full min-w-0"
style={{
minWidth: '280px',
minHeight: '280px',
}}
>
@@ -1060,7 +1103,7 @@ export default function CreatePage() {
{contentType === 'BARCODE' ? (
qrContent ? (
<div className="p-2 bg-white">
<div className="p-2 bg-white w-full max-w-full [&_svg]:!w-full [&_svg]:!h-auto [&_svg]:!max-w-full">
<Barcode
key={`${qrContent}-${content.format}-${foregroundColor}`}
value={qrContent}
@@ -1068,9 +1111,14 @@ export default function CreatePage() {
lineColor={foregroundColor}
background={backgroundColor}
width={2}
height={100}
height={80}
margin={10}
displayValue={true}
fontSize={14}
/>
<p className="mt-2 text-center text-[10px] leading-tight text-gray-600 px-2">
Scan: iPhone → Barcode Scanner App · Android → Google Lens / Camera
</p>
</div>
) : (
<div className="w-[200px] h-[200px] bg-gray-100 rounded flex items-center justify-center text-gray-500">

View File

@@ -8,6 +8,41 @@ import { Badge } from '@/components/ui/Badge';
import { Dropdown, DropdownItem } from '@/components/ui/Dropdown';
import { formatDate } from '@/lib/utils';
function addBarcodeCaptionToSvg(svgElement: SVGElement, caption: string): string {
const cloned = svgElement.cloneNode(true) as SVGElement;
const NS = 'http://www.w3.org/2000/svg';
const widthAttr = cloned.getAttribute('width');
const heightAttr = cloned.getAttribute('height');
const width = widthAttr ? parseFloat(widthAttr) : 200;
const height = heightAttr ? parseFloat(heightAttr) : 100;
const extraHeight = 18;
cloned.setAttribute('height', String(height + extraHeight));
const viewBox = cloned.getAttribute('viewBox');
if (viewBox) {
const parts = viewBox.split(/\s+/);
if (parts.length === 4) {
cloned.setAttribute(
'viewBox',
`${parts[0]} ${parts[1]} ${parts[2]} ${parseFloat(parts[3]) + extraHeight}`
);
}
}
const text = document.createElementNS(NS, 'text');
text.setAttribute('x', String(width / 2));
text.setAttribute('y', String(height + 12));
text.setAttribute('text-anchor', 'middle');
text.setAttribute('font-size', '9');
text.setAttribute('font-family', 'Arial, Helvetica, sans-serif');
text.setAttribute('fill', '#666666');
text.textContent = caption;
cloned.appendChild(text);
return new XMLSerializer().serializeToString(cloned);
}
interface QRCodeCardProps {
qr: {
id: string;
@@ -113,7 +148,9 @@ END:VCARD`;
const svgContainer = document.querySelector(`#qr-svg-${qr.id}`);
const svg = svgContainer?.querySelector('svg');
if (svg) {
let svgData = new XMLSerializer().serializeToString(svg);
let svgData = qr.contentType === 'BARCODE'
? addBarcodeCaptionToSvg(svg as SVGElement, 'Scan: iPhone -> Barcode Scanner App | Android -> Google Lens')
: new XMLSerializer().serializeToString(svg);
if (qr.style?.cornerStyle === 'rounded') {
const width = svg.getAttribute('width') || '96';
@@ -210,17 +247,23 @@ END:VCARD`;
)}
<div id={`qr-svg-${qr.id}`} className={qr.style?.cornerStyle === 'rounded' ? 'rounded-lg overflow-hidden' : ''}>
{qr.contentType === 'BARCODE' ? (
<Barcode
key={`${qr.id}-${qr.type === 'STATIC' ? qr.content?.value : qrUrl}-${qr.content?.format}`}
value={qr.type === 'STATIC' ? (qr.content?.value || '123456789') : qrUrl}
format={(qr.content?.format as any) || 'CODE128'}
lineColor={qr.style?.foregroundColor || '#000000'}
background={qr.style?.backgroundColor || '#FFFFFF'}
width={1.5}
height={60}
displayValue={true}
fontSize={10}
/>
<div className="w-full max-w-[240px] [&_svg]:!w-full [&_svg]:!h-auto [&_svg]:!max-w-full">
<Barcode
key={`${qr.id}-${qr.type === 'STATIC' ? qr.content?.value : qrUrl}-${qr.content?.format}`}
value={qr.type === 'STATIC' ? (qr.content?.value || '123456789') : qrUrl}
format={(qr.content?.format as any) || 'CODE128'}
lineColor={qr.style?.foregroundColor || '#000000'}
background={qr.style?.backgroundColor || '#FFFFFF'}
width={2}
height={70}
margin={8}
displayValue={true}
fontSize={11}
/>
<p className="mt-1 text-center text-[9px] leading-tight text-gray-600 px-1">
Scan: iPhone Barcode Scanner App · Android Google Lens
</p>
</div>
) : (
<QRCodeSVG
value={qrUrl}