Skip to content

[Remote Snapshotting] Add GraphicsContextCGDelegate and use it for PDF content - #77390

Draft
szewai wants to merge 1 commit into
WebKit:mainfrom
szewai:eng/Add-GraphicsContextCGDelegate-and-use-it-for-PDF-content-in-remote-snapshots
Draft

szewai wants to merge 1 commit into
WebKit:mainfrom
szewai:eng/Add-GraphicsContextCGDelegate-and-use-it-for-PDF-content-in-remote-snapshots

Conversation

@szewai

@szewai szewai commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

ec7e31f

[Remote Snapshotting] Add GraphicsContextCGDelegate and use it for PDF content
https://bugs.webkit.org/show_bug.cgi?id=327949
rdar://190314999

Reviewed by NOBODY (OOPS!).

With remote snapshotting, the web process records drawing into a GraphicsContext that has no
CGContext, and the GPU process draws the recording. PDFKit can only draw a page into a CGContext, so
UnifiedPDFPlugin::paintPDFContent() currently draws each page into a local bitmap and records that
image. As a result, the PDF content in printed or PDF-snapshotted pages is rasterized: text can't be
selected and the output is blurry when zoomed in.

This patch adds GraphicsContextCGDelegate, a CGContextDelegate that translates the CoreGraphics
drawing callbacks back into calls on a WebCore GraphicsContext. PDFKit draws into the CGContext that
the delegate creates, and every drawing callback maps the CG gstate (CTM, clip stack, alpha,
composite operation and blend mode, antialiasing, colors and line parameters) onto the target within
a save()/restore() pair. Because the recorded drawing is plain GraphicsContext drawing, the GPU
process writes it to the PDF as vectors.

This patch covers the core of the delegate: the queries CG makes (bounds, transform, color space),
fills and strokes of rects, lines, arcs and paths, clip stacks of rects and paths, and the
clear/erase/flush operations. When the target is a PDF document, the CGContext is a PostScript type
context, because PDFKit draws annotations as it does for printing only in PDF and PostScript
contexts; otherwise open popup annotations that local printing omits would be drawn.

Images, text, gradients, shadings, transparency layers, CG display lists, soft masks, mask clips and
pattern colors are not implemented yet and will be added by follow-up patches. Some callbacks are
intentionally not implemented: BeginPage / EndPage (the target isn't paginated, and bitmap contexts
don't implement them either), DrawWindowContents (window server content can't be represented in a
GraphicsContext), CreateImage / GetBitmapContextInfo (there is no backing store to read back) and
SerializeDisplayList (only meaningful for display list delegates).

The delegate is used only for remote snapshots. The UI process uses remote snapshotting only when
RemoteSnapshottingEnabled is on and the frame tree is hosted by multiple processes
(WebPageProxy::shouldUseRemoteSnapshotting()); the web process then records into a
RemoteSnapshotRecorderProxy, whose rendering mode is DisplayList or PDFDocument. UnifiedPDFPlugin
checks for such a context, rather than only for the lack of a platform context, so that other
recording contexts (like the remote context of a layer backing store) are not drawn through the
delegate.

Since the delegate can't draw most PDFs correctly yet, it's also gated by the new internal
preference RemoteSnapshottingUnifiedPDFVectorDrawingEnabled, which is off by default. Without it,
remote snapshots keep drawing PDF content into a bitmap.

Tests: UnifiedPDF.PrintEmbeddedPDFWithRemoteSnapshotting
       UnifiedPDF.PrintEmbeddedPDFPathsAndStrokesWithRemoteSnapshotting
       UnifiedPDF.PrintEmbeddedPDFOpenPopupAnnotationWithRemoteSnapshotting

* Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml:
* Source/WebCore/PAL/pal/spi/cg/CoreGraphicsSPI.h:
* Source/WebCore/PlatformCocoa.cmake:
* Source/WebCore/SourcesCocoa.txt:
* Source/WebCore/WebCore.xcodeproj/project.pbxproj:
* Source/WebCore/platform/graphics/cg/GraphicsContextCGDelegate.cpp: Added.
(WebCore::GStateScope::GStateScope):
(WebCore::GStateScope::shouldDraw const):
(WebCore::GStateScope::target const):
(WebCore::GStateScope::ctm const):
(WebCore::interpolationQuality):
(WebCore::compositeMode):
(WebCore::lineCap):
(WebCore::lineJoin):
(WebCore::clampPathCoordinates):
(WebCore::pathFromCGPath):
(WebCore::GStateScope::initialize):
(WebCore::GStateScope::~GStateScope):
(WebCore::GStateScope::paintForDrawingMode):
(WebCore::GStateScope::windRuleForDrawingMode):
(WebCore::GStateScope::applyClipStack):
(WebCore::GStateScope::applyCompositeOperation):
(WebCore::GStateScope::applyLineParameters):
(WebCore::GStateScope::applyBrush):
(WebCore::GraphicsContextCGDelegate::createCGContext):
(WebCore::GraphicsContextCGDelegate::GraphicsContextCGDelegate):
(WebCore::GraphicsContextCGDelegate::fromDelegate):
(WebCore::GraphicsContextCGDelegate::installCallbacks):
(WebCore::GraphicsContextCGDelegate::logUnimplementedCallback):
(WebCore::GraphicsContextCGDelegate::finalize):
(WebCore::GraphicsContextCGDelegate::getColorTransform):
(WebCore::GraphicsContextCGDelegate::getTransform):
(WebCore::GraphicsContextCGDelegate::getBounds):
(WebCore::GraphicsContextCGDelegate::deviceBounds const):
(WebCore::GraphicsContextCGDelegate::getColorSpace):
(WebCore::GraphicsContextCGDelegate::getName):
(WebCore::drawPathWithMode):
(WebCore::GraphicsContextCGDelegate::drawLines):
(WebCore::GraphicsContextCGDelegate::drawRects):
(WebCore::GraphicsContextCGDelegate::drawPath):
(WebCore::GraphicsContextCGDelegate::drawPathDirect):
(WebCore::GraphicsContextCGDelegate::strokeArc):
(WebCore::GraphicsContextCGDelegate::operation):
* Source/WebCore/platform/graphics/cg/GraphicsContextCGDelegate.h: Added.
(WebCore::GraphicsContextCGDelegate::target const):
* Source/WebKit/WebProcess/Plugins/PDF/UnifiedPDF/UnifiedPDFPlugin.h:
* Source/WebKit/WebProcess/Plugins/PDF/UnifiedPDF/UnifiedPDFPlugin.mm:
(WebKit::UnifiedPDFPlugin::shouldDrawPDFContentThroughCGDelegate const):
(WebKit::UnifiedPDFPlugin::paintPDFContent):
* Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKPrinting.mm:
(TestWebKitAPI::configurationForPrintingUnifiedPDF):
(TestWebKitAPI::printMainFrameToPDF):
(TestWebKitAPI::createPDFDocument):
(TestWebKitAPI::renderPrintedPage):
(TestWebKitAPI::colorsAreClose):
(TestWebKitAPI::countPixelsCloseTo):
(TestWebKitAPI::countDifferentPixels):
(TestWebKitAPI::countPDFResources):
(TestWebKitAPI::countPDFResourcesOfPage):
(TestWebKitAPI::countPDFResourcesOfFirstPage):
(TestWebKitAPI::waitForRequests):
(TestWebKitAPI::loadPageForPrinting):
(TestWebKitAPI::createPDFData):
(TestWebKitAPI::createPDFDataWithObjects):
(TestWebKitAPI::solidPDFColor):
(TestWebKitAPI::createPDFDataWithSolidFill):
(TestWebKitAPI::printDocumentWithEmbeddedPDFs):
(TestWebKitAPI::UNIFIED_PDF_TEST):
(TestWebKitAPI::printEmbeddedPDFData):
(TestWebKitAPI::createPDFDataWithPathsAndStrokes):
(TestWebKitAPI::createPDFDataWithOpenPopupAnnotation):
(TestWebKitAPI::countYellowPixelsInPopup):

ec7e31f

Misc iOS, visionOS, tvOS & watchOS macOS Linux Windows Apple Internal
❌ 🧪 style ❌ 🛠 ios ❌ 🛠 mac ✅ 🛠 wpe ✅ 🛠 win ⏳ 🛠 ios-apple
✅ 🧪 bindings ❌ 🛠 ios-sim ✅ 🛠 mac-AS-debug ✅ 🧪 wpe-wk2 ✅ 🧪 win-tests ⏳ 🛠 mac-apple
✅ 🧪 webkitperl ❌ 🧪 ios-wk2 ❌ 🧪 api-mac ✅ 🧪 api-wpe ⏳ 🛠 vision-apple
❌ 🧪 ios-wk2-wpt ⏳ 🧪 api-mac-debug ✅ 🧪 jsc-wpe
❌ 🧪 jsc-x86-64 ❌ 🧪 api-ios ❌ 🧪 mac-wk2 ✅ 🛠 gtk3-gcc
✅ 🛠 🧪 jsc-debug-arm64 ❌ 🛠 ios-safer-cpp ✅ 🧪 mac-AS-debug-wk2 ✅ 🛠 gtk
❌ 🛠 vision ❌ 🧪 mac-wk2-stress ✅ 🧪 gtk-wk2
❌ 🛠 vision-sim ❌ 🧪 mac-intel-wk2 ✅ 🧪 api-gtk
❌ 🧪 vision-wk2 ❌ 🛠 mac-safer-cpp ✅ 🛠 playstation
❌ 🛠 tv ❌ 🧪 mac-site-isolation
❌ 🛠 tv-sim
❌ 🛠 watch
❌ 🛠 watch-sim

@szewai szewai self-assigned this Oct 17, 2026
@szewai szewai added the New Bugs Unclassified bugs are placed in this component until the correct component can be determined. label Oct 17, 2026
…F content

https://bugs.webkit.org/show_bug.cgi?id=327949
rdar://190314999

Reviewed by NOBODY (OOPS!).

With remote snapshotting, the web process records drawing into a GraphicsContext that has no
CGContext, and the GPU process draws the recording. PDFKit can only draw a page into a CGContext, so
UnifiedPDFPlugin::paintPDFContent() currently draws each page into a local bitmap and records that
image. As a result, the PDF content in printed or PDF-snapshotted pages is rasterized: text can't be
selected and the output is blurry when zoomed in.

This patch adds GraphicsContextCGDelegate, a CGContextDelegate that translates the CoreGraphics
drawing callbacks back into calls on a WebCore GraphicsContext. PDFKit draws into the CGContext that
the delegate creates, and every drawing callback maps the CG gstate (CTM, clip stack, alpha,
composite operation and blend mode, antialiasing, colors and line parameters) onto the target within
a save()/restore() pair. Because the recorded drawing is plain GraphicsContext drawing, the GPU
process writes it to the PDF as vectors.

This patch covers the core of the delegate: the queries CG makes (bounds, transform, color space),
fills and strokes of rects, lines, arcs and paths, clip stacks of rects and paths, and the
clear/erase/flush operations. When the target is a PDF document, the CGContext is a PostScript type
context, because PDFKit draws annotations as it does for printing only in PDF and PostScript
contexts; otherwise open popup annotations that local printing omits would be drawn.

Images, text, gradients, shadings, transparency layers, CG display lists, soft masks, mask clips and
pattern colors are not implemented yet and will be added by follow-up patches. Some callbacks are
intentionally not implemented: BeginPage / EndPage (the target isn't paginated, and bitmap contexts
don't implement them either), DrawWindowContents (window server content can't be represented in a
GraphicsContext), CreateImage / GetBitmapContextInfo (there is no backing store to read back) and
SerializeDisplayList (only meaningful for display list delegates).

The delegate is used only for remote snapshots. The UI process uses remote snapshotting only when
RemoteSnapshottingEnabled is on and the frame tree is hosted by multiple processes
(WebPageProxy::shouldUseRemoteSnapshotting()); the web process then records into a
RemoteSnapshotRecorderProxy, whose rendering mode is DisplayList or PDFDocument. UnifiedPDFPlugin
checks for such a context, rather than only for the lack of a platform context, so that other
recording contexts (like the remote context of a layer backing store) are not drawn through the
delegate.

Since the delegate can't draw most PDFs correctly yet, it's also gated by the new internal
preference RemoteSnapshottingUnifiedPDFVectorDrawingEnabled, which is off by default. Without it,
remote snapshots keep drawing PDF content into a bitmap.

Tests: UnifiedPDF.PrintEmbeddedPDFWithRemoteSnapshotting
       UnifiedPDF.PrintEmbeddedPDFPathsAndStrokesWithRemoteSnapshotting
       UnifiedPDF.PrintEmbeddedPDFOpenPopupAnnotationWithRemoteSnapshotting

* Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml:
* Source/WebCore/PAL/pal/spi/cg/CoreGraphicsSPI.h:
* Source/WebCore/PlatformCocoa.cmake:
* Source/WebCore/SourcesCocoa.txt:
* Source/WebCore/WebCore.xcodeproj/project.pbxproj:
* Source/WebCore/platform/graphics/cg/GraphicsContextCGDelegate.cpp: Added.
(WebCore::GStateScope::GStateScope):
(WebCore::GStateScope::shouldDraw const):
(WebCore::GStateScope::target const):
(WebCore::GStateScope::ctm const):
(WebCore::interpolationQuality):
(WebCore::compositeMode):
(WebCore::lineCap):
(WebCore::lineJoin):
(WebCore::clampPathCoordinates):
(WebCore::pathFromCGPath):
(WebCore::GStateScope::initialize):
(WebCore::GStateScope::~GStateScope):
(WebCore::GStateScope::paintForDrawingMode):
(WebCore::GStateScope::windRuleForDrawingMode):
(WebCore::GStateScope::applyClipStack):
(WebCore::GStateScope::applyCompositeOperation):
(WebCore::GStateScope::applyLineParameters):
(WebCore::GStateScope::applyBrush):
(WebCore::GraphicsContextCGDelegate::createCGContext):
(WebCore::GraphicsContextCGDelegate::GraphicsContextCGDelegate):
(WebCore::GraphicsContextCGDelegate::fromDelegate):
(WebCore::GraphicsContextCGDelegate::installCallbacks):
(WebCore::GraphicsContextCGDelegate::logUnimplementedCallback):
(WebCore::GraphicsContextCGDelegate::finalize):
(WebCore::GraphicsContextCGDelegate::getColorTransform):
(WebCore::GraphicsContextCGDelegate::getTransform):
(WebCore::GraphicsContextCGDelegate::getBounds):
(WebCore::GraphicsContextCGDelegate::deviceBounds const):
(WebCore::GraphicsContextCGDelegate::getColorSpace):
(WebCore::GraphicsContextCGDelegate::getName):
(WebCore::drawPathWithMode):
(WebCore::GraphicsContextCGDelegate::drawLines):
(WebCore::GraphicsContextCGDelegate::drawRects):
(WebCore::GraphicsContextCGDelegate::drawPath):
(WebCore::GraphicsContextCGDelegate::drawPathDirect):
(WebCore::GraphicsContextCGDelegate::strokeArc):
(WebCore::GraphicsContextCGDelegate::operation):
* Source/WebCore/platform/graphics/cg/GraphicsContextCGDelegate.h: Added.
(WebCore::GraphicsContextCGDelegate::target const):
* Source/WebKit/WebProcess/Plugins/PDF/UnifiedPDF/UnifiedPDFPlugin.h:
* Source/WebKit/WebProcess/Plugins/PDF/UnifiedPDF/UnifiedPDFPlugin.mm:
(WebKit::UnifiedPDFPlugin::shouldDrawPDFContentThroughCGDelegate const):
(WebKit::UnifiedPDFPlugin::paintPDFContent):
* Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKPrinting.mm:
(TestWebKitAPI::configurationForPrintingUnifiedPDF):
(TestWebKitAPI::printMainFrameToPDF):
(TestWebKitAPI::createPDFDocument):
(TestWebKitAPI::renderPrintedPage):
(TestWebKitAPI::colorsAreClose):
(TestWebKitAPI::countPixelsCloseTo):
(TestWebKitAPI::countDifferentPixels):
(TestWebKitAPI::countPDFResources):
(TestWebKitAPI::countPDFResourcesOfPage):
(TestWebKitAPI::countPDFResourcesOfFirstPage):
(TestWebKitAPI::waitForRequests):
(TestWebKitAPI::loadPageForPrinting):
(TestWebKitAPI::createPDFData):
(TestWebKitAPI::createPDFDataWithObjects):
(TestWebKitAPI::solidPDFColor):
(TestWebKitAPI::createPDFDataWithSolidFill):
(TestWebKitAPI::printDocumentWithEmbeddedPDFs):
(TestWebKitAPI::UNIFIED_PDF_TEST):
(TestWebKitAPI::printEmbeddedPDFData):
(TestWebKitAPI::createPDFDataWithPathsAndStrokes):
(TestWebKitAPI::createPDFDataWithOpenPopupAnnotation):
(TestWebKitAPI::countYellowPixelsInPopup):
@szewai
szewai force-pushed the eng/Add-GraphicsContextCGDelegate-and-use-it-for-PDF-content-in-remote-snapshots branch from ac75594 to ec7e31f Compare October 17, 2026 18:21
@weinig

weinig commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

This seems like it is going to have a lot of overlap with the infrastructure in DrawGlyphsRecorder. What effort has been made to reuse that work first?

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

Labels

merging-blocked Applied to prevent a change from being merged New Bugs Unclassified bugs are placed in this component until the correct component can be determined.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants