Environment
- @liquid-js/qr-code-styling 5.5.0, SVG output
- Chrome 138+ (
Page.printToPDF / headless --print-to-pdf)
- Viewers affected: macOS Preview, Quick Look, Safari, all iOS browsers (every CoreGraphics-based PDF viewer). Chrome's own PDF viewer and Acrobat render the same file correctly, so the problem is easy to miss.
Problem
Each color layer is drawn as a full-size <rect> clipped by a <clipPath> holding every dot shape (mask-dot-color, mask-background-color, ...). When such an SVG goes through Chrome's print-to-PDF, Skia emits the whole QR as a Form XObject with a transparency group. CoreGraphics viewers fail to render that group once it covers more than roughly 25% of the page's shorter side - the QR comes out partially drawn with the rest painted as a solid black block, and the code cannot be scanned.
This hits any print use where the page is small relative to the QR code.
| Before |
After the workaround below |
 |
 |
Reproduction
<style>@page { size: 8.5cm 5.5cm; margin: 0; }</style>
<div id="qr" style="width: 90px"></div>
<script type="module">
import { QRCodeStyling } from '@liquid-js/qr-code-styling';
new QRCodeStyling({
data: 'https://example.com',
dotsOptions: { type: 'extra-rounded', size: 10 },
cornersSquareOptions: { type: 'extra-rounded' },
backgroundOptions: { color: '#ffffff' },
}).append(document.getElementById('qr'));
</script>
- Print the page to PDF with Chrome (UI or
--headless --print-to-pdf), with the SVG scaled down to ~90 px.
- Open the PDF in macOS Preview or Safari (or run
qlmanage -t -s 1400 -o . out.pdf on macOS - Quick Look uses the same engine).
- The bottom/right part of the QR is a solid black rectangle. The screenshots above are exactly this file, before and after applying the workaround.
Evidence it is the clipPath structure
- Replacing each clipped
<rect> with a <g fill> containing the clip's shapes (identical geometry, no clipping) fixes the render in every viewer: with clipping removed, Chrome inlines plain filled paths into the page content stream and creates no Form XObject at all (verified by reading the PDF objects - 0 form XObjects after, 1 transparency group before).
- The absolute QR size does not matter, only its ratio to the page: editing nothing but the PDF's MediaBox (page size) toggles the bug on and off; an A4 page with a 400 px QR fails the same way an 8.5 x 5.5 cm page with a 90 px QR does.
Suggestion
An option (or a built-in plugin) to emit the dot/corner shapes directly with a fill, grouped in <g>, skipping the clipPath indirection for solid colors. Solid-color QRs do not need the clip at all; gradients could keep the current path or use a gradientUnits="userSpaceOnUse" gradient on the group.
This would also settle two long-standing reports about the same structure: kozakdenys#92 (clipPaths breaking Figma / Illustrator imports) and kozakdenys#309 (url('#id') quoting rejected by some SVG renderers) - with shapes emitted directly there is no url() reference at all. I filed the same report upstream as kozakdenys#322, but that repo has had no release since April 2025, so this fork looks like the place where it can actually be fixed.
Workaround
The plugin API makes this easy to bolt on from the outside:
const flattenClipPaths = (svg: SVGSVGElement) => {
svg.querySelectorAll('rect[clip-path]').forEach((rect) => {
const id = /#([^'")]+)/.exec(rect.getAttribute('clip-path') ?? '')?.[1];
const fill = rect.getAttribute('fill');
const clipPath = id ? svg.querySelector(`#${CSS.escape(id)}`) : null;
if (!clipPath || !fill) return;
const group = document.createElementNS(svg.namespaceURI, 'g');
group.setAttribute('fill', fill);
group.append(...clipPath.children);
rect.replaceWith(group);
clipPath.remove();
});
};
new QRCodeStyling({
data,
plugins: [{ postProcess: (svg) => flattenClipPaths(svg) }],
});
Environment
Page.printToPDF/ headless--print-to-pdf)Problem
Each color layer is drawn as a full-size
<rect>clipped by a<clipPath>holding every dot shape (mask-dot-color,mask-background-color, ...). When such an SVG goes through Chrome's print-to-PDF, Skia emits the whole QR as a Form XObject with a transparency group. CoreGraphics viewers fail to render that group once it covers more than roughly 25% of the page's shorter side - the QR comes out partially drawn with the rest painted as a solid black block, and the code cannot be scanned.This hits any print use where the page is small relative to the QR code.
Reproduction
--headless --print-to-pdf), with the SVG scaled down to ~90 px.qlmanage -t -s 1400 -o . out.pdfon macOS - Quick Look uses the same engine).Evidence it is the clipPath structure
<rect>with a<g fill>containing the clip's shapes (identical geometry, no clipping) fixes the render in every viewer: with clipping removed, Chrome inlines plain filled paths into the page content stream and creates no Form XObject at all (verified by reading the PDF objects - 0 form XObjects after, 1 transparency group before).Suggestion
An option (or a built-in plugin) to emit the dot/corner shapes directly with a
fill, grouped in<g>, skipping the clipPath indirection for solid colors. Solid-color QRs do not need the clip at all; gradients could keep the current path or use agradientUnits="userSpaceOnUse"gradient on the group.This would also settle two long-standing reports about the same structure: kozakdenys#92 (clipPaths breaking Figma / Illustrator imports) and kozakdenys#309 (
url('#id')quoting rejected by some SVG renderers) - with shapes emitted directly there is nourl()reference at all. I filed the same report upstream as kozakdenys#322, but that repo has had no release since April 2025, so this fork looks like the place where it can actually be fixed.Workaround
The plugin API makes this easy to bolt on from the outside: