[iOS] Teach WKPDFView to navigate to pageNumber links
[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 "SessionState.h"
32 #import "WKPDFPageNumberIndicator.h"
33 #import "WKWebViewInternal.h"
34 #import "WebPageProxy.h"
35 #import <CorePDF/UIPDFDocument.h>
36 #import <CorePDF/UIPDFLinkAnnotation.h>
37 #import <CorePDF/UIPDFPage.h>
38 #import <CorePDF/UIPDFPageView.h>
39 #import <UIKit/UIScrollView_Private.h>
40 #import <WebCore/FloatRect.h>
41 #import <chrono>
42 #import <wtf/RetainPtr.h>
43 #import <wtf/Vector.h>
44
45 using namespace WebCore;
46
47 const CGFloat pdfPageMargin = 8;
48 const CGFloat pdfMinimumZoomScale = 1;
49 const CGFloat pdfMaximumZoomScale = 5;
50
51 const float overdrawHeightMultiplier = 1.5;
52
53 static const CGFloat smartMagnificationElementPadding = 0.05;
54
55 typedef struct {
56     CGRect frame;
57     RetainPtr<UIPDFPageView> view;
58     RetainPtr<UIPDFPage> page;
59 } PDFPageInfo;
60
61 @interface WKPDFView ()
62 - (void)_resetZoomAnimated:(BOOL)animated;
63 @end
64
65 @implementation WKPDFView {
66     RetainPtr<UIPDFDocument> _pdfDocument;
67     RetainPtr<NSString> _suggestedFilename;
68     RetainPtr<WKPDFPageNumberIndicator> _pageNumberIndicator;
69
70     Vector<PDFPageInfo> _pages;
71     unsigned _centerPageNumber;
72
73     CGSize _minimumSize;
74     CGSize _overlaidAccessoryViewsInset;
75     WKWebView *_webView;
76     UIScrollView *_scrollView;
77     UIView *_fixedOverlayView;
78
79     BOOL _isStartingZoom;
80     BOOL _isPerformingSameDocumentNavigation;
81 }
82
83 - (instancetype)web_initWithFrame:(CGRect)frame webView:(WKWebView *)webView
84 {
85     if (!(self = [super initWithFrame:frame]))
86         return nil;
87
88     self.backgroundColor = [UIColor grayColor];
89
90     _webView = webView;
91
92     _scrollView = webView.scrollView;
93     [_scrollView setMinimumZoomScale:pdfMinimumZoomScale];
94     [_scrollView setMaximumZoomScale:pdfMaximumZoomScale];
95     [_scrollView setBackgroundColor:[UIColor grayColor]];
96
97     return self;
98 }
99
100 - (void)dealloc
101 {
102     [self _clearPages];
103     [_pageNumberIndicator removeFromSuperview];
104     [super dealloc];
105 }
106
107 - (NSString *)suggestedFilename
108 {
109     return _suggestedFilename.get();
110 }
111
112 - (CGPDFDocumentRef)pdfDocument
113 {
114     return [_pdfDocument CGDocument];
115 }
116
117 - (void)_clearPages
118 {
119     for (auto& page : _pages) {
120         [page.view removeFromSuperview];
121         [page.view setDelegate:nil];
122         [[page.view annotationController] setDelegate:nil];
123     }
124     
125     _pages.clear();
126 }
127
128 - (void)web_setContentProviderData:(NSData *)data suggestedFilename:(NSString *)filename
129 {
130     _suggestedFilename = adoptNS([filename copy]);
131
132     [self _clearPages];
133
134     RetainPtr<CGDataProvider> dataProvider = adoptCF(CGDataProviderCreateWithCFData((CFDataRef)data));
135     RetainPtr<CGPDFDocumentRef> cgPDFDocument = adoptCF(CGPDFDocumentCreateWithProvider(dataProvider.get()));
136     _pdfDocument = adoptNS([[UIPDFDocument alloc] initWithCGPDFDocument:cgPDFDocument.get()]);
137
138     // FIXME: restore the scroll position and page scale if navigating from the back/forward list.
139
140     [self _computePageAndDocumentFrames];
141     [self _revalidateViews];
142 }
143
144 - (void)web_setMinimumSize:(CGSize)size
145 {
146     _minimumSize = size;
147
148     [self _computePageAndDocumentFrames];
149     [self _revalidateViews];
150 }
151
152 - (void)scrollViewDidScroll:(UIScrollView *)scrollView
153 {
154     if (scrollView.isZoomBouncing || scrollView._isAnimatingZoom)
155         return;
156
157     [self _revalidateViews];
158
159     if (!_isPerformingSameDocumentNavigation)
160         [_pageNumberIndicator show];
161 }
162
163 - (void)_revalidateViews
164 {
165     if (_isStartingZoom)
166         return;
167
168     CGRect targetRect = [_scrollView convertRect:_scrollView.bounds toView:self];
169
170     // We apply overdraw after applying scale in order to avoid excessive
171     // memory use caused by scaling the overdraw.
172     CGRect targetRectWithOverdraw = CGRectInset(targetRect, 0, -targetRect.size.height * overdrawHeightMultiplier);
173     CGRect targetRectForCenterPage = CGRectInset(targetRect, 0, targetRect.size.height / 2 - pdfPageMargin * 2);
174
175     _centerPageNumber = 0;
176     unsigned currentPage = 0;
177
178     for (auto& pageInfo : _pages) {
179         ++currentPage;
180
181         if (!CGRectIntersectsRect(pageInfo.frame, targetRectWithOverdraw)) {
182             [pageInfo.view removeFromSuperview];
183             pageInfo.view = nullptr;
184             continue;
185         }
186
187         if (!_centerPageNumber && CGRectIntersectsRect(pageInfo.frame, targetRectForCenterPage))
188             _centerPageNumber = currentPage;
189
190         if (pageInfo.view)
191             continue;
192
193         pageInfo.view = adoptNS([[UIPDFPageView alloc] initWithPage:pageInfo.page.get() tiledContent:YES]);
194         [pageInfo.view setUseBackingLayer:YES];
195         [pageInfo.view setDelegate:self];
196         [[pageInfo.view annotationController] setDelegate:self];
197         [self addSubview:pageInfo.view.get()];
198
199         [pageInfo.view setFrame:pageInfo.frame];
200         [pageInfo.view contentLayer].contentsScale = self.window.screen.scale;
201     }
202
203     [self _updatePageNumberIndicator];
204 }
205
206 - (CGPoint)_offsetForPageNumberIndicator
207 {
208     UIEdgeInsets contentInset = [_webView _computedContentInset];
209     return CGPointMake(contentInset.left, contentInset.top + _overlaidAccessoryViewsInset.height);
210 }
211
212 - (void)_updatePageNumberIndicator
213 {
214     if (_isPerformingSameDocumentNavigation)
215         return;
216
217     if (!_pageNumberIndicator)
218         _pageNumberIndicator = adoptNS([[WKPDFPageNumberIndicator alloc] initWithFrame:CGRectZero]);
219
220     [_fixedOverlayView addSubview:_pageNumberIndicator.get()];
221
222     [_pageNumberIndicator setCurrentPageNumber:_centerPageNumber];
223     [_pageNumberIndicator moveToPoint:[self _offsetForPageNumberIndicator] animated:NO];
224 }
225
226 - (void)web_setOverlaidAccessoryViewsInset:(CGSize)inset
227 {
228     _overlaidAccessoryViewsInset = inset;
229     [_pageNumberIndicator moveToPoint:[self _offsetForPageNumberIndicator] animated:YES];
230 }
231
232 - (void)web_computedContentInsetDidChange
233 {
234     [self _updatePageNumberIndicator];
235 }
236
237 - (void)web_setFixedOverlayView:(UIView *)fixedOverlayView
238 {
239     _fixedOverlayView = fixedOverlayView;
240
241     if (_pageNumberIndicator)
242         [_fixedOverlayView addSubview:_pageNumberIndicator.get()];
243 }
244
245 - (void)web_didSameDocumentNavigation:(WKSameDocumentNavigationType)navigationType
246 {
247     // Check for kWKSameDocumentNavigationSessionStatePop instead of kWKSameDocumentNavigationAnchorNavigation since the
248     // latter is only called once when navigating to the same anchor in succession. If the user navigates to a page
249     // then scrolls back and clicks on the same link a second time, we want to scroll again.
250     if (navigationType != kWKSameDocumentNavigationSessionStatePop)
251         return;
252
253     // FIXME: restore the scroll position and page scale if navigating back from a fragment.
254
255     NSString *fragment = _webView.URL.fragment;
256     if (![fragment hasPrefix:@"page"])
257         return;
258
259     NSInteger pageIndex = [[fragment substringFromIndex:4] integerValue] - 1;
260     if (pageIndex < 0 || static_cast<std::size_t>(pageIndex) >= _pages.size())
261         return;
262
263     _isPerformingSameDocumentNavigation = YES;
264
265     [_pageNumberIndicator hide];
266     [self _resetZoomAnimated:NO];
267
268     // Ensure that the page margin is visible below the content inset.
269     const CGFloat verticalOffset = _pages[pageIndex].frame.origin.y - _webView._computedContentInset.top - pdfPageMargin;
270     [_scrollView setContentOffset:CGPointMake(_scrollView.contentOffset.x, verticalOffset) animated:NO];
271
272     _isPerformingSameDocumentNavigation = NO;
273 }
274
275 - (void)_computePageAndDocumentFrames
276 {
277     NSUInteger pageCount = [_pdfDocument numberOfPages];
278     [_pageNumberIndicator setPageCount:pageCount];
279     
280     [self _clearPages];
281
282     _pages.reserveCapacity(pageCount);
283
284     CGRect pageFrame = CGRectMake(0, 0, _minimumSize.width, _minimumSize.height);
285     for (NSUInteger pageNumber = 0; pageNumber < pageCount; ++pageNumber) {
286         UIPDFPage *page = [_pdfDocument pageAtIndex:pageNumber];
287         if (!page)
288             continue;
289
290         CGSize pageSize = [page cropBoxAccountForRotation].size;
291         pageFrame.size.height = pageSize.height / pageSize.width * pageFrame.size.width;
292         CGRect pageFrameWithMarginApplied = CGRectInset(pageFrame, pdfPageMargin, pdfPageMargin);
293
294         PDFPageInfo pageInfo;
295         pageInfo.page = page;
296         pageInfo.frame = pageFrameWithMarginApplied;
297         _pages.append(pageInfo);
298         pageFrame.origin.y += pageFrame.size.height - pdfPageMargin;
299     }
300
301     CGFloat scale = _scrollView.zoomScale;
302     CGRect newFrame = [self frame];
303     newFrame.size.width = _minimumSize.width * scale;
304     newFrame.size.height = std::max(pageFrame.origin.y + pdfPageMargin, _minimumSize.height) * scale;
305
306     [self setFrame:newFrame];
307     [_scrollView setContentSize:newFrame.size];
308 }
309
310 - (void)_resetZoomAnimated:(BOOL)animated
311 {
312     _isStartingZoom = YES;
313
314     CGRect scrollViewBounds = _scrollView.bounds;
315     CGPoint centerOfPageInDocumentCoordinates = [_scrollView convertPoint:CGPointMake(CGRectGetMidX(scrollViewBounds), CGRectGetMidY(scrollViewBounds)) toView:self];
316     [_webView _zoomOutWithOrigin:centerOfPageInDocumentCoordinates animated:animated];
317
318     _isStartingZoom = NO;
319 }
320
321 #pragma mark UIPDFPageViewDelegate
322
323 - (void)zoom:(UIPDFPageView *)pageView to:(CGRect)targetRect atPoint:(CGPoint)origin kind:(UIPDFObjectKind)kind
324 {
325     _isStartingZoom = YES;
326
327     BOOL isImage = kind == kUIPDFObjectKindGraphic;
328
329     if (!isImage)
330         targetRect = CGRectInset(targetRect, smartMagnificationElementPadding * targetRect.size.width, smartMagnificationElementPadding * targetRect.size.height);
331
332     CGRect rectInDocumentCoordinates = [pageView convertRect:targetRect toView:self];
333     CGPoint originInDocumentCoordinates = [pageView convertPoint:origin toView:self];
334
335     [_webView _zoomToRect:rectInDocumentCoordinates withOrigin:originInDocumentCoordinates fitEntireRect:isImage minimumScale:pdfMinimumZoomScale maximumScale:pdfMaximumZoomScale minimumScrollDistance:0];
336
337     _isStartingZoom = NO;
338 }
339
340 - (void)resetZoom:(UIPDFPageView *)pageView
341 {
342     [self _resetZoomAnimated:YES];
343 }
344
345 #pragma mark UIPDFAnnotationControllerDelegate
346
347 - (void)annotation:(UIPDFAnnotation *)annotation wasTouchedAtPoint:(CGPoint)point controller:(UIPDFAnnotationController *)controller
348 {
349     ASSERT(isMainThread());
350
351     if (![annotation isKindOfClass:[UIPDFLinkAnnotation class]])
352         return;
353
354     UIPDFLinkAnnotation *linkAnnotation = (UIPDFLinkAnnotation *)annotation;
355     String urlString;
356     if (NSURL *url = linkAnnotation.url)
357         urlString = url.absoluteString;
358     else if (NSUInteger pageNumber = linkAnnotation.pageNumber) {
359         urlString = ASCIILiteral("#page");
360         urlString.append(String::number(pageNumber));
361     }
362
363     if (urlString.isEmpty())
364         return;
365
366     CGPoint documentPoint = [controller.pageView convertPoint:point toView:self];
367     CGPoint screenPoint = [self.window convertPoint:[self convertPoint:documentPoint toView:nil] toWindow:nil];
368     static const int64_t dispatchOffset = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::milliseconds(200)).count();
369     RetainPtr<WKWebView> retainedWebView = _webView;
370
371     // Call navigateToURLWithSimulatedClick() on a delay so that a tap highlight can be shown.
372     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, dispatchOffset), dispatch_get_main_queue(), ^ {
373         retainedWebView->_page->navigateToURLWithSimulatedClick(urlString, roundedIntPoint(documentPoint), roundedIntPoint(screenPoint));
374     });
375 }
376
377 @end
378
379 #endif /* PLATFORM(IOS) */