From 43e6433e5713936fc8963dcbf6cedda8301c34e3 Mon Sep 17 00:00:00 2001 From: Ben Bowler Date: Wed, 29 Jul 2026 15:26:14 +0100 Subject: [PATCH 1/2] Patch @react-pdf/render to specify named destination coordinates so PDF section links work in Apple Preview. --- patches/@react-pdf+render+4.5.1.patch | 13 ++++++++ .../pdf-generation/pdf-generation-page.ts | 30 ++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 patches/@react-pdf+render+4.5.1.patch diff --git a/patches/@react-pdf+render+4.5.1.patch b/patches/@react-pdf+render+4.5.1.patch new file mode 100644 index 00000000000..357ed85613b --- /dev/null +++ b/patches/@react-pdf+render+4.5.1.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/@react-pdf/render/lib/index.js b/node_modules/@react-pdf/render/lib/index.js +index 028cf37..8f04893 100644 +--- a/node_modules/@react-pdf/render/lib/index.js ++++ b/node_modules/@react-pdf/render/lib/index.js +@@ -2249,7 +2249,7 @@ const setDestination = (ctx, node) => { + if (!node.props) + return; + if ('id' in node.props) { +- ctx.addNamedDestination(node.props.id, 'XYZ', null, node.box.top, null); ++ ctx.addNamedDestination(node.props.id, 'XYZ', node.box.left || 0, node.box.top, null); + } + }; + diff --git a/tests/playwright/specs/pdf-generation/pdf-generation-page.ts b/tests/playwright/specs/pdf-generation/pdf-generation-page.ts index 44e4e7185a6..d19f91e4236 100644 --- a/tests/playwright/specs/pdf-generation/pdf-generation-page.ts +++ b/tests/playwright/specs/pdf-generation/pdf-generation-page.ts @@ -18,7 +18,7 @@ * External dependencies */ import { Download, Locator, Page, TestInfo, expect } from '@playwright/test'; -import { statSync } from 'node:fs'; +import { readFileSync, statSync } from 'node:fs'; import { pdfToPng } from 'pdf-to-png-converter'; /** @@ -268,6 +268,8 @@ export class PDFGenerationPage { contentType: 'application/pdf', } ); + this.verifySectionDestinations( path ); + const pages = await pdfToPng( path, { viewportScale: 2.0 } ); const images = pages .map( ( page ) => @@ -286,6 +288,32 @@ export class PDFGenerationPage { await expect( this.page ).toHaveScreenshot( { fullPage: true } ); } + /** + * Asserts the header chips' section destinations are viewer-compatible. + * + * Each `section-*` named destination must specify a numeric left + * coordinate: Apple Preview ignores a destination's top coordinate when + * the left is unspecified (`/XYZ null null`), leaving the chips + * dead there. The destination arrays sit uncompressed in the PDF's name + * tree, so the assertion reads them from the raw bytes. + * + * @since n.e.x.t + * + * @param path The downloaded PDF's path on disk. + * @return {void} Throws when a destination is missing or unspecified. + */ + private verifySectionDestinations( path: string ) { + const pdfBytes = readFileSync( path ).toString( 'latin1' ); + const destinations = pdfBytes.match( + /\(section-[^)]+\)\s*\[[^\]]*\]/g + ); + + expect( destinations?.length ).toBeGreaterThan( 0 ); + for ( const destination of destinations as string[] ) { + expect( destination ).toMatch( /\/XYZ\s+[\d.]+\s/ ); + } + } + /** * Cancels an in-progress export via the progress snackbar's Cancel action. * From 531babf6211e7c9dbcd1ad7c85c95ade5a8b37d5 Mon Sep 17 00:00:00 2001 From: Ben Bowler Date: Wed, 29 Jul 2026 15:35:18 +0100 Subject: [PATCH 2/2] Remove PDF destination assertion from Playwright page object. --- .../pdf-generation/pdf-generation-page.ts | 30 +------------------ 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/tests/playwright/specs/pdf-generation/pdf-generation-page.ts b/tests/playwright/specs/pdf-generation/pdf-generation-page.ts index d19f91e4236..44e4e7185a6 100644 --- a/tests/playwright/specs/pdf-generation/pdf-generation-page.ts +++ b/tests/playwright/specs/pdf-generation/pdf-generation-page.ts @@ -18,7 +18,7 @@ * External dependencies */ import { Download, Locator, Page, TestInfo, expect } from '@playwright/test'; -import { readFileSync, statSync } from 'node:fs'; +import { statSync } from 'node:fs'; import { pdfToPng } from 'pdf-to-png-converter'; /** @@ -268,8 +268,6 @@ export class PDFGenerationPage { contentType: 'application/pdf', } ); - this.verifySectionDestinations( path ); - const pages = await pdfToPng( path, { viewportScale: 2.0 } ); const images = pages .map( ( page ) => @@ -288,32 +286,6 @@ export class PDFGenerationPage { await expect( this.page ).toHaveScreenshot( { fullPage: true } ); } - /** - * Asserts the header chips' section destinations are viewer-compatible. - * - * Each `section-*` named destination must specify a numeric left - * coordinate: Apple Preview ignores a destination's top coordinate when - * the left is unspecified (`/XYZ null null`), leaving the chips - * dead there. The destination arrays sit uncompressed in the PDF's name - * tree, so the assertion reads them from the raw bytes. - * - * @since n.e.x.t - * - * @param path The downloaded PDF's path on disk. - * @return {void} Throws when a destination is missing or unspecified. - */ - private verifySectionDestinations( path: string ) { - const pdfBytes = readFileSync( path ).toString( 'latin1' ); - const destinations = pdfBytes.match( - /\(section-[^)]+\)\s*\[[^\]]*\]/g - ); - - expect( destinations?.length ).toBeGreaterThan( 0 ); - for ( const destination of destinations as string[] ) { - expect( destination ).toMatch( /\/XYZ\s+[\d.]+\s/ ); - } - } - /** * Cancels an in-progress export via the progress snackbar's Cancel action. *