Center image does not render on first render in Safari
Center image does not render on first render in Safari
I've tried with base64 to rule out any CORS issues etc, but it still does not work sadly.
I seem to have kind of circumvented the issue very dirtily as follows:
const qrCode = ...;
await getRawData();
if (navigator.userAgent.indexOf("Safari") !== -1) {
// weird bug where safari fails to render the image in the first call
await new Promise((resolve) => setTimeout(resolve, 500));
const qrCode = new QRCodeStyling({
// ... reinstantiating class seems to be necessary
});
return (await qrCode.getRawData("png")) as Buffer | Blob;
}
If I remove the setTimeout, it starts occurring again, same goes for the reinstantiated class
NOTE: lower values for the promise probably work too
What worked for me was to simply do a .update({ data }) to trigger a redraw after the first draw finishes.
Came to report this same bug. Here's a standalone example of it occurring. Hope this helps.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>QR Code Styling - Initial Render Bug</title>
<!-- Library Script (Version 1.9.2, as used in the user's project) -->
<script
type="text/javascript"
src="https://unpkg.com/[email protected]/lib/qr-code-styling.js"
></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
Helvetica, Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
background-color: #f0f2f5;
text-align: center;
padding: 1rem;
box-sizing: border-box;
}
#canvas {
border: 1px solid #ccc;
border-radius: 8px;
margin-bottom: 1.5rem;
}
button {
font-size: 1rem;
padding: 0.75rem 1.5rem;
border-radius: 8px;
border: none;
background-color: #007bff;
color: white;
cursor: pointer;
transition: background-color 0.2s;
}
button:hover {
background-color: #0056b3;
}
p {
max-width: 600px;
line-height: 1.6;
}
.fail {
color: #d93025;
font-weight: bold;
}
.success {
color: #1e8e3e;
font-weight: bold;
}
</style>
</head>
<body>
<h1>QR Code Styling v1.9.2 - Initial Render Bug</h1>
<p>
<strong>To Reproduce:</strong><br />
1. Open this page on a mobile browser (or a desktop browser simulating a
mobile device).<br />
2. <span class="fail">Observe:</span> The QR code renders, but the logo in
the center is missing.<br />
3. Click the "Force Re-render" button below.<br />
4. <span class="success">Observe:</span> The logo now appears correctly.
</p>
<div id="canvas"></div>
<button id="rerender-button">Force Re-render</button>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Define the options for the QR code, including the image.
// The image URL must be absolute and publicly accessible.
const options = {
width: 300,
height: 300,
data: 'https://github.com/kozakdenys/qr-code-styling',
image:
'https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/github.svg',
dotsOptions: {
color: '#4267b2',
type: 'rounded',
},
imageOptions: {
crossOrigin: 'anonymous',
margin: 10,
},
};
// 1. Create a new instance of the QR code styling library.
const qrCode = new QRCodeStyling(options);
// 2. Append it to the DOM.
// On mobile, this initial render fails to include the image.
qrCode.append(document.getElementById('canvas'));
// 3. Add a click listener to the button.
const rerenderButton = document.getElementById('rerender-button');
rerenderButton.addEventListener('click', () => {
// Calling .update() with the exact same options fixes the issue.
// This demonstrates that the initial render is the problem.
console.log('Forcing re-render with the same options...');
qrCode.update(options);
});
/*
// WORKAROUND: The bug can be fixed by forcing a second update
// immediately after the first one using a zero-delay timeout.
// This suggests an internal initialization or timing issue in the library.
setTimeout(() => {
console.log("Applying workaround: updating QR code after 0ms timeout.");
qrCode.update(options);
}, 0);
*/
});
</script>
</body>
</html>
After some further investigation, the issue here seems to be that when the publicQRCodeStyling.update() method is called, the library starts an internal, non-blocking, asynchronous rendering process, but since the public method is synchronous it doesn't return a promise. That means that you can't simply await QRCodeStyline.update(options).
The workaround is two-fold. Thankfully the library does make its internal promise available as part of the class export. So it is possible to explicitly wait for that internal promise to resolve, e.g.
QRCodeStyling.update(options);
if (QRCodeStyling._canvasDrawingPromise) {
await QRCodeStyling._canvasDrawingPromise;
}
That is the equivalent to await qrCode.update(options) if the public method were set up as an asynchronous function.
The second part of the fix is to then force a re-render after the initial render is known to have completed.
My solution is to wrap all of that together in a subclass that behaves asynchronously, and only requires changing the initial constructor and then await'ing the first usage on page load...
/**
* @file AsyncQRCodeStyling.ts
* @author Matthew Carroll
* @description A wrapper for the 'qr-code-styling' library to provide a truly async, reliable update method.
* @version 1.2.0
*/
import QRCodeStyling, { Options } from 'qr-code-styling';
/**
* A wrapper class that extends QRCodeStyling to provide a truly async `update` method.
*/
class AsyncQRCodeStyling extends QRCodeStyling {
private _isInitialRender = true;
constructor(options?: Partial<Options>) {
super(options);
}
/**
* Overrides the original update method with a new async version that
* correctly waits for the QR code to be fully rendered. The complex
* workaround for the mobile rendering bug is only applied on the first call.
* @param {Partial<Options>} options - The options object to pass to the update method.
* @returns {Promise<void>} A promise that resolves when the QR code is guaranteed to be fully rendered.
*/
async update(options?: Partial<Options>): Promise<void> {
// Call the original update method to start the render.
super.update(options);
// Always await the library's internal drawing promise.
if ((this as any)._canvasDrawingPromise) {
await (this as any)._canvasDrawingPromise;
}
// Only apply the expensive workaround on the very first render.
if (this._isInitialRender) {
this._isInitialRender = false; // Set the flag so this block never runs again.
// Return a new promise that handles the browser render delay
// and performs the corrective redraw.
return new Promise<void>((resolve) => {
setTimeout(() => {
if (options?.data) {
super.update({ data: options.data });
}
resolve();
}, 250);
});
}
}
}
export default AsyncQRCodeStyling;