/* * Copyright (C) 2014-2016 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 "WKPDFView.h" #if PLATFORM(IOS) #import "APIFindClient.h" #import "APIUIClient.h" #import "ApplicationStateTracker.h" #import "CorePDFSPI.h" #import "DrawingAreaProxy.h" #import "SessionState.h" #import "UIKitSPI.h" #import "WKPDFPageNumberIndicator.h" #import "WKPasswordView.h" #import "WKWebViewInternal.h" #import "WeakObjCPtr.h" #import "WebPageProxy.h" #import "_WKFindDelegate.h" #import "_WKWebViewPrintFormatterInternal.h" #import <MobileCoreServices/UTCoreTypes.h> #import <WebCore/FloatRect.h> #import <WebCore/LocalizedStrings.h> #import <WebCore/WebCoreNSURLExtras.h> #import <wtf/RetainPtr.h> #import <wtf/Vector.h> using namespace WebCore; using namespace WebKit; const CGFloat pdfPageMargin = 8; const CGFloat pdfMinimumZoomScale = 1; const CGFloat pdfMaximumZoomScale = 5; const float overdrawHeightMultiplier = 1.5; static const CGFloat smartMagnificationElementPadding = 0.05; typedef struct { CGRect frame; RetainPtr<UIPDFPageView> view; RetainPtr<UIPDFPage> page; unsigned index; } PDFPageInfo; @interface WKPDFView () - (void)_resetZoomAnimated:(BOOL)animated; @end @implementation WKPDFView { RetainPtr<CGPDFDocumentRef> _cgPDFDocument; RetainPtr<UIPDFDocument> _pdfDocument; RetainPtr<NSString> _suggestedFilename; RetainPtr<WKPDFPageNumberIndicator> _pageNumberIndicator; Vector<PDFPageInfo> _pages; unsigned _centerPageNumber; CGSize _minimumSize; CGSize _overlaidAccessoryViewsInset; WKWebView *_webView; UIScrollView *_scrollView; UIView *_fixedOverlayView; BOOL _isStartingZoom; BOOL _isPerformingSameDocumentNavigation; RetainPtr<WKActionSheetAssistant> _actionSheetAssistant; WebKit::InteractionInformationAtPosition _positionInformation; unsigned _currentFindPageIndex; unsigned _currentFindMatchIndex; RetainPtr<UIPDFSelection> _currentFindSelection; RetainPtr<NSString> _cachedFindString; Vector<RetainPtr<UIPDFSelection>> _cachedFindMatches; unsigned _cachedFindMaximumCount; _WKFindOptions _cachedFindOptionsAffectingResults; std::atomic<unsigned> _nextComputeMatchesOperationID; RetainPtr<NSString> _nextCachedFindString; unsigned _nextCachedFindMaximumCount; _WKFindOptions _nextCachedFindOptionsAffectingResults; dispatch_queue_t _findQueue; RetainPtr<UIWKSelectionAssistant> _webSelectionAssistant; std::unique_ptr<ApplicationStateTracker> _applicationStateTracker; UIEdgeInsets _lastUnobscuredSafeAreaInset; CGFloat _lastLayoutWidth; } - (instancetype)web_initWithFrame:(CGRect)frame webView:(WKWebView *)webView { if (!(self = [super initWithFrame:frame])) return nil; self.backgroundColor = [UIColor grayColor]; _webView = webView; _scrollView = webView.scrollView; [_scrollView setMinimumZoomScale:pdfMinimumZoomScale]; [_scrollView setMaximumZoomScale:pdfMaximumZoomScale]; [_scrollView setBackgroundColor:[UIColor grayColor]]; _actionSheetAssistant = adoptNS([[WKActionSheetAssistant alloc] initWithView:self]); [_actionSheetAssistant setDelegate:self]; _findQueue = dispatch_queue_create("com.apple.WebKit.WKPDFViewComputeMatchesQueue", DISPATCH_QUEUE_SERIAL); return self; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; [self _clearPages]; [_pageNumberIndicator removeFromSuperview]; dispatch_release(_findQueue); [_actionSheetAssistant cleanupSheet]; [super dealloc]; } - (NSString *)suggestedFilename { return _suggestedFilename.get(); } - (CGPDFDocumentRef)pdfDocument { return _cgPDFDocument.get(); } static void detachViewForPage(PDFPageInfo& page) { [page.view removeFromSuperview]; [page.view setDelegate:nil]; [[page.view annotationController] setDelegate:nil]; page.view = nil; } - (void)_clearPages { for (auto& page : _pages) detachViewForPage(page); _pages.clear(); } - (void)_didLoadPDFDocument { _pdfDocument = adoptNS([[UIPDFDocument alloc] initWithCGPDFDocument:_cgPDFDocument.get()]); // FIXME: Restore the scroll position and page scale if navigating from the back/forward list. [self _computePageAndDocumentFrames]; [self _revalidateViews]; [self _scrollToFragment:_webView.URL.fragment]; } - (void)web_setContentProviderData:(NSData *)data suggestedFilename:(NSString *)filename { _suggestedFilename = adoptNS([filename copy]); [self _clearPages]; RetainPtr<CGDataProviderRef> dataProvider = adoptCF(CGDataProviderCreateWithCFData((CFDataRef)data)); _cgPDFDocument = adoptCF(CGPDFDocumentCreateWithProvider(dataProvider.get())); if (!_cgPDFDocument) return; if (CGPDFDocumentIsUnlocked(_cgPDFDocument.get())) { [self _didLoadPDFDocument]; return; } [self _showPasswordEntryField]; } - (void)web_setMinimumSize:(CGSize)size { if (CGSizeEqualToSize(size, _minimumSize)) return; _minimumSize = size; if (_webView._passwordView) { self.frame = { self.frame.origin, size }; return; } CGFloat oldDocumentLeftFraction = 0; CGFloat oldDocumentTopFraction = 0; CGSize contentSize = _scrollView.contentSize; if (contentSize.width && contentSize.height) { CGPoint contentOffset = _scrollView.contentOffset; UIEdgeInsets contentInset = _scrollView.contentInset; oldDocumentLeftFraction = (contentOffset.x + contentInset.left) / contentSize.width; oldDocumentTopFraction = (contentOffset.y + contentInset.top) / contentSize.height; } [self _computePageAndDocumentFrames]; CGSize newContentSize = _scrollView.contentSize; UIEdgeInsets contentInset = _scrollView.contentInset; [_scrollView setContentOffset:CGPointMake((oldDocumentLeftFraction * newContentSize.width) - contentInset.left, (oldDocumentTopFraction * newContentSize.height) - contentInset.top) animated:NO]; [self _revalidateViews]; } - (void)scrollViewDidScroll:(UIScrollView *)scrollView { if (scrollView.isZoomBouncing || scrollView._isAnimatingZoom) return; [self _revalidateViews]; if (!_isPerformingSameDocumentNavigation) [_pageNumberIndicator show]; } - (void)_ensureViewForPage:(PDFPageInfo&)pageInfo { if (pageInfo.view) return; pageInfo.view = adoptNS([[UIPDFPageView alloc] initWithPage:pageInfo.page.get() tiledContent:YES]); [pageInfo.view setUseBackingLayer:YES]; [pageInfo.view setDelegate:self]; [[pageInfo.view annotationController] setDelegate:self]; [self addSubview:pageInfo.view.get()]; [pageInfo.view setFrame:pageInfo.frame]; [pageInfo.view contentLayer].contentsScale = self.window.screen.scale; } - (void)_revalidateViews { if (_isStartingZoom) return; CGRect targetRect = [_scrollView convertRect:_scrollView.bounds toView:self]; // We apply overdraw after applying scale in order to avoid excessive // memory use caused by scaling the overdraw. CGRect targetRectWithOverdraw = CGRectInset(targetRect, 0, -targetRect.size.height * overdrawHeightMultiplier); CGRect targetRectForCenterPage = CGRectInset(targetRect, 0, targetRect.size.height / 2 - pdfPageMargin * 2); _centerPageNumber = 0; for (auto& pageInfo : _pages) { if (!CGRectIntersectsRect(pageInfo.frame, targetRectWithOverdraw) && pageInfo.index != _currentFindPageIndex) { detachViewForPage(pageInfo); continue; } if (!_centerPageNumber && CGRectIntersectsRect(pageInfo.frame, targetRectForCenterPage)) _centerPageNumber = pageInfo.index + 1; [self _ensureViewForPage:pageInfo]; } if (!_centerPageNumber && _pages.size()) { if (CGRectGetMinY(_pages.first().frame) > CGRectGetMaxY(targetRectForCenterPage)) _centerPageNumber = 1; else { ASSERT(CGRectGetMaxY(_pages.last().frame) < CGRectGetMinY(targetRectForCenterPage)); _centerPageNumber = _pages.size(); } } [self _updatePageNumberIndicator]; } - (CGPoint)_offsetForPageNumberIndicator { UIEdgeInsets insets = UIEdgeInsetsAdd(_webView._computedUnobscuredSafeAreaInset, _webView._computedContentInset, UIRectEdgeAll); return CGPointMake(insets.left, insets.top + _overlaidAccessoryViewsInset.height); } - (void)_updatePageNumberIndicator { if (_isPerformingSameDocumentNavigation) return; if (!_pageNumberIndicator) _pageNumberIndicator = adoptNS([[WKPDFPageNumberIndicator alloc] initWithFrame:CGRectZero]); [_fixedOverlayView addSubview:_pageNumberIndicator.get()]; [_pageNumberIndicator setCurrentPageNumber:_centerPageNumber]; [_pageNumberIndicator moveToPoint:[self _offsetForPageNumberIndicator] animated:NO]; } - (void)web_setOverlaidAccessoryViewsInset:(CGSize)inset { _overlaidAccessoryViewsInset = inset; [_pageNumberIndicator moveToPoint:[self _offsetForPageNumberIndicator] animated:YES]; } - (void)web_computedContentInsetDidChange { [self _updatePageNumberIndicator]; if (UIEdgeInsetsEqualToEdgeInsets(_webView._computedUnobscuredSafeAreaInset, _lastUnobscuredSafeAreaInset)) return; [self _computePageAndDocumentFrames]; [self _revalidateViews]; } - (void)web_setFixedOverlayView:(UIView *)fixedOverlayView { _fixedOverlayView = fixedOverlayView; if (_pageNumberIndicator) [_fixedOverlayView addSubview:_pageNumberIndicator.get()]; } - (void)_scrollToFragment:(NSString *)fragment { if (![fragment hasPrefix:@"page"]) return; NSInteger pageIndex = [[fragment substringFromIndex:4] integerValue] - 1; if (pageIndex < 0 || static_cast<std::size_t>(pageIndex) >= _pages.size()) return; _isPerformingSameDocumentNavigation = YES; [_pageNumberIndicator hide]; [self _resetZoomAnimated:NO]; // Ensure that the page margin is visible below the content inset. const CGFloat verticalOffset = _pages[pageIndex].frame.origin.y - _webView._computedContentInset.top - pdfPageMargin; [_scrollView setContentOffset:CGPointMake(_scrollView.contentOffset.x, verticalOffset) animated:NO]; _isPerformingSameDocumentNavigation = NO; } - (void)web_didSameDocumentNavigation:(WKSameDocumentNavigationType)navigationType { // Check for kWKSameDocumentNavigationSessionStatePop instead of kWKSameDocumentNavigationAnchorNavigation since the // latter is only called once when navigating to the same anchor in succession. If the user navigates to a page // then scrolls back and clicks on the same link a second time, we want to scroll again. if (navigationType != kWKSameDocumentNavigationSessionStatePop) return; // FIXME: restore the scroll position and page scale if navigating back from a fragment. [self _scrollToFragment:_webView.URL.fragment]; } - (void)_computePageAndDocumentFrames { UIEdgeInsets safeAreaInsets = _webView._computedUnobscuredSafeAreaInset; _lastUnobscuredSafeAreaInset = safeAreaInsets; CGSize minimumSizeRespectingContentInset = CGSizeMake(_minimumSize.width - (safeAreaInsets.left + safeAreaInsets.right), _minimumSize.height - (safeAreaInsets.top + safeAreaInsets.bottom)); if (!_pages.isEmpty() && _lastLayoutWidth == minimumSizeRespectingContentInset.width) { [self _updateDocumentFrame]; return; } NSUInteger pageCount = [_pdfDocument numberOfPages]; if (!pageCount) return; _lastLayoutWidth = minimumSizeRespectingContentInset.width; [_pageNumberIndicator setPageCount:pageCount]; [self _clearPages]; _pages.reserveCapacity(pageCount); CGRect pageFrame = CGRectMake(0, 0, minimumSizeRespectingContentInset.width, minimumSizeRespectingContentInset.height); for (NSUInteger pageIndex = 0; pageIndex < pageCount; ++pageIndex) { UIPDFPage *page = [_pdfDocument pageAtIndex:pageIndex]; if (!page) continue; CGSize pageSize = [page cropBoxAccountForRotation].size; pageFrame.size.height = pageSize.height / pageSize.width * pageFrame.size.width; CGRect pageFrameWithMarginApplied = CGRectInset(pageFrame, pdfPageMargin, pdfPageMargin); if (CGRectIsNull(pageFrameWithMarginApplied)) pageFrameWithMarginApplied = CGRectZero; PDFPageInfo pageInfo; pageInfo.page = page; pageInfo.frame = pageFrameWithMarginApplied; pageInfo.index = pageIndex; _pages.append(pageInfo); pageFrame.origin.y += pageFrame.size.height - pdfPageMargin; } [self _updateDocumentFrame]; } - (void)_updateDocumentFrame { if (_pages.isEmpty()) return; UIEdgeInsets safeAreaInsets = _webView._computedUnobscuredSafeAreaInset; CGFloat scale = _scrollView.zoomScale; CGRect newFrame = CGRectZero; newFrame.origin.x = safeAreaInsets.left; newFrame.origin.y = safeAreaInsets.top; newFrame.size.width = _lastLayoutWidth * scale; newFrame.size.height = std::max((CGRectGetMaxY(_pages.last().frame) + pdfPageMargin) * scale + safeAreaInsets.bottom, _minimumSize.height * scale); [self setFrame:newFrame]; [_scrollView setContentSize:newFrame.size]; } - (void)_resetZoomAnimated:(BOOL)animated { _isStartingZoom = YES; CGRect scrollViewBounds = _scrollView.bounds; CGPoint centerOfPageInDocumentCoordinates = [_scrollView convertPoint:CGPointMake(CGRectGetMidX(scrollViewBounds), CGRectGetMidY(scrollViewBounds)) toView:self]; [_webView _zoomOutWithOrigin:centerOfPageInDocumentCoordinates animated:animated]; _isStartingZoom = NO; } - (void)_highlightLinkAnnotation:(UIPDFLinkAnnotation *)linkAnnotation forDuration:(NSTimeInterval)duration completionHandler:(void (^)(void))completionHandler { static const CGFloat highlightBorderRadius = 3; static const CGFloat highlightColorComponent = 26.0 / 255; static UIColor *highlightColor = [[UIColor alloc] initWithRed:highlightColorComponent green:highlightColorComponent blue:highlightColorComponent alpha:0.3]; UIPDFPageView *pageView = linkAnnotation.annotationController.pageView; CGRect highlightViewFrame = [self convertRect:[pageView convertRectFromPDFPageSpace:linkAnnotation.Rect] fromView:pageView]; RetainPtr<_UIHighlightView> highlightView = adoptNS([[_UIHighlightView alloc] initWithFrame:CGRectInset(highlightViewFrame, -highlightBorderRadius, -highlightBorderRadius)]); [highlightView setOpaque:NO]; [highlightView setCornerRadius:highlightBorderRadius]; [highlightView setColor:highlightColor]; ASSERT(RunLoop::isMain()); [self addSubview:highlightView.get()]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, duration * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ [highlightView removeFromSuperview]; completionHandler(); }); } - (NSURL *)_URLForLinkAnnotation:(UIPDFLinkAnnotation *)linkAnnotation { NSURL *documentURL = _webView.URL; if (NSURL *url = linkAnnotation.url) return [NSURL URLWithString:url.relativeString relativeToURL:documentURL]; if (NSUInteger pageNumber = linkAnnotation.pageNumber) { String anchorString = ASCIILiteral("#page"); anchorString.append(String::number(pageNumber)); return [NSURL URLWithString:anchorString relativeToURL:documentURL]; } return nil; } #pragma mark Find-in-Page static NSStringCompareOptions stringCompareOptions(_WKFindOptions options) { NSStringCompareOptions findOptions = 0; if (options & _WKFindOptionsCaseInsensitive) findOptions |= NSCaseInsensitiveSearch; if (options & _WKFindOptionsBackwards) findOptions |= NSBackwardsSearch; return findOptions; } - (void)_computeMatchesForString:(NSString *)string options:(_WKFindOptions)options maxCount:(NSUInteger)maxCount completionHandler:(void (^)(BOOL success))completionHandler { if (!_pdfDocument) { completionHandler(NO); return; } _WKFindOptions optionsAffectingResults = options & ~_WKFindOptionsIrrelevantForBatchResults; // If this search is equivalent to the currently cached search, bail and call the completion handler, because the existing cached results are valid. if (!_cachedFindMatches.isEmpty() && [_cachedFindString isEqualToString:string] && _cachedFindOptionsAffectingResults == optionsAffectingResults && _cachedFindMaximumCount == maxCount) { // Also, cancel any running search, because it will eventually replace the now-valid results. ++_nextComputeMatchesOperationID; completionHandler(YES); return; } // If this search is equivalent to the currently running asynchronous search, bail as if this search were cancelled; the original search's completion handler will eventually fire. if ([_nextCachedFindString isEqualToString:string] && _nextCachedFindOptionsAffectingResults == optionsAffectingResults && _nextCachedFindMaximumCount == maxCount) { completionHandler(NO); return; } NSStringCompareOptions findOptions = stringCompareOptions(optionsAffectingResults); Vector<PDFPageInfo> pages = _pages; unsigned computeMatchesOperationID = ++_nextComputeMatchesOperationID; _nextCachedFindString = string; _nextCachedFindOptionsAffectingResults = optionsAffectingResults; _nextCachedFindMaximumCount = maxCount; RetainPtr<WKPDFView> retainedSelf = self; typeof(completionHandler) completionHandlerCopy = Block_copy(completionHandler); dispatch_async(_findQueue, [pages, string, findOptions, optionsAffectingResults, maxCount, computeMatchesOperationID, retainedSelf, completionHandlerCopy] { Vector<RetainPtr<UIPDFSelection>> matches; for (unsigned pageIndex = 0; pageIndex < pages.size(); ++pageIndex) { UIPDFSelection *match = nullptr; while ((match = [pages[pageIndex].page findString:string fromSelection:match options:findOptions])) { matches.append(match); if (matches.size() > maxCount) goto maxCountExceeded; // If we've enqueued another search, cancel this one. if (retainedSelf->_nextComputeMatchesOperationID != computeMatchesOperationID) { dispatch_async(dispatch_get_main_queue(), [completionHandlerCopy] { completionHandlerCopy(NO); Block_release(completionHandlerCopy); }); return; } }; } maxCountExceeded: dispatch_async(dispatch_get_main_queue(), [computeMatchesOperationID, string, optionsAffectingResults, maxCount, matches, completionHandlerCopy, retainedSelf] { // If another search has been enqueued in the meantime, ignore this result. if (retainedSelf->_nextComputeMatchesOperationID != computeMatchesOperationID) { Block_release(completionHandlerCopy); return; } retainedSelf->_cachedFindString = string; retainedSelf->_cachedFindOptionsAffectingResults = optionsAffectingResults; retainedSelf->_cachedFindMaximumCount = maxCount; retainedSelf->_cachedFindMatches = matches; retainedSelf->_nextCachedFindString = nil; completionHandlerCopy(YES); Block_release(completionHandlerCopy); }); }); } - (void)web_countStringMatches:(NSString *)string options:(_WKFindOptions)options maxCount:(NSUInteger)maxCount { RefPtr<WebKit::WebPageProxy> page = _webView->_page; [self _computeMatchesForString:string options:options maxCount:maxCount completionHandler:^(BOOL success) { if (!success) return; page->findClient().didCountStringMatches(page.get(), string, _cachedFindMatches.size()); }]; } - (void)_didFindMatch:(UIPDFSelection *)match { for (auto& pageInfo : _pages) { if (pageInfo.page == match.page) { [self _ensureViewForPage:pageInfo]; [pageInfo.view highlightSearchSelection:match animated:NO]; _currentFindPageIndex = pageInfo.index; _currentFindSelection = match; CGRect zoomRect = [pageInfo.view convertRectFromPDFPageSpace:match.bounds]; if (CGRectIsNull(zoomRect)) return; [self zoom:pageInfo.view.get() to:zoomRect atPoint:CGPointZero kind:kUIPDFObjectKindText]; return; } } } - (void)web_findString:(NSString *)string options:(_WKFindOptions)options maxCount:(NSUInteger)maxCount { if (_currentFindSelection) [_pages[_currentFindPageIndex].view clearSearchHighlights]; RetainPtr<UIPDFSelection> previousFindSelection = _currentFindSelection; unsigned previousFindPageIndex = 0; if (previousFindSelection) { previousFindPageIndex = _currentFindPageIndex; if (![_cachedFindString isEqualToString:string]) { NSUInteger location = [_currentFindSelection startIndex]; if (location) previousFindSelection = adoptNS([[UIPDFSelection alloc] initWithPage:[_currentFindSelection page] fromIndex:location - 1 toIndex:location]); } } NSStringCompareOptions findOptions = stringCompareOptions(options); bool backwards = (options & _WKFindOptionsBackwards); RefPtr<WebKit::WebPageProxy> page = _webView->_page; [self _computeMatchesForString:string options:options maxCount:maxCount completionHandler:^(BOOL success) { if (!success) return; unsigned pageIndex = previousFindPageIndex; for (unsigned i = 0; i < _pages.size(); ++i) { UIPDFSelection *match = [_pages[pageIndex].page findString:string fromSelection:(pageIndex == previousFindPageIndex ? previousFindSelection.get() : nil) options:findOptions]; if (!match) { if (!pageIndex && backwards) pageIndex = _pages.size() - 1; else if (pageIndex == _pages.size() - 1 && !backwards) pageIndex = 0; else pageIndex += backwards ? -1 : 1; continue; } [self _didFindMatch:match]; if (_cachedFindMatches.size() <= maxCount) { _currentFindMatchIndex = 0; for (const auto& knownMatch : _cachedFindMatches) { if (match.stringRange.location == [knownMatch stringRange].location && match.stringRange.length == [knownMatch stringRange].length) { page->findClient().didFindString(page.get(), string, { }, _cachedFindMatches.size(), _currentFindMatchIndex, false); break; } _currentFindMatchIndex++; } } return; } page->findClient().didFailToFindString(page.get(), string); }]; } - (void)web_hideFindUI { if (_currentFindSelection) [_pages[_currentFindPageIndex].view clearSearchHighlights]; _currentFindSelection = nullptr; _cachedFindString = nullptr; _cachedFindMatches.clear(); } #pragma mark UIPDFPageViewDelegate - (void)zoom:(UIPDFPageView *)pageView to:(CGRect)targetRect atPoint:(CGPoint)origin kind:(UIPDFObjectKind)kind { _isStartingZoom = YES; BOOL isImage = kind == kUIPDFObjectKindGraphic; if (!isImage) targetRect = CGRectInset(targetRect, smartMagnificationElementPadding * targetRect.size.width, smartMagnificationElementPadding * targetRect.size.height); CGRect rectInDocumentCoordinates = [pageView convertRect:targetRect toView:self]; CGPoint originInDocumentCoordinates = [pageView convertPoint:origin toView:self]; [_webView _zoomToRect:rectInDocumentCoordinates withOrigin:originInDocumentCoordinates fitEntireRect:isImage minimumScale:pdfMinimumZoomScale maximumScale:pdfMaximumZoomScale minimumScrollDistance:0]; _isStartingZoom = NO; } - (void)resetZoom:(UIPDFPageView *)pageView { [self _resetZoomAnimated:YES]; } #pragma mark UIPDFAnnotationControllerDelegate - (void)annotation:(UIPDFAnnotation *)annotation wasTouchedAtPoint:(CGPoint)point controller:(UIPDFAnnotationController *)controller { if (![annotation isKindOfClass:[UIPDFLinkAnnotation class]]) return; UIPDFLinkAnnotation *linkAnnotation = (UIPDFLinkAnnotation *)annotation; RetainPtr<NSURL> url = [self _URLForLinkAnnotation:linkAnnotation]; if (!url) return; CGPoint documentPoint = [controller.pageView convertPoint:point toView:self]; CGPoint screenPoint = [self.window convertPoint:[self convertPoint:documentPoint toView:nil] toWindow:nil]; RetainPtr<WKWebView> retainedWebView = _webView; [self _highlightLinkAnnotation:linkAnnotation forDuration:.2 completionHandler:^{ retainedWebView->_page->navigateToPDFLinkWithSimulatedClick([url absoluteString], roundedIntPoint(documentPoint), roundedIntPoint(screenPoint)); }]; } - (void)annotation:(UIPDFAnnotation *)annotation isBeingPressedAtPoint:(CGPoint)point controller:(UIPDFAnnotationController *)controller { if (![annotation isKindOfClass:[UIPDFLinkAnnotation class]]) return; UIPDFLinkAnnotation *linkAnnotation = (UIPDFLinkAnnotation *)annotation; NSURL *url = [self _URLForLinkAnnotation:linkAnnotation]; if (!url) return; _positionInformation.request.point = roundedIntPoint([controller.pageView convertPoint:point toView:self]); _positionInformation.url = url; _positionInformation.bounds = roundedIntRect([self convertRect:[controller.pageView convertRectFromPDFPageSpace:annotation.Rect] fromView:controller.pageView]); [self _highlightLinkAnnotation:linkAnnotation forDuration:.75 completionHandler:^{ [_actionSheetAssistant showLinkSheet]; }]; } #pragma mark WKActionSheetAssistantDelegate - (std::optional<WebKit::InteractionInformationAtPosition>)positionInformationForActionSheetAssistant:(WKActionSheetAssistant *)assistant { return _positionInformation; } - (void)actionSheetAssistant:(WKActionSheetAssistant *)assistant performAction:(WebKit::SheetAction)action { if (action != WebKit::SheetAction::Copy) return; NSDictionary *representations = @{ (NSString *)kUTTypeUTF8PlainText : (NSString *)_positionInformation.url, (NSString *)kUTTypeURL : (NSURL *)_positionInformation.url }; [UIPasteboard generalPasteboard].items = @[ representations ]; } - (void)actionSheetAssistant:(WKActionSheetAssistant *)assistant openElementAtLocation:(CGPoint)location { CGPoint screenPoint = [self.window convertPoint:[self convertPoint:location toView:nil] toWindow:nil]; _webView->_page->navigateToPDFLinkWithSimulatedClick(_positionInformation.url, roundedIntPoint(location), roundedIntPoint(screenPoint)); } - (void)actionSheetAssistant:(WKActionSheetAssistant *)assistant shareElementWithURL:(NSURL *)url rect:(CGRect)boundingRect { _webSelectionAssistant = adoptNS([[UIWKSelectionAssistant alloc] initWithView:self]); [_webSelectionAssistant showShareSheetFor:userVisibleString(url) fromRect:boundingRect]; _webSelectionAssistant = nil; } #if HAVE(APP_LINKS) - (BOOL)actionSheetAssistant:(WKActionSheetAssistant *)assistant shouldIncludeAppLinkActionsForElement:(_WKActivatedElementInfo *)element { return _webView->_page->uiClient().shouldIncludeAppLinkActionsForElement(element); } #endif - (RetainPtr<NSArray>)actionSheetAssistant:(WKActionSheetAssistant *)assistant decideActionsForElement:(_WKActivatedElementInfo *)element defaultActions:(RetainPtr<NSArray>)defaultActions { return _webView->_page->uiClient().actionsForElement(element, WTFMove(defaultActions)); } #pragma mark Password protection UI - (void)_showPasswordEntryField { [_webView _showPasswordViewWithDocumentName:_suggestedFilename.get() passwordHandler:[retainedSelf = retainPtr(self), webView = retainPtr(_webView)](NSString *password) { if (!CGPDFDocumentUnlockWithPassword(retainedSelf->_cgPDFDocument.get(), password.UTF8String)) { [[webView _passwordView] showPasswordFailureAlert]; return; } [webView _hidePasswordView]; [retainedSelf _didLoadPDFDocument]; }]; } - (void)willMoveToWindow:(UIWindow *)newWindow { if (newWindow) return; ASSERT(self.window); ASSERT(_applicationStateTracker); _applicationStateTracker = nullptr; } - (void)didMoveToWindow { if (!self.window) return; ASSERT(!_applicationStateTracker); _applicationStateTracker = std::make_unique<ApplicationStateTracker>(self, @selector(_applicationDidEnterBackground), @selector(_applicationDidCreateWindowContext), @selector(_applicationDidFinishSnapshottingAfterEnteringBackground), @selector(_applicationWillEnterForeground)); } - (BOOL)isBackground { if (!_applicationStateTracker) return YES; return _applicationStateTracker->isInBackground(); } - (void)_applicationDidEnterBackground { _webView->_page->applicationDidEnterBackground(); _webView->_page->activityStateDidChange(ActivityState::AllFlags & ~ActivityState::IsInWindow); } - (void)_applicationDidCreateWindowContext { } - (void)_applicationDidFinishSnapshottingAfterEnteringBackground { _webView->_page->applicationDidFinishSnapshottingAfterEnteringBackground(); } - (void)_applicationWillEnterForeground { _webView->_page->applicationWillEnterForeground(); if (auto drawingArea = _webView->_page->drawingArea()) drawingArea->hideContentUntilAnyUpdate(); _webView->_page->activityStateDidChange(ActivityState::AllFlags & ~ActivityState::IsInWindow, true, WebPageProxy::ActivityStateChangeDispatchMode::Immediate); } @end #pragma mark Printing @interface WKPDFView (_WKWebViewPrintFormatter) <_WKWebViewPrintProvider> @end @implementation WKPDFView (_WKWebViewPrintFormatter) - (NSUInteger)_wk_pageCountForPrintFormatter:(_WKWebViewPrintFormatter *)printFormatter { CGPDFDocumentRef document = _cgPDFDocument.get(); if (!CGPDFDocumentAllowsPrinting(document)) return 0; size_t numberOfPages = CGPDFDocumentGetNumberOfPages(document); if (printFormatter.snapshotFirstPage) return std::min<NSUInteger>(numberOfPages, 1); return numberOfPages; } - (CGPDFDocumentRef)_wk_printedDocument { return _cgPDFDocument.get(); } @end #endif /* PLATFORM(IOS) */