2 * Copyright (C) 2014 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
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.
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.
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>
42 #import <wtf/RetainPtr.h>
43 #import <wtf/Vector.h>
45 using namespace WebCore;
47 const CGFloat pdfPageMargin = 8;
48 const CGFloat pdfMinimumZoomScale = 1;
49 const CGFloat pdfMaximumZoomScale = 5;
51 const float overdrawHeightMultiplier = 1.5;
53 static const CGFloat smartMagnificationElementPadding = 0.05;
57 RetainPtr<UIPDFPageView> view;
58 RetainPtr<UIPDFPage> page;
61 @interface WKPDFView ()
62 - (void)_resetZoomAnimated:(BOOL)animated;
65 @implementation WKPDFView {
66 RetainPtr<UIPDFDocument> _pdfDocument;
67 RetainPtr<NSString> _suggestedFilename;
68 RetainPtr<WKPDFPageNumberIndicator> _pageNumberIndicator;
70 Vector<PDFPageInfo> _pages;
71 unsigned _centerPageNumber;
74 CGSize _overlaidAccessoryViewsInset;
76 UIScrollView *_scrollView;
77 UIView *_fixedOverlayView;
80 BOOL _isPerformingSameDocumentNavigation;
83 - (instancetype)web_initWithFrame:(CGRect)frame webView:(WKWebView *)webView
85 if (!(self = [super initWithFrame:frame]))
88 self.backgroundColor = [UIColor grayColor];
92 _scrollView = webView.scrollView;
93 [_scrollView setMinimumZoomScale:pdfMinimumZoomScale];
94 [_scrollView setMaximumZoomScale:pdfMaximumZoomScale];
95 [_scrollView setBackgroundColor:[UIColor grayColor]];
103 [_pageNumberIndicator removeFromSuperview];
107 - (NSString *)suggestedFilename
109 return _suggestedFilename.get();
112 - (CGPDFDocumentRef)pdfDocument
114 return [_pdfDocument CGDocument];
119 for (auto& page : _pages) {
120 [page.view removeFromSuperview];
121 [page.view setDelegate:nil];
122 [[page.view annotationController] setDelegate:nil];
128 - (void)web_setContentProviderData:(NSData *)data suggestedFilename:(NSString *)filename
130 _suggestedFilename = adoptNS([filename copy]);
134 RetainPtr<CGDataProvider> dataProvider = adoptCF(CGDataProviderCreateWithCFData((CFDataRef)data));
135 RetainPtr<CGPDFDocumentRef> cgPDFDocument = adoptCF(CGPDFDocumentCreateWithProvider(dataProvider.get()));
136 _pdfDocument = adoptNS([[UIPDFDocument alloc] initWithCGPDFDocument:cgPDFDocument.get()]);
138 // FIXME: restore the scroll position and page scale if navigating from the back/forward list.
140 [self _computePageAndDocumentFrames];
141 [self _revalidateViews];
144 - (void)web_setMinimumSize:(CGSize)size
148 [self _computePageAndDocumentFrames];
149 [self _revalidateViews];
152 - (void)scrollViewDidScroll:(UIScrollView *)scrollView
154 if (scrollView.isZoomBouncing || scrollView._isAnimatingZoom)
157 [self _revalidateViews];
159 if (!_isPerformingSameDocumentNavigation)
160 [_pageNumberIndicator show];
163 - (void)_revalidateViews
168 CGRect targetRect = [_scrollView convertRect:_scrollView.bounds toView:self];
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);
175 _centerPageNumber = 0;
176 unsigned currentPage = 0;
178 for (auto& pageInfo : _pages) {
181 if (!CGRectIntersectsRect(pageInfo.frame, targetRectWithOverdraw)) {
182 [pageInfo.view removeFromSuperview];
183 pageInfo.view = nullptr;
187 if (!_centerPageNumber && CGRectIntersectsRect(pageInfo.frame, targetRectForCenterPage))
188 _centerPageNumber = currentPage;
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()];
199 [pageInfo.view setFrame:pageInfo.frame];
200 [pageInfo.view contentLayer].contentsScale = self.window.screen.scale;
203 [self _updatePageNumberIndicator];
206 - (CGPoint)_offsetForPageNumberIndicator
208 UIEdgeInsets contentInset = [_webView _computedContentInset];
209 return CGPointMake(contentInset.left, contentInset.top + _overlaidAccessoryViewsInset.height);
212 - (void)_updatePageNumberIndicator
214 if (_isPerformingSameDocumentNavigation)
217 if (!_pageNumberIndicator)
218 _pageNumberIndicator = adoptNS([[WKPDFPageNumberIndicator alloc] initWithFrame:CGRectZero]);
220 [_fixedOverlayView addSubview:_pageNumberIndicator.get()];
222 [_pageNumberIndicator setCurrentPageNumber:_centerPageNumber];
223 [_pageNumberIndicator moveToPoint:[self _offsetForPageNumberIndicator] animated:NO];
226 - (void)web_setOverlaidAccessoryViewsInset:(CGSize)inset
228 _overlaidAccessoryViewsInset = inset;
229 [_pageNumberIndicator moveToPoint:[self _offsetForPageNumberIndicator] animated:YES];
232 - (void)web_computedContentInsetDidChange
234 [self _updatePageNumberIndicator];
237 - (void)web_setFixedOverlayView:(UIView *)fixedOverlayView
239 _fixedOverlayView = fixedOverlayView;
241 if (_pageNumberIndicator)
242 [_fixedOverlayView addSubview:_pageNumberIndicator.get()];
245 - (void)web_didSameDocumentNavigation:(WKSameDocumentNavigationType)navigationType
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)
253 // FIXME: restore the scroll position and page scale if navigating back from a fragment.
255 NSString *fragment = _webView.URL.fragment;
256 if (![fragment hasPrefix:@"page"])
259 NSInteger pageIndex = [[fragment substringFromIndex:4] integerValue] - 1;
260 if (pageIndex < 0 || static_cast<std::size_t>(pageIndex) >= _pages.size())
263 _isPerformingSameDocumentNavigation = YES;
265 [_pageNumberIndicator hide];
266 [self _resetZoomAnimated:NO];
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];
272 _isPerformingSameDocumentNavigation = NO;
275 - (void)_computePageAndDocumentFrames
277 NSUInteger pageCount = [_pdfDocument numberOfPages];
278 [_pageNumberIndicator setPageCount:pageCount];
282 _pages.reserveCapacity(pageCount);
284 CGRect pageFrame = CGRectMake(0, 0, _minimumSize.width, _minimumSize.height);
285 for (NSUInteger pageNumber = 0; pageNumber < pageCount; ++pageNumber) {
286 UIPDFPage *page = [_pdfDocument pageAtIndex:pageNumber];
290 CGSize pageSize = [page cropBoxAccountForRotation].size;
291 pageFrame.size.height = pageSize.height / pageSize.width * pageFrame.size.width;
292 CGRect pageFrameWithMarginApplied = CGRectInset(pageFrame, pdfPageMargin, pdfPageMargin);
294 PDFPageInfo pageInfo;
295 pageInfo.page = page;
296 pageInfo.frame = pageFrameWithMarginApplied;
297 _pages.append(pageInfo);
298 pageFrame.origin.y += pageFrame.size.height - pdfPageMargin;
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;
306 [self setFrame:newFrame];
307 [_scrollView setContentSize:newFrame.size];
310 - (void)_resetZoomAnimated:(BOOL)animated
312 _isStartingZoom = YES;
314 CGRect scrollViewBounds = _scrollView.bounds;
315 CGPoint centerOfPageInDocumentCoordinates = [_scrollView convertPoint:CGPointMake(CGRectGetMidX(scrollViewBounds), CGRectGetMidY(scrollViewBounds)) toView:self];
316 [_webView _zoomOutWithOrigin:centerOfPageInDocumentCoordinates animated:animated];
318 _isStartingZoom = NO;
321 #pragma mark UIPDFPageViewDelegate
323 - (void)zoom:(UIPDFPageView *)pageView to:(CGRect)targetRect atPoint:(CGPoint)origin kind:(UIPDFObjectKind)kind
325 _isStartingZoom = YES;
327 BOOL isImage = kind == kUIPDFObjectKindGraphic;
330 targetRect = CGRectInset(targetRect, smartMagnificationElementPadding * targetRect.size.width, smartMagnificationElementPadding * targetRect.size.height);
332 CGRect rectInDocumentCoordinates = [pageView convertRect:targetRect toView:self];
333 CGPoint originInDocumentCoordinates = [pageView convertPoint:origin toView:self];
335 [_webView _zoomToRect:rectInDocumentCoordinates withOrigin:originInDocumentCoordinates fitEntireRect:isImage minimumScale:pdfMinimumZoomScale maximumScale:pdfMaximumZoomScale minimumScrollDistance:0];
337 _isStartingZoom = NO;
340 - (void)resetZoom:(UIPDFPageView *)pageView
342 [self _resetZoomAnimated:YES];
345 #pragma mark UIPDFAnnotationControllerDelegate
347 - (void)annotation:(UIPDFAnnotation *)annotation wasTouchedAtPoint:(CGPoint)point controller:(UIPDFAnnotationController *)controller
349 ASSERT(isMainThread());
351 if (![annotation isKindOfClass:[UIPDFLinkAnnotation class]])
354 UIPDFLinkAnnotation *linkAnnotation = (UIPDFLinkAnnotation *)annotation;
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));
363 if (urlString.isEmpty())
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;
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));
379 #endif /* PLATFORM(IOS) */