/** * `shareActions`: the ways the share created page hands a share over besides copying its link — * the device's share sheet, where the browser has one, and the QR code in its dialog saved as a * PNG. Both carry only the link. * * The PNG is drawn in the browser from the dialog's SVG, which is 1024 pixels square, so the * server needs no image extension and every browser rasterises it at full size. */ document.addEventListener('alpine:init', () => { window.Alpine.data('shareActions', ({ url, title, filename, messages }) => ({ open: false, canShare: typeof navigator.share === 'function', async share() { try { await navigator.share({ title, url }) } catch (error) { if (error?.name !== 'AbortError') { window.materialToast(messages.shareFailed, { type: 'error' }) } } }, async downloadQrCode(svg) { try { const png = await rasterise(svg) const link = document.createElement('a') const href = URL.createObjectURL(png) link.href = href link.download = filename link.click() setTimeout(() => URL.revokeObjectURL(href), 0) } catch { window.materialToast(messages.downloadFailed, { type: 'error' }) } }, })) }) /** * The SVG element as a PNG blob, at the SVG's own width and height, on white. */ async function rasterise(svg) { const width = Number(svg.getAttribute('width')) const height = Number(svg.getAttribute('height')) const source = URL.createObjectURL(new Blob([new XMLSerializer().serializeToString(svg)], { type: 'image/svg+xml' })) try { const image = new Image() image.src = source await image.decode() const canvas = document.createElement('canvas') canvas.width = width canvas.height = height const context = canvas.getContext('2d') context.imageSmoothingEnabled = false context.fillStyle = '#ffffff' context.fillRect(0, 0, width, height) context.drawImage(image, 0, 0, width, height) const png = await new Promise((resolve) => canvas.toBlob(resolve, 'image/png')) if (!png) { throw new Error('The canvas gave no image.') } return png } finally { URL.revokeObjectURL(source) } }