Skip to content

SVG clipPath layers render as a solid black block in CoreGraphics PDF viewers after Chrome print-to-PDF #8

Description

@sp-kblazek

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
QR rendered with a solid black block QR rendered correctly

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>
  1. Print the page to PDF with Chrome (UI or --headless --print-to-pdf), with the SVG scaled down to ~90 px.
  2. 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).
  3. 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) }],
});

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions