e8d8cc069bcd16620f6df6451fd67605421510c4
[WebKit-https.git] / Source / WebKit2 / UIProcess / ios / WKPDFView.mm
1 /*
2  * Copyright (C) 2014 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "config.h"
27 #import "WKPDFView.h"
28
29 #if PLATFORM(IOS)
30
31 #import "APIFindClient.h"
32 #import "APIUIClient.h"
33 #import "CorePDFSPI.h"
34 #import "SessionState.h"
35 #import "UIKitSPI.h"
36 #import "WKPDFPageNumberIndicator.h"
37 #import "WKWebViewInternal.h"
38 #import "WeakObjCPtr.h"
39 #import "WebPageProxy.h"
40 #import "_WKFindDelegate.h"
41 #import <MobileCoreServices/UTCoreTypes.h>
42 #import <WebCore/FloatRect.h>
43 #import <WebCore/LocalizedStrings.h>
44 #import <wtf/RetainPtr.h>
45 #import <wtf/Vector.h>
46
47 using namespace WebCore;
48
49 const CGFloat pdfPageMargin = 8;
50 const CGFloat pdfMinimumZoomScale = 1;
51 const CGFloat pdfMaximumZoomScale = 5;
52
53 const CGFloat passwordEntryFieldPadding = 10;
54
55 const float overdrawHeightMultiplier = 1.5;
56
57 static const CGFloat smartMagnificationElementPadding = 0.05;
58
59 typedef struct {
60     CGRect frame;
61     RetainPtr<UIPDFPageView> view;
62     RetainPtr<UIPDFPage> page;
63     unsigned index;
64 } PDFPageInfo;
65
66 @interface WKPDFView ()
67 - (void)_resetZoomAnimated:(BOOL)animated;
68 @end
69
70 @implementation WKPDFView {
71     RetainPtr<CGPDFDocumentRef> _cgPDFDocument;
72     RetainPtr<UIPDFDocument> _pdfDocument;
73     RetainPtr<NSString> _suggestedFilename;
74     RetainPtr<WKPDFPageNumberIndicator> _pageNumberIndicator;
75
76     RetainPtr<UIDocumentPasswordView> _passwordView;
77
78     Vector<PDFPageInfo> _pages;
79     unsigned _centerPageNumber;
80
81     CGSize _minimumSize;
82     CGSize _overlaidAccessoryViewsInset;
83     WKWebView *_webView;
84     UIScrollView *_scrollView;
85     UIView *_fixedOverlayView;
86
87     BOOL _isStartingZoom;
88     BOOL _isPerformingSameDocumentNavigation;
89
90     RetainPtr<WKActionSheetAssistant> _actionSheetAssistant;
91     WebKit::InteractionInformationAtPosition _positionInformation;
92
93     unsigned _currentFindPageIndex;
94     unsigned _currentFindMatchIndex;
95     RetainPtr<UIPDFSelection> _currentFindSelection;
96
97     RetainPtr<NSString> _cachedFindString;
98     Vector<RetainPtr<UIPDFSelection>> _cachedFindMatches;
99     unsigned _cachedFindMaximumCount;
100     _WKFindOptions _cachedFindOptionsAffectingResults;
101
102     std::atomic<unsigned> _nextComputeMatchesOperationID;
103     RetainPtr<NSString> _nextCachedFindString;
104     unsigned _nextCachedFindMaximumCount;
105     _WKFindOptions _nextCachedFindOptionsAffectingResults;
106
107     dispatch_queue_t _findQueue;
108 }
109
110 - (instancetype)web_initWithFrame:(CGRect)frame webView:(WKWebView *)webView
111 {
112     if (!(self = [super initWithFrame:frame]))
113         return nil;
114
115     self.backgroundColor = [UIColor grayColor];
116
117     _webView = webView;
118
119     _scrollView = webView.scrollView;
120     [_scrollView setMinimumZoomScale:pdfMinimumZoomScale];
121     [_scrollView setMaximumZoomScale:pdfMaximumZoomScale];
122     [_scrollView setBackgroundColor:[UIColor grayColor]];
123
124     _actionSheetAssistant = adoptNS([[WKActionSheetAssistant alloc] initWithView:self]);
125     [_actionSheetAssistant setDelegate:self];
126
127     _findQueue = dispatch_queue_create("com.apple.WebKit.WKPDFViewComputeMatchesQueue", DISPATCH_QUEUE_SERIAL);
128
129     return self;
130 }
131
132 - (void)dealloc
133 {
134     [[NSNotificationCenter defaultCenter] removeObserver:self];
135
136     [self _clearPages];
137     [_pageNumberIndicator removeFromSuperview];
138     dispatch_release(_findQueue);
139     [super dealloc];
140 }
141
142 - (NSString *)suggestedFilename
143 {
144     return _suggestedFilename.get();
145 }
146
147 - (CGPDFDocumentRef)pdfDocument
148 {
149     return [_pdfDocument CGDocument];
150 }
151
152 - (void)_clearPages
153 {
154     for (auto& page : _pages) {
155         [page.view removeFromSuperview];
156         [page.view setDelegate:nil];
157         [[page.view annotationController] setDelegate:nil];
158     }
159     
160     _pages.clear();
161 }
162
163 - (void)_didLoadPDFDocument
164 {
165     _pdfDocument = adoptNS([[UIPDFDocument alloc] initWithCGPDFDocument:_cgPDFDocument.get()]);
166
167     // FIXME: Restore the scroll position and page scale if navigating from the back/forward list.
168
169     [self _computePageAndDocumentFrames];
170     [self _revalidateViews];
171     [self _scrollToFragment:_webView.URL.fragment];
172 }
173
174 - (void)web_setContentProviderData:(NSData *)data suggestedFilename:(NSString *)filename
175 {
176     _suggestedFilename = adoptNS([filename copy]);
177
178     [self _clearPages];
179
180     RetainPtr<CGDataProviderRef> dataProvider = adoptCF(CGDataProviderCreateWithCFData((CFDataRef)data));
181     _cgPDFDocument = adoptCF(CGPDFDocumentCreateWithProvider(dataProvider.get()));
182
183     if (!_cgPDFDocument)
184         return;
185
186     if (CGPDFDocumentIsUnlocked(_cgPDFDocument.get())) {
187         [self _didLoadPDFDocument];
188         return;
189     }
190
191     [self _showPasswordEntryField];
192 }
193
194 - (void)web_setMinimumSize:(CGSize)size
195 {
196     if (_passwordView) {
197         [self _updatePasswordEntryField];
198         return;
199     }
200
201     _minimumSize = size;
202
203     CGFloat oldDocumentLeftFraction = 0;
204     CGFloat oldDocumentTopFraction = 0;
205     CGSize contentSize = _scrollView.contentSize;
206     if (contentSize.width && contentSize.height) {
207         CGPoint contentOffset = _scrollView.contentOffset;
208         UIEdgeInsets contentInset = _scrollView.contentInset;
209         oldDocumentLeftFraction = (contentOffset.x + contentInset.left) / contentSize.width;
210         oldDocumentTopFraction = (contentOffset.y + contentInset.top) / contentSize.height;
211     }
212
213     [self _computePageAndDocumentFrames];
214
215     // FIXME: This dispatch_async is unnecessary except to work around rdar://problem/15035620.
216     // Once that is resolved, we should do the setContentOffset without the dispatch_async.
217     RetainPtr<WKPDFView> retainedSelf = self;
218     dispatch_async(dispatch_get_main_queue(), [retainedSelf, oldDocumentLeftFraction, oldDocumentTopFraction] {
219         CGSize contentSize = retainedSelf->_scrollView.contentSize;
220         UIEdgeInsets contentInset = retainedSelf->_scrollView.contentInset;
221         [retainedSelf->_scrollView setContentOffset:CGPointMake((oldDocumentLeftFraction * contentSize.width) - contentInset.left, (oldDocumentTopFraction * contentSize.height) - contentInset.top) animated:NO];
222
223         [retainedSelf _revalidateViews];
224     });
225 }
226
227 - (void)scrollViewDidScroll:(UIScrollView *)scrollView
228 {
229     if (scrollView.isZoomBouncing || scrollView._isAnimatingZoom)
230         return;
231
232     [self _revalidateViews];
233
234     if (!_isPerformingSameDocumentNavigation)
235         [_pageNumberIndicator show];
236 }
237
238 - (void)_ensureViewForPage:(PDFPageInfo&)pageInfo
239 {
240     if (pageInfo.view)
241         return;
242
243     pageInfo.view = adoptNS([[UIPDFPageView alloc] initWithPage:pageInfo.page.get() tiledContent:YES]);
244     [pageInfo.view setUseBackingLayer:YES];
245     [pageInfo.view setDelegate:self];
246     [[pageInfo.view annotationController] setDelegate:self];
247     [self addSubview:pageInfo.view.get()];
248
249     [pageInfo.view setFrame:pageInfo.frame];
250     [pageInfo.view contentLayer].contentsScale = self.window.screen.scale;
251 }
252
253 - (void)_revalidateViews
254 {
255     if (_isStartingZoom)
256         return;
257
258     CGRect targetRect = [_scrollView convertRect:_scrollView.bounds toView:self];
259
260     // We apply overdraw after applying scale in order to avoid excessive
261     // memory use caused by scaling the overdraw.
262     CGRect targetRectWithOverdraw = CGRectInset(targetRect, 0, -targetRect.size.height * overdrawHeightMultiplier);
263     CGRect targetRectForCenterPage = CGRectInset(targetRect, 0, targetRect.size.height / 2 - pdfPageMargin * 2);
264
265     _centerPageNumber = 0;
266
267     for (auto& pageInfo : _pages) {
268         if (!CGRectIntersectsRect(pageInfo.frame, targetRectWithOverdraw) && pageInfo.index != _currentFindPageIndex) {
269             [pageInfo.view removeFromSuperview];
270             pageInfo.view = nullptr;
271             continue;
272         }
273
274         if (!_centerPageNumber && CGRectIntersectsRect(pageInfo.frame, targetRectForCenterPage))
275             _centerPageNumber = pageInfo.index + 1;
276
277         [self _ensureViewForPage:pageInfo];
278     }
279
280     [self _updatePageNumberIndicator];
281 }
282
283 - (CGPoint)_offsetForPageNumberIndicator
284 {
285     UIEdgeInsets contentInset = [_webView _computedContentInset];
286     return CGPointMake(contentInset.left, contentInset.top + _overlaidAccessoryViewsInset.height);
287 }
288
289 - (void)_updatePageNumberIndicator
290 {
291     if (_isPerformingSameDocumentNavigation)
292         return;
293
294     if (!_pageNumberIndicator)
295         _pageNumberIndicator = adoptNS([[WKPDFPageNumberIndicator alloc] initWithFrame:CGRectZero]);
296
297     [_fixedOverlayView addSubview:_pageNumberIndicator.get()];
298
299     [_pageNumberIndicator setCurrentPageNumber:_centerPageNumber];
300     [_pageNumberIndicator moveToPoint:[self _offsetForPageNumberIndicator] animated:NO];
301 }
302
303 - (void)web_setOverlaidAccessoryViewsInset:(CGSize)inset
304 {
305     _overlaidAccessoryViewsInset = inset;
306     [_pageNumberIndicator moveToPoint:[self _offsetForPageNumberIndicator] animated:YES];
307 }
308
309 - (void)web_computedContentInsetDidChange
310 {
311     [self _updatePageNumberIndicator];
312 }
313
314 - (void)web_setFixedOverlayView:(UIView *)fixedOverlayView
315 {
316     _fixedOverlayView = fixedOverlayView;
317
318     if (_pageNumberIndicator)
319         [_fixedOverlayView addSubview:_pageNumberIndicator.get()];
320 }
321
322 - (void)_scrollToFragment:(NSString *)fragment
323 {
324     if (![fragment hasPrefix:@"page"])
325         return;
326
327     NSInteger pageIndex = [[fragment substringFromIndex:4] integerValue] - 1;
328     if (pageIndex < 0 || static_cast<std::size_t>(pageIndex) >= _pages.size())
329         return;
330
331     _isPerformingSameDocumentNavigation = YES;
332
333     [_pageNumberIndicator hide];
334     [self _resetZoomAnimated:NO];
335
336     // Ensure that the page margin is visible below the content inset.
337     const CGFloat verticalOffset = _pages[pageIndex].frame.origin.y - _webView._computedContentInset.top - pdfPageMargin;
338     [_scrollView setContentOffset:CGPointMake(_scrollView.contentOffset.x, verticalOffset) animated:NO];
339
340     _isPerformingSameDocumentNavigation = NO;
341 }
342
343 - (void)web_didSameDocumentNavigation:(WKSameDocumentNavigationType)navigationType
344 {
345     // Check for kWKSameDocumentNavigationSessionStatePop instead of kWKSameDocumentNavigationAnchorNavigation since the
346     // latter is only called once when navigating to the same anchor in succession. If the user navigates to a page
347     // then scrolls back and clicks on the same link a second time, we want to scroll again.
348     if (navigationType != kWKSameDocumentNavigationSessionStatePop)
349         return;
350
351     // FIXME: restore the scroll position and page scale if navigating back from a fragment.
352
353     [self _scrollToFragment:_webView.URL.fragment];
354 }
355
356 - (void)_computePageAndDocumentFrames
357 {
358     if (_passwordView)
359         return;
360
361     NSUInteger pageCount = [_pdfDocument numberOfPages];
362     [_pageNumberIndicator setPageCount:pageCount];
363     
364     [self _clearPages];
365
366     _pages.reserveCapacity(pageCount);
367
368     CGRect pageFrame = CGRectMake(0, 0, _minimumSize.width, _minimumSize.height);
369     for (NSUInteger pageIndex = 0; pageIndex < pageCount; ++pageIndex) {
370         UIPDFPage *page = [_pdfDocument pageAtIndex:pageIndex];
371         if (!page)
372             continue;
373
374         CGSize pageSize = [page cropBoxAccountForRotation].size;
375         pageFrame.size.height = pageSize.height / pageSize.width * pageFrame.size.width;
376         CGRect pageFrameWithMarginApplied = CGRectInset(pageFrame, pdfPageMargin, pdfPageMargin);
377
378         PDFPageInfo pageInfo;
379         pageInfo.page = page;
380         pageInfo.frame = pageFrameWithMarginApplied;
381         pageInfo.index = pageIndex;
382         _pages.append(pageInfo);
383         pageFrame.origin.y += pageFrame.size.height - pdfPageMargin;
384     }
385
386     CGFloat scale = _scrollView.zoomScale;
387     CGRect newFrame = [self frame];
388     newFrame.size.width = _minimumSize.width * scale;
389     newFrame.size.height = std::max(pageFrame.origin.y + pdfPageMargin, _minimumSize.height) * scale;
390
391     [self setFrame:newFrame];
392     [_scrollView setContentSize:newFrame.size];
393 }
394
395 - (void)_resetZoomAnimated:(BOOL)animated
396 {
397     _isStartingZoom = YES;
398
399     CGRect scrollViewBounds = _scrollView.bounds;
400     CGPoint centerOfPageInDocumentCoordinates = [_scrollView convertPoint:CGPointMake(CGRectGetMidX(scrollViewBounds), CGRectGetMidY(scrollViewBounds)) toView:self];
401     [_webView _zoomOutWithOrigin:centerOfPageInDocumentCoordinates animated:animated];
402
403     _isStartingZoom = NO;
404 }
405
406 - (void)_highlightLinkAnnotation:(UIPDFLinkAnnotation *)linkAnnotation forDuration:(NSTimeInterval)duration completionHandler:(void (^)(void))completionHandler
407 {
408     static const CGFloat highlightBorderRadius = 3;
409     static const CGFloat highlightColorComponent = 26.0 / 255;
410     static UIColor *highlightColor = [[UIColor alloc] initWithRed:highlightColorComponent green:highlightColorComponent blue:highlightColorComponent alpha:0.3];
411
412     UIPDFPageView *pageView = linkAnnotation.annotationController.pageView;
413     CGRect highlightViewFrame = [self convertRect:[pageView convertRectFromPDFPageSpace:linkAnnotation.Rect] fromView:pageView];
414     RetainPtr<_UIHighlightView> highlightView = adoptNS([[_UIHighlightView alloc] initWithFrame:CGRectInset(highlightViewFrame, -highlightBorderRadius, -highlightBorderRadius)]);
415     [highlightView setOpaque:NO];
416     [highlightView setCornerRadius:highlightBorderRadius];
417     [highlightView setColor:highlightColor];
418
419     ASSERT(isMainThread());
420     [self addSubview:highlightView.get()];
421     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, duration * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
422         [highlightView removeFromSuperview];
423         completionHandler();
424     });
425 }
426
427 - (NSURL *)_URLForLinkAnnotation:(UIPDFLinkAnnotation *)linkAnnotation
428 {
429     NSURL *documentURL = _webView.URL;
430
431     if (NSURL *url = linkAnnotation.url)
432         return [NSURL URLWithString:url.relativeString relativeToURL:documentURL];
433
434     if (NSUInteger pageNumber = linkAnnotation.pageNumber) {
435         String anchorString = ASCIILiteral("#page");
436         anchorString.append(String::number(pageNumber));
437         return [NSURL URLWithString:anchorString relativeToURL:documentURL];
438     }
439
440     return nil;
441 }
442
443 #pragma mark Find-in-Page
444
445 static NSStringCompareOptions stringCompareOptions(_WKFindOptions options)
446 {
447     NSStringCompareOptions findOptions = 0;
448     if (options & _WKFindOptionsCaseInsensitive)
449         findOptions |= NSCaseInsensitiveSearch;
450     if (options & _WKFindOptionsBackwards)
451         findOptions |= NSBackwardsSearch;
452     return findOptions;
453 }
454
455 - (void)_computeMatchesForString:(NSString *)string options:(_WKFindOptions)options maxCount:(NSUInteger)maxCount completionHandler:(void (^)(BOOL success))completionHandler
456 {
457     if (!_pdfDocument) {
458         completionHandler(NO);
459         return;
460     }
461
462     _WKFindOptions optionsAffectingResults = options & ~_WKFindOptionsIrrelevantForBatchResults;
463
464     // If this search is equivalent to the currently cached search, bail and call the completion handler, because the existing cached results are valid.
465     if (!_cachedFindMatches.isEmpty() && [_cachedFindString isEqualToString:string] && _cachedFindOptionsAffectingResults == optionsAffectingResults && _cachedFindMaximumCount == maxCount) {
466         // Also, cancel any running search, because it will eventually replace the now-valid results.
467         ++_nextComputeMatchesOperationID;
468
469         completionHandler(YES);
470         return;
471     }
472
473     // 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.
474     if ([_nextCachedFindString isEqualToString:string] && _nextCachedFindOptionsAffectingResults == optionsAffectingResults && _nextCachedFindMaximumCount == maxCount) {
475         completionHandler(NO);
476         return;
477     }
478
479     NSStringCompareOptions findOptions = stringCompareOptions(optionsAffectingResults);
480
481     Vector<PDFPageInfo> pages = _pages;
482
483     unsigned computeMatchesOperationID = ++_nextComputeMatchesOperationID;
484     _nextCachedFindString = string;
485     _nextCachedFindOptionsAffectingResults = optionsAffectingResults;
486     _nextCachedFindMaximumCount = maxCount;
487
488     RetainPtr<WKPDFView> retainedSelf = self;
489     typeof(completionHandler) completionHandlerCopy = Block_copy(completionHandler);
490
491     dispatch_async(_findQueue, [pages, string, findOptions, optionsAffectingResults, maxCount, computeMatchesOperationID, retainedSelf, completionHandlerCopy] {
492         Vector<RetainPtr<UIPDFSelection>> matches;
493
494         for (unsigned pageIndex = 0; pageIndex < pages.size(); ++pageIndex) {
495             UIPDFSelection *match = nullptr;
496             while ((match = [pages[pageIndex].page findString:string fromSelection:match options:findOptions])) {
497                 matches.append(match);
498                 if (matches.size() > maxCount)
499                     goto maxCountExceeded;
500
501                 // If we've enqueued another search, cancel this one.
502                 if (retainedSelf->_nextComputeMatchesOperationID != computeMatchesOperationID) {
503                     dispatch_async(dispatch_get_main_queue(), [completionHandlerCopy] {
504                         completionHandlerCopy(NO);
505                         Block_release(completionHandlerCopy);
506                     });
507                     return;
508                 }
509             };
510         }
511
512     maxCountExceeded:
513         dispatch_async(dispatch_get_main_queue(), [computeMatchesOperationID, string, optionsAffectingResults, maxCount, matches, completionHandlerCopy, retainedSelf] {
514
515             // If another search has been enqueued in the meantime, ignore this result.
516             if (retainedSelf->_nextComputeMatchesOperationID != computeMatchesOperationID) {
517                 Block_release(completionHandlerCopy);
518                 return;
519             }
520
521             retainedSelf->_cachedFindString = string;
522             retainedSelf->_cachedFindOptionsAffectingResults = optionsAffectingResults;
523             retainedSelf->_cachedFindMaximumCount = maxCount;
524             retainedSelf->_cachedFindMatches = matches;
525
526             retainedSelf->_nextCachedFindString = nil;
527
528             completionHandlerCopy(YES);
529             Block_release(completionHandlerCopy);
530         });
531     });
532 }
533
534 - (void)web_countStringMatches:(NSString *)string options:(_WKFindOptions)options maxCount:(NSUInteger)maxCount
535 {
536     RefPtr<WebKit::WebPageProxy> page = _webView->_page;
537     [self _computeMatchesForString:string options:options maxCount:maxCount completionHandler:^(BOOL success) {
538         if (!success)
539             return;
540         page->findClient().didCountStringMatches(page.get(), string, _cachedFindMatches.size());
541     }];
542 }
543
544 - (void)_didFindMatch:(UIPDFSelection *)match
545 {
546     for (auto& pageInfo : _pages) {
547         if (pageInfo.page == match.page) {
548             [self _ensureViewForPage:pageInfo];
549
550             [pageInfo.view highlightSearchSelection:match animated:NO];
551
552             _currentFindPageIndex = pageInfo.index;
553             _currentFindSelection = match;
554
555             CGRect zoomRect = [pageInfo.view convertRectFromPDFPageSpace:match.bounds];
556             [self zoom:pageInfo.view.get() to:zoomRect atPoint:CGPointZero kind:kUIPDFObjectKindText];
557
558             return;
559         }
560     }
561 }
562
563 - (void)web_findString:(NSString *)string options:(_WKFindOptions)options maxCount:(NSUInteger)maxCount
564 {
565     if (_currentFindSelection)
566         [_pages[_currentFindPageIndex].view clearSearchHighlights];
567
568     RetainPtr<UIPDFSelection> previousFindSelection = _currentFindSelection;
569     unsigned previousFindPageIndex = 0;
570     if (previousFindSelection) {
571         previousFindPageIndex = _currentFindPageIndex;
572         if (![_cachedFindString isEqualToString:string]) {
573             NSUInteger location = [_currentFindSelection startIndex];
574             if (location)
575                 previousFindSelection = adoptNS([[UIPDFSelection alloc] initWithPage:[_currentFindSelection page] fromIndex:location - 1 toIndex:location]);
576         }
577     }
578
579     NSStringCompareOptions findOptions = stringCompareOptions(options);
580     bool backwards = (options & _WKFindOptionsBackwards);
581     RefPtr<WebKit::WebPageProxy> page = _webView->_page;
582
583     [self _computeMatchesForString:string options:options maxCount:maxCount completionHandler:^(BOOL success) {
584         if (!success)
585             return;
586
587         unsigned pageIndex = previousFindPageIndex;
588         for (unsigned i = 0; i < _pages.size(); ++i) {
589             UIPDFSelection *match = [_pages[pageIndex].page findString:string fromSelection:(pageIndex == previousFindPageIndex ? previousFindSelection.get() : nil) options:findOptions];
590
591             if (!match) {
592                 if (!pageIndex && backwards)
593                     pageIndex = _pages.size() - 1;
594                 else if (pageIndex == _pages.size() - 1 && !backwards)
595                     pageIndex = 0;
596                 else
597                     pageIndex += backwards ? -1 : 1;
598                 continue;
599             }
600
601             [self _didFindMatch:match];
602
603             if (_cachedFindMatches.size() <= maxCount) {
604                 _currentFindMatchIndex = 0;
605                 for (const auto& knownMatch : _cachedFindMatches) {
606                     if (match.stringRange.location == [knownMatch stringRange].location && match.stringRange.length == [knownMatch stringRange].length) {
607                         page->findClient().didFindString(page.get(), string, _cachedFindMatches.size(), _currentFindMatchIndex);
608                         break;
609                     }
610                     _currentFindMatchIndex++;
611                 }
612             }
613
614             return;
615         }
616         
617         page->findClient().didFailToFindString(page.get(), string);
618     }];
619 }
620
621 - (void)web_hideFindUI
622 {
623     if (_currentFindSelection)
624         [_pages[_currentFindPageIndex].view clearSearchHighlights];
625
626     _currentFindSelection = nullptr;
627     _cachedFindString = nullptr;
628     _cachedFindMatches.clear();
629 }
630
631 #pragma mark UIPDFPageViewDelegate
632
633 - (void)zoom:(UIPDFPageView *)pageView to:(CGRect)targetRect atPoint:(CGPoint)origin kind:(UIPDFObjectKind)kind
634 {
635     _isStartingZoom = YES;
636
637     BOOL isImage = kind == kUIPDFObjectKindGraphic;
638
639     if (!isImage)
640         targetRect = CGRectInset(targetRect, smartMagnificationElementPadding * targetRect.size.width, smartMagnificationElementPadding * targetRect.size.height);
641
642     CGRect rectInDocumentCoordinates = [pageView convertRect:targetRect toView:self];
643     CGPoint originInDocumentCoordinates = [pageView convertPoint:origin toView:self];
644
645     [_webView _zoomToRect:rectInDocumentCoordinates withOrigin:originInDocumentCoordinates fitEntireRect:isImage minimumScale:pdfMinimumZoomScale maximumScale:pdfMaximumZoomScale minimumScrollDistance:0];
646
647     _isStartingZoom = NO;
648 }
649
650 - (void)resetZoom:(UIPDFPageView *)pageView
651 {
652     [self _resetZoomAnimated:YES];
653 }
654
655 #pragma mark UIPDFAnnotationControllerDelegate
656
657 - (void)annotation:(UIPDFAnnotation *)annotation wasTouchedAtPoint:(CGPoint)point controller:(UIPDFAnnotationController *)controller
658 {
659     if (![annotation isKindOfClass:[UIPDFLinkAnnotation class]])
660         return;
661
662     UIPDFLinkAnnotation *linkAnnotation = (UIPDFLinkAnnotation *)annotation;
663     RetainPtr<NSURL> url = [self _URLForLinkAnnotation:linkAnnotation];
664     if (!url)
665         return;
666
667     CGPoint documentPoint = [controller.pageView convertPoint:point toView:self];
668     CGPoint screenPoint = [self.window convertPoint:[self convertPoint:documentPoint toView:nil] toWindow:nil];
669     RetainPtr<WKWebView> retainedWebView = _webView;
670
671     [self _highlightLinkAnnotation:linkAnnotation forDuration:.2 completionHandler:^{
672         retainedWebView->_page->navigateToPDFLinkWithSimulatedClick([url absoluteString], roundedIntPoint(documentPoint), roundedIntPoint(screenPoint));
673     }];
674 }
675
676 - (void)annotation:(UIPDFAnnotation *)annotation isBeingPressedAtPoint:(CGPoint)point controller:(UIPDFAnnotationController *)controller
677 {
678     if (![annotation isKindOfClass:[UIPDFLinkAnnotation class]])
679         return;
680
681     UIPDFLinkAnnotation *linkAnnotation = (UIPDFLinkAnnotation *)annotation;
682     NSURL *url = [self _URLForLinkAnnotation:linkAnnotation];
683     if (!url)
684         return;
685
686     _positionInformation.url = url.absoluteString;
687     _positionInformation.point = roundedIntPoint([controller.pageView convertPoint:point toView:self]);
688     _positionInformation.bounds = roundedIntRect([self convertRect:[controller.pageView convertRectFromPDFPageSpace:annotation.Rect] fromView:controller.pageView]);
689
690     [self _highlightLinkAnnotation:linkAnnotation forDuration:.75 completionHandler:^{
691         [_actionSheetAssistant showLinkSheet];
692     }];
693 }
694
695 #pragma mark WKActionSheetAssistantDelegate
696
697 - (const WebKit::InteractionInformationAtPosition&)positionInformationForActionSheetAssistant:(WKActionSheetAssistant *)assistant
698 {
699     return _positionInformation;
700 }
701
702 - (void)actionSheetAssistant:(WKActionSheetAssistant *)assistant performAction:(WebKit::SheetAction)action
703 {
704     if (action != WebKit::SheetAction::Copy)
705         return;
706
707     NSDictionary *representations = @{
708         (NSString *)kUTTypeUTF8PlainText : _positionInformation.url,
709         (NSString *)kUTTypeURL : [NSURL URLWithString:_positionInformation.url]
710     };
711
712     [UIPasteboard generalPasteboard].items = @[ representations ];
713 }
714
715 - (void)actionSheetAssistant:(WKActionSheetAssistant *)assistant openElementAtLocation:(CGPoint)location
716 {
717     CGPoint screenPoint = [self.window convertPoint:[self convertPoint:location toView:nil] toWindow:nil];
718     _webView->_page->navigateToPDFLinkWithSimulatedClick(_positionInformation.url, roundedIntPoint(location), roundedIntPoint(screenPoint));
719 }
720
721 #if HAVE(APP_LINKS)
722 - (BOOL)actionSheetAssistant:(WKActionSheetAssistant *)assistant shouldIncludeAppLinkActionsForElement:(_WKActivatedElementInfo *)element
723 {
724     return _webView->_page->uiClient().shouldIncludeAppLinkActionsForElement(element);
725 }
726 #endif
727
728 - (RetainPtr<NSArray>)actionSheetAssistant:(WKActionSheetAssistant *)assistant decideActionsForElement:(_WKActivatedElementInfo *)element defaultActions:(RetainPtr<NSArray>)defaultActions
729 {
730     return _webView->_page->uiClient().actionsForElement(element, WTF::move(defaultActions));
731 }
732
733 #pragma mark Password protection UI
734
735 - (void)_updatePasswordEntryField
736 {
737     [_passwordView setFrame:CGRectMake(0, 0, _webView.bounds.size.width, _webView.bounds.size.height)];
738     [_scrollView setContentSize:[_passwordView bounds].size];
739 }
740
741 - (void)_keyboardDidShow:(NSNotification *)notification
742 {
743     UITextField *passwordField = [_passwordView passwordField];
744     if (!passwordField.isEditing)
745         return;
746
747     CGRect keyboardRect = [UIPeripheralHost visiblePeripheralFrame];
748     if (CGRectIsEmpty(keyboardRect))
749         return;
750
751     UIWindow *window = _scrollView.window;
752     keyboardRect = [window convertRect:keyboardRect fromWindow:nil];
753     keyboardRect = [_scrollView convertRect:keyboardRect fromView:window];
754
755     CGRect passwordFieldFrame = [passwordField convertRect:passwordField.bounds toView:_scrollView];
756
757     CGSize contentSize = [_passwordView bounds].size;
758     contentSize.height += CGRectGetHeight(keyboardRect);
759     [_scrollView setContentSize:contentSize];
760
761     if (CGRectIntersectsRect(passwordFieldFrame, keyboardRect)) {
762         CGFloat yDelta = CGRectGetMaxY(passwordFieldFrame) - CGRectGetMinY(keyboardRect);
763
764         CGPoint contentOffset = _scrollView.contentOffset;
765         contentOffset.y += yDelta + passwordEntryFieldPadding;
766
767         [_scrollView setContentOffset:contentOffset animated:YES];
768     }
769 }
770
771 - (void)_showPasswordEntryField
772 {
773     [_scrollView setMinimumZoomScale:1];
774     [_scrollView setMaximumZoomScale:1];
775     [_scrollView setBackgroundColor:[UIColor groupTableViewBackgroundColor]];
776
777     _passwordView = adoptNS([[UIDocumentPasswordView alloc] initWithDocumentName:_suggestedFilename.get()]);
778     [_passwordView setPasswordDelegate:self];
779
780     [self _updatePasswordEntryField];
781
782     [self addSubview:_passwordView.get()];
783 }
784
785 - (void)_hidePasswordEntryField
786 {
787     [_passwordView removeFromSuperview];
788     _passwordView = nil;
789
790     [_scrollView setMinimumZoomScale:pdfMinimumZoomScale];
791     [_scrollView setMaximumZoomScale:pdfMaximumZoomScale];
792     [_scrollView setBackgroundColor:[UIColor grayColor]];
793 }
794
795 - (void)userDidEnterPassword:(NSString *)password forPasswordView:(UIDocumentPasswordView *)passwordView
796 {
797     [self _tryToUnlockWithPassword:password];
798 }
799
800 - (void)didBeginEditingPassword:(UITextField *)passwordField inView:(UIDocumentPasswordView *)passwordView
801 {
802     [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_keyboardDidShow:) name:UIKeyboardDidShowNotification object:nil];
803 }
804
805 - (void)didEndEditingPassword:(UITextField *)passwordField inView:(UIDocumentPasswordView *)passwordView
806 {
807     [_scrollView setContentSize:[_passwordView frame].size];
808     [_scrollView setContentOffset:CGPointMake(-_scrollView.contentInset.left, -_scrollView.contentInset.top) animated:YES];
809
810     [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardDidShowNotification object:nil];
811 }
812
813 - (void)_didFailToUnlock
814 {
815     [[_passwordView passwordField] setText:@""];
816     UIAlertController* alert = [UIAlertController alertControllerWithTitle:WEB_UI_STRING("The document could not be opened with that password.", "PDF password failure alert message") message:@"" preferredStyle:UIAlertControllerStyleAlert];
817
818     UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:WEB_UI_STRING_KEY("OK", "OK (PDF password failure alert)", "OK button label in PDF password failure alert") style:UIAlertActionStyleDefault handler:[](UIAlertAction *) { }];
819     
820     [alert addAction:defaultAction];
821
822     [self.window.rootViewController presentViewController:alert animated:YES completion:nil];
823 }
824
825 - (BOOL)_tryToUnlockWithPassword:(NSString *)password
826 {
827     if (CGPDFDocumentUnlockWithPassword(_cgPDFDocument.get(), [password UTF8String])) {
828         [self _hidePasswordEntryField];
829         [self _didLoadPDFDocument];
830         return YES;
831     }
832
833     [self _didFailToUnlock];
834     return NO;
835 }
836
837 @end
838
839 #endif /* PLATFORM(IOS) */