Add SPI to save a PDF from the contents of a WKWebView.
authorbeidson@apple.com <beidson@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 10 Sep 2019 23:12:49 +0000 (23:12 +0000)
committerbeidson@apple.com <beidson@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 10 Sep 2019 23:12:49 +0000 (23:12 +0000)
<rdar://problem/48955900> and https://bugs.webkit.org/show_bug.cgi?id=195765

Reviewed by Tim Horton.

Source/WebCore:

Covered by API tests.

* page/FrameView.cpp:
(WebCore::FrameView::paintContents): Don't paint the debug color outside of the FrameRect.

* platform/graphics/cg/GraphicsContextCG.cpp:
(WebCore::GraphicsContext::setURLForRect):

* rendering/PaintPhase.h: Add "AnnotateLinks" option to always gather and annotation links.

* rendering/RenderElement.cpp:
(WebCore::RenderElement::hasOutlineAnnotation const): Also return true when the PaintOptions include "AnnotateLinks"

Source/WebKit:

This is refactoring a combination of "snapshotFirstPage" PDF printing code and the
"takeSnapshot" API code to capture the on-screen visible page to a PDF at full fidelity.

* UIProcess/API/Cocoa/WKWebView.mm:
(-[WKWebView _takePDFSnapshotWithConfiguration:completionHandler:]):
* UIProcess/API/Cocoa/WKWebViewPrivate.h:

* UIProcess/WebPageProxy.cpp:
(WebKit::WebPageProxy::drawToPDF):
(WebKit::WebPageProxy::drawToPDFCallback):
* UIProcess/WebPageProxy.h:
* UIProcess/WebPageProxy.messages.in:

* UIProcess/ios/WKContentView.mm:
(-[WKContentView _wk_pageCountForPrintFormatter:]):

* UIProcess/ios/WebPageProxyIOS.mm:
(WebKit::WebPageProxy::drawToPDFCallback): Move to cross platform WebPageProxy.

* WebProcess/WebPage/Cocoa/WebPageCocoa.mm:
(WebKit::WebPage::pdfSnapshotAtSize):

* WebProcess/WebPage/WebPage.cpp:
(WebKit::WebPage::pdfSnapshotAtSize): Instead of assuming 1 page capped at 200 inches,
  paginate every 200 inches.
(WebKit::WebPage::drawToPDF):
(WebKit::paintSnapshotAtSize): Deleted.
(WebKit::WebPage::pdfSnapshotAtSize): Deleted.
* WebProcess/WebPage/WebPage.h:
* WebProcess/WebPage/WebPage.messages.in:

Tools:

-Add an "Export to PDF..." menu option to MiniBrowser.
-Add API tests for the API itself.

* MiniBrowser/MiniBrowser.entitlements:

* MiniBrowser/mac/BrowserWindowController.h:
* MiniBrowser/mac/BrowserWindowController.m:
(-[BrowserWindowController forceRepaint:]):
(-[BrowserWindowController saveAsPDF:]):

* MiniBrowser/mac/MainMenu.xib:

* MiniBrowser/mac/WK1BrowserWindowController.m:
(-[WK1BrowserWindowController validateMenuItem:]):
* MiniBrowser/mac/WK2BrowserWindowController.m:
(-[WK2BrowserWindowController validateMenuItem:]):
(-[WK2BrowserWindowController saveAsPDF:]):

* TestWebKitAPI/Configurations/Base.xcconfig:
* TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
* TestWebKitAPI/Tests/WebKitCocoa/PDFSnapshot.mm: Added.
(TestWebKitAPI::TEST):
* TestWebKitAPI/cocoa/TestPDFDocument.h: Added.
* TestWebKitAPI/cocoa/TestPDFDocument.mm: Added.
(TestWebKitAPI::toCGRect):
(TestWebKitAPI::toPlatformPoint):
(TestWebKitAPI::TestPDFAnnotation::TestPDFAnnotation):
(TestWebKitAPI::TestPDFAnnotation::isLink const):
(TestWebKitAPI::TestPDFAnnotation::bounds const):
(TestWebKitAPI::TestPDFAnnotation::linkURL const):
(TestWebKitAPI::TestPDFPage::create):
(TestWebKitAPI::TestPDFPage::TestPDFPage):
(TestWebKitAPI::TestPDFPage::annotations):
(TestWebKitAPI::TestPDFPage::characterCount const):
(TestWebKitAPI::TestPDFPage::text const):
(TestWebKitAPI::TestPDFPage::rectForCharacterAtIndex const):
(TestWebKitAPI::TestPDFPage::characterIndexAtPoint const):
(TestWebKitAPI::TestPDFPage::bounds const):
(TestWebKitAPI::TestPDFPage::colorAtPoint const):
(TestWebKitAPI::TestPDFDocument::createFromData):
(TestWebKitAPI::TestPDFDocument::TestPDFDocument):
(TestWebKitAPI::TestPDFDocument::pageCount const):
(TestWebKitAPI::TestPDFDocument::page):

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@249739 268f45cc-cd09-0410-ab3c-d52691b4dbfc

29 files changed:
Source/WebCore/ChangeLog
Source/WebCore/page/FrameView.cpp
Source/WebCore/platform/graphics/cg/GraphicsContextCG.cpp
Source/WebCore/rendering/PaintPhase.h
Source/WebCore/rendering/RenderElement.cpp
Source/WebKit/ChangeLog
Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm
Source/WebKit/UIProcess/API/Cocoa/WKWebViewPrivate.h
Source/WebKit/UIProcess/WebPageProxy.cpp
Source/WebKit/UIProcess/WebPageProxy.h
Source/WebKit/UIProcess/WebPageProxy.messages.in
Source/WebKit/UIProcess/ios/WKContentView.mm
Source/WebKit/UIProcess/ios/WebPageProxyIOS.mm
Source/WebKit/WebProcess/WebPage/Cocoa/WebPageCocoa.mm
Source/WebKit/WebProcess/WebPage/WebPage.cpp
Source/WebKit/WebProcess/WebPage/WebPage.h
Source/WebKit/WebProcess/WebPage/WebPage.messages.in
Tools/ChangeLog
Tools/MiniBrowser/MiniBrowser.entitlements
Tools/MiniBrowser/mac/BrowserWindowController.h
Tools/MiniBrowser/mac/BrowserWindowController.m
Tools/MiniBrowser/mac/MainMenu.xib
Tools/MiniBrowser/mac/WK1BrowserWindowController.m
Tools/MiniBrowser/mac/WK2BrowserWindowController.m
Tools/TestWebKitAPI/Configurations/Base.xcconfig
Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
Tools/TestWebKitAPI/Tests/WebKitCocoa/PDFSnapshot.mm [new file with mode: 0644]
Tools/TestWebKitAPI/cocoa/TestPDFDocument.h [new file with mode: 0644]
Tools/TestWebKitAPI/cocoa/TestPDFDocument.mm [new file with mode: 0644]

index 82a5206..d40a248 100644 (file)
@@ -1,3 +1,23 @@
+2019-09-10  Brady Eidson  <beidson@apple.com>
+
+        Add SPI to save a PDF from the contents of a WKWebView.
+        <rdar://problem/48955900> and https://bugs.webkit.org/show_bug.cgi?id=195765
+
+        Reviewed by Tim Horton.
+
+        Covered by API tests.
+
+        * page/FrameView.cpp:
+        (WebCore::FrameView::paintContents): Don't paint the debug color outside of the FrameRect.
+
+        * platform/graphics/cg/GraphicsContextCG.cpp:
+        (WebCore::GraphicsContext::setURLForRect):
+
+        * rendering/PaintPhase.h: Add "AnnotateLinks" option to always gather and annotation links.
+
+        * rendering/RenderElement.cpp:
+        (WebCore::RenderElement::hasOutlineAnnotation const): Also return true when the PaintOptions include "AnnotateLinks"
+
 2019-09-10  Jiewen Tan  <jiewen_tan@apple.com>
 
         REGRESSION: [ Catalina WK2 ] http/wpt/webauthn/public-key-credential-create-success-u2f.https.html is failing
index a356151..eaf89e3 100644 (file)
@@ -4113,9 +4113,12 @@ void FrameView::paintContents(GraphicsContext& context, const IntRect& dirtyRect
         fillWithWarningColor = false; // Element images are transparent, don't fill with red.
     else
         fillWithWarningColor = true;
-    
-    if (fillWithWarningColor)
-        context.fillRect(dirtyRect, Color(255, 64, 255));
+
+    if (fillWithWarningColor) {
+        IntRect debugRect = frameRect();
+        debugRect.intersect(dirtyRect);
+        context.fillRect(debugRect, Color(255, 64, 255));
+    }
 #endif
 
     RenderView* renderView = this->renderView();
index ee2738f..c0cbd1f 100644 (file)
@@ -1635,6 +1635,7 @@ void GraphicsContext::drawLinesForText(const FloatPoint& point, float thickness,
 
 void GraphicsContext::setURLForRect(const URL& link, const FloatRect& destRect)
 {
+    // FIXME: <rdar://problem/54900133> PDF exporting on iOS should include URL rects
 #if !PLATFORM(IOS_FAMILY)
     if (paintingDisabled())
         return;
index 2e3c023..7fcb28f 100644 (file)
@@ -67,6 +67,7 @@ enum class PaintBehavior : uint16_t {
     FlattenCompositingLayers    = 1 << 9, // Paint doesn't stop at compositing layer boundaries.
     Snapshotting                = 1 << 10,
     TileFirstPaint              = 1 << 11,
+    AnnotateLinks               = 1 << 12, // Collect all renderers with links to annotate their URLs (e.g. PDFs)
 };
 
 } // namespace WebCore
index 2d062b5..0357413 100644 (file)
@@ -2033,7 +2033,7 @@ void RenderElement::updateOutlineAutoAncestor(bool hasOutlineAuto)
 
 bool RenderElement::hasOutlineAnnotation() const
 {
-    return element() && element()->isLink() && document().printing();
+    return element() && element()->isLink() && (document().printing() || (view().frameView().paintBehavior() & PaintBehavior::AnnotateLinks));
 }
 
 bool RenderElement::hasSelfPaintingLayer() const
index 0282121..6e31e7a 100644 (file)
@@ -1,3 +1,41 @@
+2019-09-10  Brady Eidson  <beidson@apple.com>
+
+        Add SPI to save a PDF from the contents of a WKWebView.
+        <rdar://problem/48955900> and https://bugs.webkit.org/show_bug.cgi?id=195765
+
+        Reviewed by Tim Horton.
+
+        This is refactoring a combination of "snapshotFirstPage" PDF printing code and the
+        "takeSnapshot" API code to capture the on-screen visible page to a PDF at full fidelity.
+
+        * UIProcess/API/Cocoa/WKWebView.mm:
+        (-[WKWebView _takePDFSnapshotWithConfiguration:completionHandler:]):
+        * UIProcess/API/Cocoa/WKWebViewPrivate.h:
+
+        * UIProcess/WebPageProxy.cpp:
+        (WebKit::WebPageProxy::drawToPDF):
+        (WebKit::WebPageProxy::drawToPDFCallback):
+        * UIProcess/WebPageProxy.h:
+        * UIProcess/WebPageProxy.messages.in:
+
+        * UIProcess/ios/WKContentView.mm:
+        (-[WKContentView _wk_pageCountForPrintFormatter:]):
+
+        * UIProcess/ios/WebPageProxyIOS.mm:
+        (WebKit::WebPageProxy::drawToPDFCallback): Move to cross platform WebPageProxy.
+
+        * WebProcess/WebPage/Cocoa/WebPageCocoa.mm:
+        (WebKit::WebPage::pdfSnapshotAtSize):
+
+        * WebProcess/WebPage/WebPage.cpp:
+        (WebKit::WebPage::pdfSnapshotAtSize): Instead of assuming 1 page capped at 200 inches,
+          paginate every 200 inches.
+        (WebKit::WebPage::drawToPDF):
+        (WebKit::paintSnapshotAtSize): Deleted.
+        (WebKit::WebPage::pdfSnapshotAtSize): Deleted.
+        * WebProcess/WebPage/WebPage.h:
+        * WebProcess/WebPage/WebPage.messages.in:
+
 2019-09-10  Chris Dumez  <cdumez@apple.com>
 
         Hangs on Swiss.com due to the web process being blocked on StorageAreaMap::LoadValuesIfNeeded
index 8fa9f86..7dca9eb 100644 (file)
@@ -5037,6 +5037,32 @@ FOR_EACH_PRIVATE_WKCONTENTVIEW_ACTION(FORWARD_ACTION_TO_WKCONTENTVIEW)
     });
 }
 
+- (void)_takePDFSnapshotWithConfiguration:(WKSnapshotConfiguration *)snapshotConfiguration completionHandler:(void (^)(NSData *, NSError *))completionHandler
+{
+    WebCore::FrameIdentifier frameID;
+    if (auto mainFrame = _page->mainFrame())
+        frameID = mainFrame->frameID();
+    else {
+        completionHandler(nil, createNSError(WKErrorUnknown).get());
+        return;
+    }
+
+    Optional<WebCore::FloatRect> floatRect;
+    if (snapshotConfiguration && !CGRectIsNull(snapshotConfiguration.rect))
+        floatRect = WebCore::FloatRect(snapshotConfiguration.rect);
+
+    auto handler = makeBlockPtr(completionHandler);
+    _page->drawToPDF(frameID, floatRect, [retainedSelf = retainPtr(self), handler = WTFMove(handler)](const IPC::DataReference& pdfData, WebKit::CallbackBase::Error error) {
+        if (error != WebKit::CallbackBase::Error::None) {
+            handler(nil, createNSError(WKErrorUnknown).get());
+            return;
+        }
+
+        auto data = adoptCF(CFDataCreate(kCFAllocatorDefault, pdfData.data(), pdfData.size()));
+        handler((NSData *)data.get(), nil);
+    });
+}
+
 #if PLATFORM(MAC)
 - (void)_setShouldSuppressFirstResponderChanges:(BOOL)shouldSuppress
 {
index bd5eb7f..2c6b5cb 100644 (file)
@@ -432,6 +432,7 @@ typedef NS_OPTIONS(NSUInteger, _WKRectEdge) {
 - (void)_requestTextInputContextsInRect:(CGRect)rect completionHandler:(void(^)(NSArray<_WKTextInputContext *> *))completionHandler WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA));
 - (void)_focusTextInputContext:(_WKTextInputContext *)textInputElement completionHandler:(void(^)(BOOL))completionHandler WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA));
 
+- (void)_takePDFSnapshotWithConfiguration:(WKSnapshotConfiguration *)snapshotConfiguration completionHandler:(void (^)(NSData *pdfSnapshotData, NSError *error))completionHandler WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA));
 @end
 
 #if TARGET_OS_IPHONE && !TARGET_OS_WATCH
index e5f2a6e..c3c5a64 100644 (file)
@@ -7783,6 +7783,26 @@ void WebPageProxy::drawPagesForPrinting(WebFrameProxy* frame, const PrintInfo& p
 }
 #endif
 
+#if PLATFORM(COCOA)
+void WebPageProxy::drawToPDF(FrameIdentifier frameID, const Optional<FloatRect>& rect, DrawToPDFCallback::CallbackFunction&& callback)
+{
+    if (!hasRunningProcess()) {
+        callback(IPC::DataReference(), CallbackBase::Error::OwnerWasInvalidated);
+        return;
+    }
+
+    auto callbackID = m_callbacks.put(WTFMove(callback), m_process->throttler().backgroundActivityToken());
+    m_process->send(Messages::WebPage::DrawToPDF(frameID, rect, callbackID), m_webPageID);
+}
+
+void WebPageProxy::drawToPDFCallback(const IPC::DataReference& pdfData, CallbackID callbackID)
+{
+    auto callback = m_callbacks.take<DrawToPDFCallback>(callbackID);
+    RELEASE_ASSERT(callback);
+    callback->performCallbackWithReturnValue(pdfData);
+}
+#endif // PLATFORM(COCOA)
+
 void WebPageProxy::updateBackingStoreDiscardableState()
 {
     ASSERT(hasRunningProcess());
index 7d5b59c..6abe827 100644 (file)
@@ -360,11 +360,10 @@ struct ElementDidFocusArguments {
     OptionSet<WebCore::ActivityState::Flag> activityStateChanges;
     RefPtr<API::Object> userData;
 };
-
-using DrawToPDFCallback = GenericCallback<const IPC::DataReference&>;
 #endif
 
 #if PLATFORM(COCOA)
+using DrawToPDFCallback = GenericCallback<const IPC::DataReference&>;
 typedef GenericCallback<const WTF::MachSendRight&> MachSendRightCallback;
 typedef GenericCallback<bool, bool, String, double, double, uint64_t> NowPlayingInfoCallback;
 #endif
@@ -1172,9 +1171,10 @@ public:
 #if PLATFORM(COCOA)
     void drawRectToImage(WebFrameProxy*, const PrintInfo&, const WebCore::IntRect&, const WebCore::IntSize&, Ref<ImageCallback>&&);
     void drawPagesToPDF(WebFrameProxy*, const PrintInfo&, uint32_t first, uint32_t count, Ref<DataCallback>&&);
+    void drawToPDF(WebCore::FrameIdentifier, const Optional<WebCore::FloatRect>&, DrawToPDFCallback::CallbackFunction&&);
+    void drawToPDFCallback(const IPC::DataReference& pdfData, WebKit::CallbackID);
 #if PLATFORM(IOS_FAMILY)
     uint32_t computePagesForPrintingAndDrawToPDF(WebCore::FrameIdentifier, const PrintInfo&, DrawToPDFCallback::CallbackFunction&&);
-    void drawToPDFCallback(const IPC::DataReference& pdfData, WebKit::CallbackID);
 #endif
 #elif PLATFORM(GTK)
     void drawPagesForPrinting(WebFrameProxy*, const PrintInfo&, Ref<PrintFinishedCallback>&&);
index 93d3414..1344a51 100644 (file)
@@ -196,7 +196,6 @@ messages -> WebPageProxy {
     DidCompleteSyntheticClick()
     DisableDoubleTapGesturesDuringTapIfNecessary(uint64_t requestID)
     HandleSmartMagnificationInformationForPotentialTap(uint64_t requestID, WebCore::FloatRect renderRect, bool fitEntireRect, double viewportMinimumScale, double viewportMaximumScale)
-    DrawToPDFCallback(IPC::DataReference pdfData, WebKit::CallbackID callbackID)
     SelectionRectsCallback(Vector<WebCore::SelectionRect> selectionRects, WebKit::CallbackID callbackID);
 #endif
 #if ENABLE(DATA_DETECTION)
@@ -206,6 +205,7 @@ messages -> WebPageProxy {
     PrintFinishedCallback(WebCore::ResourceError error, WebKit::CallbackID callbackID)
 #endif
 #if PLATFORM(COCOA)
+    DrawToPDFCallback(IPC::DataReference pdfData, WebKit::CallbackID callbackID)
     MachSendRightCallback(MachSendRight sendRight, WebKit::CallbackID callbackID)
     NowPlayingInfoCallback(bool active, bool registeredAsNowPlayingApplication, String title, double duration, double elapsedTime, uint64_t uniqueIdentifier, WebKit::CallbackID callbackID)
 #endif
index 3822059..5939b39 100644 (file)
@@ -720,6 +720,8 @@ static void storeAccessibilityRemoteConnectionInformation(id element, pid_t pid,
     WebKit::PrintInfo printInfo;
     printInfo.pageSetupScaleFactor = 1;
     printInfo.snapshotFirstPage = printFormatter.snapshotFirstPage;
+
+    // FIXME: Paginate when exporting PDFs taller than 200"
     if (printInfo.snapshotFirstPage) {
         static const CGFloat maximumPDFHeight = 200 * 72; // maximum PDF height for a single page is 200 inches
         CGSize contentSize = self.bounds.size;
index 0d8d8fd..baf8d36 100644 (file)
@@ -1086,13 +1086,6 @@ uint32_t WebPageProxy::computePagesForPrintingAndDrawToPDF(FrameIdentifier frame
     return pageCount;
 }
 
-void WebPageProxy::drawToPDFCallback(const IPC::DataReference& pdfData, CallbackID callbackID)
-{
-    auto callback = m_callbacks.take<DrawToPDFCallback>(callbackID);
-    RELEASE_ASSERT(callback);
-    callback->performCallbackWithReturnValue(pdfData);
-}
-
 void WebPageProxy::contentSizeCategoryDidChange(const String& contentSizeCategory)
 {
     process().send(Messages::WebPage::ContentSizeCategoryDidChange(contentSizeCategory), m_webPageID);
index 85a43b5..bc14be4 100644 (file)
@@ -232,7 +232,58 @@ void WebPage::updateMockAccessibilityElementAfterCommittingLoad()
     auto* document = mainFrame()->document();
     [m_mockAccessibilityElement setHasMainFramePlugin:document ? document->isPluginDocument() : false];
 }
-    
+
+RetainPtr<CFDataRef> WebPage::pdfSnapshotAtSize(IntRect rect, IntSize bitmapSize, SnapshotOptions options)
+{
+    Frame* coreFrame = m_mainFrame->coreFrame();
+    if (!coreFrame)
+        return nullptr;
+
+    FrameView* frameView = coreFrame->view();
+    if (!frameView)
+        return nullptr;
+
+    auto data = adoptCF(CFDataCreateMutable(kCFAllocatorDefault, 0));
+
+    auto dataConsumer = adoptCF(CGDataConsumerCreateWithCFData(data.get()));
+    auto mediaBox = CGRectMake(0, 0, bitmapSize.width(), bitmapSize.height());
+    auto pdfContext = adoptCF(CGPDFContextCreate(dataConsumer.get(), &mediaBox, nullptr));
+
+    int64_t remainingHeight = bitmapSize.height();
+    int64_t nextRectY = rect.y();
+    while (remainingHeight > 0) {
+        // PDFs have a per-page height limit of 200 inches at 72dpi.
+        // We'll export one PDF page at a time, up to that maximum height.
+        static const int64_t maxPageHeight = 72 * 200;
+        bitmapSize.setHeight(std::min(remainingHeight, maxPageHeight));
+        rect.setHeight(bitmapSize.height());
+        rect.setY(nextRectY);
+
+        CGRect mediaBox = CGRectMake(0, 0, bitmapSize.width(), bitmapSize.height());
+        auto mediaBoxData = adoptCF(CFDataCreate(NULL, (const UInt8 *)&mediaBox, sizeof(CGRect)));
+        auto dictionary = (CFDictionaryRef)@{
+            (NSString *)kCGPDFContextMediaBox : (NSData *)mediaBoxData.get()
+        };
+
+        CGPDFContextBeginPage(pdfContext.get(), dictionary);
+
+        GraphicsContext graphicsContext { pdfContext.get() };
+        graphicsContext.scale({ 1, -1 });
+        graphicsContext.translate(0, -bitmapSize.height());
+
+        paintSnapshotAtSize(rect, bitmapSize, options, *coreFrame, *frameView, graphicsContext);
+
+        CGPDFContextEndPage(pdfContext.get());
+
+        nextRectY += bitmapSize.height();
+        remainingHeight -= maxPageHeight;
+    }
+
+    CGPDFContextClose(pdfContext.get());
+
+    return data;
+}
+
 } // namespace WebKit
 
 #endif // PLATFORM(COCOA)
index 66fe601..cec5644 100644 (file)
@@ -2312,7 +2312,7 @@ RefPtr<WebImage> WebPage::scaledSnapshotWithOptions(const IntRect& rect, double
     return snapshotAtSize(rect, bitmapSize, options);
 }
 
-static void paintSnapshotAtSize(const IntRect& rect, const IntSize& bitmapSize, SnapshotOptions options, Frame& frame, FrameView& frameView, GraphicsContext& graphicsContext)
+void WebPage::paintSnapshotAtSize(const IntRect& rect, const IntSize& bitmapSize, SnapshotOptions options, Frame& frame, FrameView& frameView, GraphicsContext& graphicsContext)
 {
     IntRect snapshotRect = rect;
     float horizontalScaleFactor = static_cast<float>(bitmapSize.width()) / rect.width();
@@ -2386,39 +2386,6 @@ RefPtr<WebImage> WebPage::snapshotAtSize(const IntRect& rect, const IntSize& bit
     return snapshot;
 }
 
-#if USE(CF)
-RetainPtr<CFDataRef> WebPage::pdfSnapshotAtSize(const IntRect& rect, const IntSize& bitmapSize, SnapshotOptions options)
-{
-    Frame* coreFrame = m_mainFrame->coreFrame();
-    if (!coreFrame)
-        return nullptr;
-
-    FrameView* frameView = coreFrame->view();
-    if (!frameView)
-        return nullptr;
-
-    auto data = adoptCF(CFDataCreateMutable(kCFAllocatorDefault, 0));
-
-#if USE(CG)
-    auto dataConsumer = adoptCF(CGDataConsumerCreateWithCFData(data.get()));
-    auto mediaBox = CGRectMake(0, 0, bitmapSize.width(), bitmapSize.height());
-    auto pdfContext = adoptCF(CGPDFContextCreate(dataConsumer.get(), &mediaBox, nullptr));
-
-    CGPDFContextBeginPage(pdfContext.get(), nullptr);
-
-    GraphicsContext graphicsContext { pdfContext.get() };
-    graphicsContext.scale({ 1, -1 });
-    graphicsContext.translate(0, -bitmapSize.height());
-    paintSnapshotAtSize(rect, bitmapSize, options, *coreFrame, *frameView, graphicsContext);
-
-    CGPDFContextEndPage(pdfContext.get());
-    CGPDFContextClose(pdfContext.get());
-#endif
-
-    return data;
-}
-#endif
-
 RefPtr<WebImage> WebPage::snapshotNode(WebCore::Node& node, SnapshotOptions options, unsigned maximumPixelCount)
 {
     Frame* coreFrame = m_mainFrame->coreFrame();
@@ -4755,6 +4722,34 @@ void WebPage::computePagesForPrintingImpl(FrameIdentifier frameID, const PrintIn
 }
 
 #if PLATFORM(COCOA)
+void WebPage::drawToPDF(FrameIdentifier frameID, const Optional<FloatRect>& rect, CallbackID callbackID)
+{
+    auto& frameView = *m_page->mainFrame().view();
+    IntSize snapshotSize;
+    if (rect)
+        snapshotSize = IntSize(rect->size());
+    else
+        snapshotSize = { frameView.contentsSize() };
+
+    IntRect snapshotRect;
+    if (rect)
+        snapshotRect = { {(int)rect->x(), (int)rect->y()}, snapshotSize };
+    else
+        snapshotRect = { {0, 0}, snapshotSize };
+
+    auto originalLayoutViewportOverrideRect = frameView.layoutViewportOverrideRect();
+    frameView.setLayoutViewportOverrideRect(LayoutRect(snapshotRect));
+    auto originalPaintBehavior = frameView.paintBehavior();
+    frameView.setPaintBehavior(originalPaintBehavior | PaintBehavior::AnnotateLinks);
+
+    auto pdfData = pdfSnapshotAtSize(snapshotRect, snapshotSize, 0);
+
+    frameView.setLayoutViewportOverrideRect(originalLayoutViewportOverrideRect);
+    frameView.setPaintBehavior(originalPaintBehavior);
+
+    send(Messages::WebPageProxy::DrawToPDFCallback(IPC::DataReference(CFDataGetBytePtr(pdfData.get()), CFDataGetLength(pdfData.get())), callbackID));
+}
+
 void WebPage::drawRectToImage(FrameIdentifier frameID, const PrintInfo& printInfo, const IntRect& rect, const WebCore::IntSize& imageSize, CallbackID callbackID)
 {
     WebFrame* frame = WebProcess::singleton().webFrame(frameID);
index a64bdf1..92a2423 100644 (file)
@@ -881,6 +881,8 @@ public:
     void computePagesForPrintingAndDrawToPDF(WebCore::FrameIdentifier, const PrintInfo&, CallbackID, Messages::WebPage::ComputePagesForPrintingAndDrawToPDF::DelayedReply&&);
 #endif
 
+    void drawToPDF(WebCore::FrameIdentifier, const Optional<WebCore::FloatRect>&, CallbackID);
+
 #if PLATFORM(GTK)
     void drawPagesForPrinting(WebCore::FrameIdentifier, const PrintInfo&, CallbackID);
     void didFinishPrintOperation(const WebCore::ResourceError&, CallbackID);
@@ -1606,8 +1608,8 @@ private:
 
     RefPtr<WebImage> snapshotAtSize(const WebCore::IntRect&, const WebCore::IntSize& bitmapSize, SnapshotOptions);
     RefPtr<WebImage> snapshotNode(WebCore::Node&, SnapshotOptions, unsigned maximumPixelCount = std::numeric_limits<unsigned>::max());
-#if USE(CF)
-    RetainPtr<CFDataRef> pdfSnapshotAtSize(const WebCore::IntRect&, const WebCore::IntSize& bitmapSize, SnapshotOptions);
+#if PLATFORM(COCOA)
+    RetainPtr<CFDataRef> pdfSnapshotAtSize(WebCore::IntRect, WebCore::IntSize bitmapSize, SnapshotOptions);
 #endif
 
 #if ENABLE(ATTACHMENT_ELEMENT)
@@ -1622,6 +1624,8 @@ private:
 
     void updateMockAccessibilityElementAfterCommittingLoad();
 
+    void paintSnapshotAtSize(const WebCore::IntRect&, const WebCore::IntSize&, SnapshotOptions, WebCore::Frame&, WebCore::FrameView&, WebCore::GraphicsContext&);
+
     WebCore::PageIdentifier m_identifier;
 
     std::unique_ptr<WebCore::Page> m_page;
index 2d5d20c..9d4f93e 100644 (file)
@@ -389,6 +389,7 @@ GenerateSyntheticEditingCommand(enum:uint8_t WebKit::SyntheticEditingCommandType
 #if PLATFORM(IOS_FAMILY)
     ComputePagesForPrintingAndDrawToPDF(WebCore::FrameIdentifier frameID, struct WebKit::PrintInfo printInfo, WebKit::CallbackID callbackID) -> (uint32_t pageCount) Synchronous
 #endif
+    DrawToPDF(WebCore::FrameIdentifier frameID, Optional<WebCore::FloatRect> rect, WebKit::CallbackID callbackID)
 #endif
 #if PLATFORM(GTK)
     DrawPagesForPrinting(WebCore::FrameIdentifier frameID, struct WebKit::PrintInfo printInfo, WebKit::CallbackID callbackID)
index 04d85d0..bc39760 100644 (file)
@@ -1,3 +1,54 @@
+2019-09-10  Brady Eidson  <beidson@apple.com>
+
+        Add SPI to save a PDF from the contents of a WKWebView.
+        <rdar://problem/48955900> and https://bugs.webkit.org/show_bug.cgi?id=195765
+
+        Reviewed by Tim Horton.
+
+        -Add an "Export to PDF..." menu option to MiniBrowser.
+        -Add API tests for the API itself.
+
+        * MiniBrowser/MiniBrowser.entitlements:
+
+        * MiniBrowser/mac/BrowserWindowController.h:
+        * MiniBrowser/mac/BrowserWindowController.m:
+        (-[BrowserWindowController forceRepaint:]):
+        (-[BrowserWindowController saveAsPDF:]):
+
+        * MiniBrowser/mac/MainMenu.xib:
+
+        * MiniBrowser/mac/WK1BrowserWindowController.m:
+        (-[WK1BrowserWindowController validateMenuItem:]):
+        * MiniBrowser/mac/WK2BrowserWindowController.m:
+        (-[WK2BrowserWindowController validateMenuItem:]):
+        (-[WK2BrowserWindowController saveAsPDF:]):
+
+        * TestWebKitAPI/Configurations/Base.xcconfig:
+        * TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
+        * TestWebKitAPI/Tests/WebKitCocoa/PDFSnapshot.mm: Added.
+        (TestWebKitAPI::TEST):
+        * TestWebKitAPI/cocoa/TestPDFDocument.h: Added.
+        * TestWebKitAPI/cocoa/TestPDFDocument.mm: Added.
+        (TestWebKitAPI::toCGRect):
+        (TestWebKitAPI::toPlatformPoint):
+        (TestWebKitAPI::TestPDFAnnotation::TestPDFAnnotation):
+        (TestWebKitAPI::TestPDFAnnotation::isLink const):
+        (TestWebKitAPI::TestPDFAnnotation::bounds const):
+        (TestWebKitAPI::TestPDFAnnotation::linkURL const):
+        (TestWebKitAPI::TestPDFPage::create):
+        (TestWebKitAPI::TestPDFPage::TestPDFPage):
+        (TestWebKitAPI::TestPDFPage::annotations):
+        (TestWebKitAPI::TestPDFPage::characterCount const):
+        (TestWebKitAPI::TestPDFPage::text const):
+        (TestWebKitAPI::TestPDFPage::rectForCharacterAtIndex const):
+        (TestWebKitAPI::TestPDFPage::characterIndexAtPoint const):
+        (TestWebKitAPI::TestPDFPage::bounds const):
+        (TestWebKitAPI::TestPDFPage::colorAtPoint const):
+        (TestWebKitAPI::TestPDFDocument::createFromData):
+        (TestWebKitAPI::TestPDFDocument::TestPDFDocument):
+        (TestWebKitAPI::TestPDFDocument::pageCount const):
+        (TestWebKitAPI::TestPDFDocument::page):
+
 2019-09-10  Keith Rollin  <krollin@apple.com>
 
         Remove SSL-based TLSDeprecation.mm contents
index c9ea376..e4c2ba1 100644 (file)
@@ -2,20 +2,20 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
+       <key>com.apple.security.app-sandbox</key>
+       <true/>
        <key>com.apple.security.device.usb</key>
        <true/>
-       <key>com.apple.security.files.user-selected.read-only</key>
+       <key>com.apple.security.files.user-selected.read-write</key>
+       <true/>
+       <key>com.apple.security.network.client</key>
        <true/>
+       <key>com.apple.security.temporary-exception.files.absolute-path.read-only</key>
+       <string>/</string>
        <key>com.apple.security.temporary-exception.mach-lookup.global-name</key>
        <array>
                <string>com.apple.Safari.SafeBrowsing.Service</string>
                <string>com.apple.WebKit.NetworkingDaemon</string>
        </array>
-       <key>com.apple.security.app-sandbox</key>
-       <true/>
-       <key>com.apple.security.network.client</key>
-       <true/>
-       <key>com.apple.security.temporary-exception.files.absolute-path.read-only</key>
-       <string>/</string>
 </dict>
 </plist>
index c5b9d54..dfdb640 100644 (file)
@@ -46,6 +46,8 @@
 
 - (IBAction)openLocation:(id)sender;
 
+- (IBAction)saveAsPDF:(id)sender;
+
 - (IBAction)fetch:(id)sender;
 - (IBAction)share:(id)sender;
 - (IBAction)reload:(id)sender;
index 4b99057..4d45739 100644 (file)
     [self doesNotRecognizeSelector:_cmd];
 }
 
+- (IBAction)saveAsPDF:(id)sender
+{
+    [self doesNotRecognizeSelector:_cmd];
+}
+
 - (IBAction)goBack:(id)sender
 {
     [self doesNotRecognizeSelector:_cmd];
index 546a60f..0716346 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="13771" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none">
+<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none">
     <dependencies>
-        <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="13771"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
     </dependencies>
     <objects>
         <customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
                                     <action selector="saveDocumentAs:" target="-1" id="363"/>
                                 </connections>
                             </menuItem>
+                            <menuItem title="Save As PDF…" keyEquivalent="S" id="gmS-3Q-oLs">
+                                <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
+                                <connections>
+                                    <action selector="saveAsPDF:" target="-1" id="25T-Id-334"/>
+                                </connections>
+                            </menuItem>
                             <menuItem title="Revert to Saved" id="112">
                                 <modifierMask key="keyEquivalentModifierMask"/>
                                 <connections>
index 7ed6c3d..6d66bfb 100644 (file)
@@ -134,6 +134,9 @@ static BOOL areEssentiallyEqual(double a, double b)
 {
     SEL action = [menuItem action];
 
+    if (action == @selector(saveAsPDF:))
+        return NO;
+
     if (action == @selector(zoomIn:))
         return [self canZoomIn];
     if (action == @selector(zoomOut:))
index f78dbdf..cf4d3af 100644 (file)
@@ -207,6 +207,9 @@ static BOOL areEssentiallyEqual(double a, double b)
 {
     SEL action = menuItem.action;
 
+    if (action == @selector(saveAsPDF:))
+        return YES;
+
     if (action == @selector(zoomIn:))
         return [self canZoomIn];
     if (action == @selector(zoomOut:))
@@ -814,4 +817,17 @@ static NSSet *dataTypes()
     decisionHandler(false);
 }
 
+- (IBAction)saveAsPDF:(id)sender
+{
+    NSSavePanel *panel = [NSSavePanel savePanel];
+    panel.allowedFileTypes = @[ @"pdf" ];
+    [panel beginSheetModalForWindow:self.window completionHandler:^(NSInteger result) {
+        if (result == NSModalResponseOK) {
+            [_webView _takePDFSnapshotWithConfiguration:nil completionHandler:^(NSData *pdfSnapshotData, NSError *error) {
+                [pdfSnapshotData writeToURL:[panel URL] options:0 error:nil];
+            }];
+        }
+    }];
+}
+
 @end
index 89c6b1e..3fa3bab 100644 (file)
@@ -113,3 +113,5 @@ WK_COCOA_TOUCH_watchos = cocoatouch;
 WK_COCOA_TOUCH_watchsimulator = cocoatouch;
 WK_COCOA_TOUCH_appletvos = cocoatouch;
 WK_COCOA_TOUCH_appletvsimulator = cocoatouch;
+
+SYSTEM_FRAMEWORK_SEARCH_PATHS = $(SDKROOT)$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks $(SDKROOT)$(SYSTEM_LIBRARY_DIR)/Frameworks $(SDKROOT)$(SYSTEM_LIBRARY_DIR)/Frameworks/Quartz.framework/Frameworks $(inherited);
index d537d2c..c0ae05d 100644 (file)
                514958BE1F7427AC00E87BAD /* WKWebViewAutofillTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 514958BD1F7427AC00E87BAD /* WKWebViewAutofillTests.mm */; };
                515BE16F1D428BB100DD7C68 /* StoreBlobToBeDeleted.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 515BE16E1D4288FF00DD7C68 /* StoreBlobToBeDeleted.html */; };
                515BE1711D428E4B00DD7C68 /* StoreBlobThenDelete.mm in Sources */ = {isa = PBXBuildFile; fileRef = 515BE1701D428BD100DD7C68 /* StoreBlobThenDelete.mm */; };
+               516281252325C18000BB7E42 /* TestPDFDocument.mm in Sources */ = {isa = PBXBuildFile; fileRef = 516281242325C17B00BB7E42 /* TestPDFDocument.mm */; };
+               516281272325C19800BB7E42 /* PDFSnapshot.mm in Sources */ = {isa = PBXBuildFile; fileRef = 516281262325C19100BB7E42 /* PDFSnapshot.mm */; };
+               516281292325C45400BB7E42 /* PDFKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 516281282325C45400BB7E42 /* PDFKit.framework */; };
                5165FE04201EE620009F7EC3 /* MessagePortProviders.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5165FE03201EE617009F7EC3 /* MessagePortProviders.mm */; };
                51714EB41CF8C78C004723C4 /* WebProcessKillIDBCleanup-1.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 51714EB21CF8C761004723C4 /* WebProcessKillIDBCleanup-1.html */; };
                51714EB51CF8C78C004723C4 /* WebProcessKillIDBCleanup-2.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 51714EB31CF8C761004723C4 /* WebProcessKillIDBCleanup-2.html */; };
                514958BD1F7427AC00E87BAD /* WKWebViewAutofillTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = WKWebViewAutofillTests.mm; sourceTree = "<group>"; };
                515BE16E1D4288FF00DD7C68 /* StoreBlobToBeDeleted.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = StoreBlobToBeDeleted.html; sourceTree = "<group>"; };
                515BE1701D428BD100DD7C68 /* StoreBlobThenDelete.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = StoreBlobThenDelete.mm; sourceTree = "<group>"; };
+               516281232325C17A00BB7E42 /* TestPDFDocument.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TestPDFDocument.h; path = cocoa/TestPDFDocument.h; sourceTree = "<group>"; };
+               516281242325C17B00BB7E42 /* TestPDFDocument.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = TestPDFDocument.mm; path = cocoa/TestPDFDocument.mm; sourceTree = "<group>"; };
+               516281262325C19100BB7E42 /* PDFSnapshot.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PDFSnapshot.mm; sourceTree = "<group>"; };
+               516281282325C45400BB7E42 /* PDFKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PDFKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/PDFKit.framework; sourceTree = DEVELOPER_DIR; };
                5165FE03201EE617009F7EC3 /* MessagePortProviders.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MessagePortProviders.mm; sourceTree = "<group>"; };
                51714EB21CF8C761004723C4 /* WebProcessKillIDBCleanup-1.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "WebProcessKillIDBCleanup-1.html"; sourceTree = "<group>"; };
                51714EB31CF8C761004723C4 /* WebProcessKillIDBCleanup-2.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "WebProcessKillIDBCleanup-2.html"; sourceTree = "<group>"; };
                                5CFACF63226F73C60056C7D0 /* libboringssl.a in Frameworks */,
                                7C83E03F1D0A61A000FEBCF3 /* libicucore.dylib in Frameworks */,
                                578CBD67204FB2C80083B9F2 /* LocalAuthentication.framework in Frameworks */,
+                               516281292325C45400BB7E42 /* PDFKit.framework in Frameworks */,
                                7A010BCD1D877C0D00EDE72A /* QuartzCore.framework in Frameworks */,
                                574F55D2204D47F0002948C6 /* Security.framework in Frameworks */,
                        );
                                5CE7594722A883A500C12409 /* TestContextMenuDriver.mm */,
                                2D1C04A51D76298B000A6816 /* TestNavigationDelegate.h */,
                                2D1C04A61D76298B000A6816 /* TestNavigationDelegate.mm */,
+                               516281232325C17A00BB7E42 /* TestPDFDocument.h */,
+                               516281242325C17B00BB7E42 /* TestPDFDocument.mm */,
                                A14FC58D1B8AE36500D107EB /* TestProtocol.h */,
                                A14FC58E1B8AE36500D107EB /* TestProtocol.mm */,
                                2EFF06D21D8AEDBB0004BB30 /* TestWKWebView.h */,
                                9BDD95561F83683600D20C60 /* PasteRTFD.mm */,
                                9B2346411F943A2400DB1D23 /* PasteWebArchive.mm */,
                                51D8C18F2267B26700797E40 /* PDFLinkReferrer.mm */,
+                               516281262325C19100BB7E42 /* PDFSnapshot.mm */,
                                3FCC4FE41EC4E8520076E37C /* PictureInPictureDelegate.mm */,
                                83BAEE8C1EF4625500DDE894 /* PluginLoadClientPolicies.mm */,
                                C95501BE19AD2FAF0049BE3E /* Preferences.mm */,
                                4135FB862011FABF00332139 /* libWebCoreTestSupport.dylib */,
                                7C83E0291D0A5CDF00FEBCF3 /* libWTF.a */,
                                578CBD66204FB2C70083B9F2 /* LocalAuthentication.framework */,
+                               516281282325C45400BB7E42 /* PDFKit.framework */,
                                7A010BCC1D877C0D00EDE72A /* QuartzCore.framework */,
                                574F55D0204D471C002948C6 /* Security.framework */,
                        );
                                9BDD95581F83683600D20C60 /* PasteRTFD.mm in Sources */,
                                9B2346421F943E2700DB1D23 /* PasteWebArchive.mm in Sources */,
                                51D8C1902267B26D00797E40 /* PDFLinkReferrer.mm in Sources */,
+                               516281272325C19800BB7E42 /* PDFSnapshot.mm in Sources */,
                                7C83E0531D0A643A00FEBCF3 /* PendingAPIRequestURL.cpp in Sources */,
                                3FCC4FE51EC4E8520076E37C /* PictureInPictureDelegate.mm in Sources */,
                                7CCE7EA61A411A0F00447C4C /* PlatformUtilitiesMac.mm in Sources */,
                                F4F5BB5221667BAA002D06B9 /* TestFontOptions.mm in Sources */,
                                F45E15762112CE6200307E82 /* TestInputDelegate.mm in Sources */,
                                F45D3891215A7B4B002A2979 /* TestInspectorBar.mm in Sources */,
+                               516281252325C18000BB7E42 /* TestPDFDocument.mm in Sources */,
                                5774AA6821FBBF7800AF2A1B /* TestSOAuthorization.mm in Sources */,
                                F4CD74C920FDB49600DE3794 /* TestURLSchemeHandler.mm in Sources */,
                                F4517B672054C49500C26721 /* TestWKWebViewController.mm in Sources */,
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/PDFSnapshot.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/PDFSnapshot.mm
new file mode 100644 (file)
index 0000000..8422f32
--- /dev/null
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2019 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "config.h"
+
+#import "PlatformUtilities.h"
+#import "Test.h"
+#import "TestPDFDocument.h"
+#import "TestWKWebView.h"
+#import <WebCore/Color.h>
+#import <WebKit/WKWebViewPrivate.h>
+
+using WebCore::Color;
+
+namespace TestWebKitAPI {
+
+TEST(PDFSnapshot, FullContent)
+{
+    static bool didTakeSnapshot;
+
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600)]);
+
+    [webView synchronouslyLoadHTMLString:@"<meta name='viewport' content='width=device-width'><body bgcolor=#00ff00>Hello</body>"];
+
+    [webView _takePDFSnapshotWithConfiguration:nil completionHandler:^(NSData *pdfSnapshotData, NSError *error) {
+        EXPECT_NULL(error);
+        auto document = TestPDFDocument::createFromData(pdfSnapshotData);
+        EXPECT_EQ(document->pageCount(), 1u);
+        auto page = document->page(0);
+        EXPECT_NE(page, nullptr);
+        EXPECT_TRUE(CGRectEqualToRect(page->bounds(), CGRectMake(0, 0, 800, 600)));
+
+        EXPECT_EQ(page->characterCount(), 5u);
+        EXPECT_EQ(page->text()[0], 'H');
+        EXPECT_EQ(page->text()[4], 'o');
+
+        // The entire page should be green. Pick a point in the middle to check.
+        EXPECT_TRUE(page->colorAtPoint(400, 300) == Color::createUnchecked(0, 255, 0));
+
+        didTakeSnapshot = true;
+    }];
+
+    Util::run(&didTakeSnapshot);
+}
+
+TEST(PDFSnapshot, Subregions)
+{
+    static bool didTakeSnapshot;
+
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600)]);
+
+    [webView synchronouslyLoadHTMLString:@"<meta name='viewport' content='width=device-width'><body bgcolor=#00ff00>Hello</body>"];
+
+    // Snapshot a subregion contained entirely within the view
+    auto snapshotConfiguration = adoptNS([[WKSnapshotConfiguration alloc] init]);
+    [snapshotConfiguration setRect:NSMakeRect(200, 150, 400, 300)];
+    [snapshotConfiguration setSnapshotWidth:@400];
+
+    [webView _takePDFSnapshotWithConfiguration:snapshotConfiguration.get() completionHandler:^(NSData *pdfSnapshotData, NSError *error) {
+        EXPECT_NULL(error);
+        auto document = TestPDFDocument::createFromData(pdfSnapshotData);
+        EXPECT_EQ(document->pageCount(), 1u);
+        auto page = document->page(0);
+        EXPECT_NE(page, nullptr);
+        EXPECT_TRUE(CGRectEqualToRect(page->bounds(), CGRectMake(0, 0, 400, 300)));
+
+        EXPECT_EQ(page->characterCount(), 0u);
+
+        // The entire page should be green. Pick a point in the middle to check.
+        EXPECT_TRUE(page->colorAtPoint(200, 150) == Color::createUnchecked(0, 255, 0));
+
+        didTakeSnapshot = true;
+    }];
+
+    Util::run(&didTakeSnapshot);
+    didTakeSnapshot = false;
+
+    // Snapshot a region larger than the view
+    [snapshotConfiguration setRect:NSMakeRect(0, 0, 1200, 1200)];
+    [snapshotConfiguration setSnapshotWidth:@1200];
+
+    [webView _takePDFSnapshotWithConfiguration:snapshotConfiguration.get() completionHandler:^(NSData *pdfSnapshotData, NSError *error) {
+        EXPECT_NULL(error);
+        auto document = TestPDFDocument::createFromData(pdfSnapshotData);
+        EXPECT_EQ(document->pageCount(), 1u);
+        auto page = document->page(0);
+        EXPECT_NE(page, nullptr);
+        EXPECT_TRUE(CGRectEqualToRect(page->bounds(), CGRectMake(0, 0, 1200, 1200)));
+
+        // A pixel that was in the view should be green. Pick a point in the middle to check.
+        EXPECT_TRUE(page->colorAtPoint(200, 150) == Color::createUnchecked(0, 255, 0));
+
+        // A pixel that was outside the view should also be green (we extend background color out). Pick a point in the middle to check.
+        EXPECT_TRUE(page->colorAtPoint(900, 700) == Color::createUnchecked(0, 255, 0));
+
+        didTakeSnapshot = true;
+    }];
+
+    Util::run(&didTakeSnapshot);
+    didTakeSnapshot = false;
+
+}
+
+TEST(PDFSnapshot, Over200Inches)
+{
+    static bool didTakeSnapshot;
+
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 29400)]);
+
+    [webView synchronouslyLoadHTMLString:@"<meta name='viewport' content='width=device-width'><body bgcolor=#00ff00>Hello</body>"];
+
+    [webView _takePDFSnapshotWithConfiguration:nil completionHandler:^(NSData *pdfSnapshotData, NSError *error) {
+        EXPECT_NULL(error);
+        auto document = TestPDFDocument::createFromData(pdfSnapshotData);
+        EXPECT_EQ(document->pageCount(), 3u);
+
+        auto page = document->page(0);
+        EXPECT_NE(page, nullptr);
+        EXPECT_TRUE(CGRectEqualToRect(page->bounds(), CGRectMake(0, 0, 800, 14400)));
+        EXPECT_TRUE(page->colorAtPoint(400, 300) == Color::createUnchecked(0, 255, 0));
+        EXPECT_EQ(page->characterCount(), 5u);
+
+        page = document->page(1);
+        EXPECT_NE(page, nullptr);
+        EXPECT_TRUE(CGRectEqualToRect(page->bounds(), CGRectMake(0, 0, 800, 14400)));
+        EXPECT_TRUE(page->colorAtPoint(400, 300) == Color::createUnchecked(0, 255, 0));
+
+        EXPECT_EQ(page->characterCount(), 0u);
+
+        page = document->page(2);
+        EXPECT_NE(page, nullptr);
+        EXPECT_TRUE(CGRectEqualToRect(page->bounds(), CGRectMake(0, 0, 800, 600)));
+        EXPECT_TRUE(page->colorAtPoint(400, 300) == Color::createUnchecked(0, 255, 0));
+        EXPECT_EQ(page->characterCount(), 0u);
+
+        didTakeSnapshot = true;
+    }];
+
+    Util::run(&didTakeSnapshot);
+}
+
+TEST(PDFSnapshot, Links)
+{
+    static bool didTakeSnapshot;
+
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 15000)]);
+    [webView synchronouslyLoadHTMLString:@"<meta name='viewport' content='width=device-width'><div style=\"-webkit-line-box-contain: glyphs\"><a href=\"https://webkit.org/\">Click me</a></div>"];
+
+    [webView _takePDFSnapshotWithConfiguration:nil completionHandler:^(NSData *pdfSnapshotData, NSError *error) {
+        EXPECT_NULL(error);
+        auto document = TestPDFDocument::createFromData(pdfSnapshotData);
+        EXPECT_EQ(document->pageCount(), 2u);
+
+        auto page = document->page(0);
+        EXPECT_NE(page, nullptr);
+
+        EXPECT_TRUE(CGRectEqualToRect(page->bounds(), CGRectMake(0, 0, 800, 14400)));
+        EXPECT_TRUE(page->colorAtPoint(400, 300) == Color::createUnchecked(255, 255, 255));
+
+        EXPECT_EQ(page->characterCount(), 8u);
+        EXPECT_EQ(page->text()[0], 'C');
+        EXPECT_EQ(page->text()[7], 'e');
+
+        // FIXME: iOS doesn't have link annotations yet.
+#if PLATFORM(MAC)
+        auto annotations = page->annotations();
+        EXPECT_EQ(annotations.size(), 1u);
+        if (annotations.size()) {
+            EXPECT_TRUE(annotations[0].isLink());
+            EXPECT_TRUE([annotations[0].linkURL() isEqual:[NSURL URLWithString:@"https://webkit.org/"]]);
+
+            auto cRect = page->rectForCharacterAtIndex(1);
+            auto cMidpoint = CGPointMake(CGRectGetMidX(cRect), CGRectGetMidY(cRect));
+            auto annotationBounds = annotations[0].bounds();
+
+            EXPECT_TRUE(CGRectContainsPoint(annotationBounds, cMidpoint));
+        }
+#endif
+
+        didTakeSnapshot = true;
+    }];
+
+    Util::run(&didTakeSnapshot);
+}
+
+TEST(PDFSnapshot, InlineLinks)
+{
+    static bool didTakeSnapshot;
+
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600)]);
+    [webView synchronouslyLoadHTMLString:@"<meta name='viewport' content='width=device-width'><a href=\"https://webkit.org/\">Click me</a>"];
+
+    [webView _takePDFSnapshotWithConfiguration:nil completionHandler:^(NSData *pdfSnapshotData, NSError *error) {
+        EXPECT_NULL(error);
+        auto document = TestPDFDocument::createFromData(pdfSnapshotData);
+        EXPECT_EQ(document->pageCount(), 1u);
+
+        auto page = document->page(0);
+        EXPECT_NE(page, nullptr);
+
+        // FIXME: There should be a link here, but due to the way we gather links for annotation using the RenderInline tree
+        // it is missed.
+
+//        auto annotations = page->annotations();
+//        EXPECT_EQ(annotations.size(), 1u);
+//        EXPECT_TRUE(annotations[0].isLink());
+//        EXPECT_TRUE([annotations[0].linkURL() isEqual:[NSURL URLWithString:@"https://webkit.org/"]]);
+
+        didTakeSnapshot = true;
+    }];
+
+    Util::run(&didTakeSnapshot);
+}
+
+}
+
diff --git a/Tools/TestWebKitAPI/cocoa/TestPDFDocument.h b/Tools/TestWebKitAPI/cocoa/TestPDFDocument.h
new file mode 100644 (file)
index 0000000..04c1e88
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2019 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#import <PDFKit/PDFKit.h>
+#import <wtf/RefCounted.h>
+#import <wtf/RetainPtr.h>
+#import <wtf/Vector.h>
+#import <wtf/text/WTFString.h>
+
+namespace WebCore {
+class Color;
+}
+
+namespace TestWebKitAPI {
+
+// Annotations aren't ref counted. They remain valid as long as their containing TestPDFPage remains valid
+class TestPDFAnnotation {
+public:
+    TestPDFAnnotation(PDFAnnotation *);
+
+    bool isLink() const;
+    CGRect bounds() const;
+    NSURL *linkURL() const;
+    
+private:
+    RetainPtr<PDFAnnotation> m_annotation;
+};
+
+class TestPDFPage : public RefCounted<TestPDFPage> {
+public:
+    static Ref<TestPDFPage> create(PDFPage *);
+
+    const Vector<TestPDFAnnotation>& annotations();
+    size_t characterCount() const;
+    String text() const;
+    CGRect rectForCharacterAtIndex(size_t) const;
+    size_t characterIndexAtPoint(CGPoint) const;
+    CGRect bounds() const;
+
+    WebCore::Color colorAtPoint(int x, int y) const;
+    
+private:
+    TestPDFPage(PDFPage *);
+    RetainPtr<PDFPage> m_page;
+    Optional<Vector<TestPDFAnnotation>> m_annotations;
+    mutable Optional<String> m_textWithoutSurroundingWhitespace;
+};
+
+class TestPDFDocument : public RefCounted<TestPDFDocument> {
+public:
+    static Ref<TestPDFDocument> createFromData(NSData *);
+
+    void dump();
+
+    size_t pageCount() const;
+    TestPDFPage* page(size_t index);
+
+private:
+    TestPDFDocument(NSData *);
+
+    RetainPtr<PDFDocument> m_document;
+    Vector<RefPtr<TestPDFPage>> m_pages;
+};
+
+} // namespace TestWebKitAPI
diff --git a/Tools/TestWebKitAPI/cocoa/TestPDFDocument.mm b/Tools/TestWebKitAPI/cocoa/TestPDFDocument.mm
new file mode 100644 (file)
index 0000000..5054170
--- /dev/null
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2019 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "config.h"
+#import "TestPDFDocument.h"
+
+#import <WebCore/ColorMac.h>
+#import <pal/spi/cg/CoreGraphicsSPI.h>
+
+namespace TestWebKitAPI {
+
+#if PLATFORM(MAC)
+CGRect toCGRect(NSRect rect) { return NSRectToCGRect(rect); }
+NSPoint toPlatformPoint(CGPoint point) { return NSPointFromCGPoint(point); }
+#else
+CGRect toCGRect(CGRect rect) { return rect; }
+NSPoint toPlatformPoint(CGPoint point) { return point; }
+#endif
+
+// Annotations
+TestPDFAnnotation::TestPDFAnnotation(PDFAnnotation *annotation)
+    : m_annotation(annotation)
+{
+}
+
+bool TestPDFAnnotation::isLink() const
+{
+    bool isLink = [m_annotation.get().type isEqualToString:@"Link"];
+    if (isLink)
+        ASSERT([m_annotation.get().action isKindOfClass:[PDFActionURL class]]);
+    return isLink;
+}
+
+CGRect TestPDFAnnotation::bounds() const
+{
+    return toCGRect(m_annotation.get().bounds);
+}
+
+NSURL *TestPDFAnnotation::linkURL() const
+{
+    if (!isLink())
+        return nil;
+    return ((PDFActionURL *)m_annotation.get().action).URL;
+}
+
+// Pages
+
+Ref<TestPDFPage> TestPDFPage::create(PDFPage *page)
+{
+    return adoptRef(*new TestPDFPage(page));
+}
+
+TestPDFPage::TestPDFPage(PDFPage *page)
+    : m_page(page)
+{
+}
+
+const Vector<TestPDFAnnotation>& TestPDFPage::annotations()
+{
+    if (!m_annotations) {
+        m_annotations = Vector<TestPDFAnnotation>();
+        for (PDFAnnotation *annotation in m_page.get().annotations)
+            m_annotations->append({ annotation });
+    }
+
+    return *m_annotations;
+}
+
+size_t TestPDFPage::characterCount() const
+{
+    return text().length();
+}
+
+inline bool shouldStrip(UChar character)
+{
+    // This is a list of trailing and leading white space characters we've seen in PDF generation.
+    // It can be expanded if we see more popup.
+    return character == ' ' || character == '\n';
+}
+
+String TestPDFPage::text() const
+{
+    if (!m_textWithoutSurroundingWhitespace)
+        m_textWithoutSurroundingWhitespace = String(m_page.get().string).stripLeadingAndTrailingCharacters(shouldStrip);
+
+    return *m_textWithoutSurroundingWhitespace;
+}
+
+CGRect TestPDFPage::rectForCharacterAtIndex(size_t index) const
+{
+    return toCGRect([m_page characterBoundsAtIndex:index]);
+}
+
+size_t TestPDFPage::characterIndexAtPoint(CGPoint point) const
+{
+    return [m_page characterIndexAtPoint:toPlatformPoint(point)];
+}
+
+CGRect TestPDFPage::bounds() const
+{
+    return toCGRect([m_page boundsForBox:kPDFDisplayBoxMediaBox]);
+}
+
+
+WebCore::Color TestPDFPage::colorAtPoint(int x, int y) const
+{
+    auto boundsRect = bounds();
+    auto colorSpace = adoptCF(CGColorSpaceCreateWithName(kCGColorSpaceSRGB));
+    auto context = adoptCF(CGBitmapContextCreate(NULL, boundsRect.size.width, boundsRect.size.height, 8, 0, colorSpace.get(), kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big));
+
+    auto cgPage = m_page.get().pageRef;
+    CGContextDrawPDFPageWithAnnotations(context.get(), cgPage, nullptr);
+
+    const unsigned char* pixel = (const unsigned char*)CGBitmapContextGetData(context.get());
+    size_t i = (y * x * 4) + (x * 4);
+
+    auto r = pixel[i];
+    auto g = pixel[i + 1];
+    auto b = pixel[i + 2];
+    auto a = pixel[i + 3];
+
+    if (!a)
+        return { 0, 0, 0, 0 };
+    return { r * 255 / a, g * 255 / a, b * 255 / a, a };
+}
+
+// Documents
+
+Ref<TestPDFDocument> TestPDFDocument::createFromData(NSData *data)
+{
+    return adoptRef(*new TestPDFDocument(data));
+}
+
+TestPDFDocument::TestPDFDocument(NSData *data)
+    : m_document(adoptNS([[PDFDocument alloc] initWithData:data]))
+    , m_pages(pageCount())
+{
+}
+
+size_t TestPDFDocument::pageCount() const
+{
+    return [m_document pageCount];
+}
+
+TestPDFPage* TestPDFDocument::page(size_t index)
+{
+    if (index >= pageCount())
+        return nullptr;
+    if (!m_pages[index])
+        m_pages[index] = TestPDFPage::create([m_document pageAtIndex:index]);
+    return m_pages[index].get();
+}
+
+} // namespace TestWebKitAPI