| /* |
| * Copyright (C) 2006, 2007, 2008, 2010, 2011 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. |
| */ |
| |
| #if PLATFORM(IOS_FAMILY) |
| |
| #import "WebPDFViewPlaceholder.h" |
| |
| #import "WebFrameInternal.h" |
| #import "WebPDFViewIOS.h" |
| #import <JavaScriptCore/JSContextRef.h> |
| #import <JavaScriptCore/OpaqueJSString.h> |
| #import <WebCore/DataTransfer.h> |
| #import <WebCore/EventHandler.h> |
| #import <WebCore/EventNames.h> |
| #import <WebCore/FormState.h> |
| #import <WebCore/Frame.h> |
| #import <WebCore/FrameLoadRequest.h> |
| #import <WebCore/FrameLoader.h> |
| #import <WebCore/HTMLFormElement.h> |
| #import <WebCore/MouseEvent.h> |
| #import <WebKitLegacy/WebDataSourcePrivate.h> |
| #import <WebKitLegacy/WebFramePrivate.h> |
| #import <WebKitLegacy/WebJSPDFDoc.h> |
| #import <WebKitLegacy/WebNSURLExtras.h> |
| #import <WebKitLegacy/WebNSViewExtras.h> |
| #import <WebKitLegacy/WebPDFDocumentExtras.h> |
| #import <WebKitLegacy/WebViewPrivate.h> |
| #import <wtf/MonotonicTime.h> |
| #import <wtf/SoftLinking.h> |
| #import <wtf/Vector.h> |
| |
| using namespace WebCore; |
| |
| #pragma mark Constants |
| |
| static const float PAGE_WIDTH_INSET = 4.0f * 2.0f; |
| static const float PAGE_HEIGHT_INSET = 4.0f * 2.0f; |
| |
| #pragma mark NSValue helpers |
| |
| @interface NSValue (_web_Extensions) |
| + (NSValue *)_web_valueWithCGRect:(CGRect)rect; |
| @end |
| |
| @implementation NSValue (_web_Extensions) |
| |
| + (NSValue *)_web_valueWithCGRect:(CGRect)rect |
| { |
| return [NSValue valueWithBytes:&rect objCType:@encode(CGRect)]; |
| } |
| |
| - (CGRect)_web_CGRectValue |
| { |
| CGRect result; |
| [self getValue:&result]; |
| return result; |
| } |
| |
| @end |
| |
| #pragma mark - |
| |
| @interface WebPDFViewPlaceholder () |
| |
| - (void)_evaluateJSForDocument:(CGPDFDocumentRef)document; |
| - (void)_updateTitleForDocumentIfAvailable; |
| - (void)_updateTitleForURL:(NSURL *)URL; |
| - (CGSize)_computePageRects:(CGPDFDocumentRef)document; |
| |
| @property (retain) NSArray *pageRects; |
| @property (retain) NSArray *pageYOrigins; |
| @property (retain) NSString *title; |
| |
| @end |
| |
| #pragma mark WebPDFViewPlaceholder implementation |
| |
| @implementation WebPDFViewPlaceholder { |
| NSString *_title; |
| NSArray *_pageRects; |
| NSArray *_pageYOrigins; |
| CGPDFDocumentRef _document; |
| WebDataSource *_dataSource; // weak to prevent cycles. |
| |
| NSObject<WebPDFViewPlaceholderDelegate> *_delegate; |
| |
| BOOL _didFinishLoad; |
| |
| CGSize _containerSize; |
| } |
| |
| @synthesize delegate = _delegate; |
| @synthesize pageRects = _pageRects; |
| @synthesize pageYOrigins = _pageYOrigins; |
| @synthesize document = _document; |
| @synthesize title = _title; |
| @synthesize containerSize = _containerSize; |
| |
| - (CGPDFDocumentRef)document |
| { |
| @synchronized(self) { |
| if (_document) |
| return _document; |
| } |
| |
| if ([self.delegate respondsToSelector:@selector(cgPDFDocument)]) |
| return [self.delegate cgPDFDocument]; |
| |
| return nil; |
| } |
| |
| - (void)setDocument:(CGPDFDocumentRef)document |
| { |
| @synchronized(self) { |
| CGPDFDocumentRetain(document); |
| CGPDFDocumentRelease(_document); |
| _document = document; |
| } |
| } |
| |
| - (void)clearDocument |
| { |
| [self setDocument:nil]; |
| } |
| |
| - (CGPDFDocumentRef)doc |
| { |
| return [self document]; |
| } |
| |
| - (NSUInteger)totalPages |
| { |
| return CGPDFDocumentGetNumberOfPages([self document]); |
| } |
| |
| + (void)setAsPDFDocRepAndView |
| { |
| [WebView _setPDFRepresentationClass:[WebPDFViewPlaceholder class]]; |
| [WebView _setPDFViewClass:[WebPDFViewPlaceholder class]]; |
| } |
| |
| // This is a secret protocol for WebDataSource and WebFrameView to offer us the opportunity to do something different |
| // depending upon which frame is trying to instantiate a representation for PDF. |
| + (Class)_representationClassForWebFrame:(WebFrame *)webFrame |
| { |
| return [WebPDFView _representationClassForWebFrame:webFrame]; |
| } |
| |
| - (void)dealloc |
| { |
| if ([self.delegate respondsToSelector:@selector(viewWillClose)]) |
| [self.delegate viewWillClose]; |
| |
| [self setTitle:nil]; |
| |
| [self setPageRects:nil]; |
| [self setPageYOrigins:nil]; |
| |
| [self setDocument:nil]; |
| [super dealloc]; |
| } |
| |
| #pragma mark WebPDFDocumentView and WebPDFDocumentRepresentation protocols |
| |
| + (NSArray *)supportedMIMETypes |
| { |
| return [WebPDFView supportedMIMETypes]; |
| } |
| |
| #pragma mark WebDocumentView and WebDocumentRepresentation protocols |
| |
| - (void)setDataSource:(WebDataSource *)dataSource |
| { |
| [self dataSourceUpdated:dataSource]; |
| |
| if ([dataSource request]) |
| [self _updateTitleForURL:[[dataSource request] URL]]; |
| |
| WAKView *superview = [self superview]; |
| if (superview) |
| [self setBoundsSize:[self convertRect:[superview bounds] fromView:superview].size]; |
| } |
| |
| #pragma mark WebPDFViewPlaceholderDelegate stuff |
| |
| - (void)_notifyDidCompleteLayout |
| { |
| if ([NSThread isMainThread]) { |
| if ([self.delegate respondsToSelector:@selector(didCompleteLayout)]) |
| [self.delegate didCompleteLayout]; |
| } else |
| [self performSelectorOnMainThread:@selector(_notifyDidCompleteLayout) withObject:nil waitUntilDone:NO]; |
| } |
| |
| #pragma mark WebDocumentView protocol |
| |
| - (void)dataSourceUpdated:(WebDataSource *)dataSource |
| { |
| _dataSource = dataSource; |
| } |
| |
| - (void)layout |
| { |
| if (!_didFinishLoad) |
| return; |
| |
| if (self.pageRects) |
| return; |
| |
| CGSize boundingSize = [self _computePageRects:_document]; |
| |
| [self setBoundsSize:boundingSize]; |
| |
| _didCompleteLayout = YES; |
| [self _notifyDidCompleteLayout]; |
| } |
| |
| - (void)viewWillMoveToHostWindow:(NSWindow *)hostWindow |
| { |
| } |
| |
| - (void)viewDidMoveToHostWindow |
| { |
| } |
| |
| #pragma mark WebDocumentRepresentation protocol |
| |
| - (void)receivedData:(NSData *)data withDataSource:(WebDataSource *)dataSource |
| { |
| } |
| |
| - (void)receivedError:(NSError *)error withDataSource:(WebDataSource *)dataSource |
| { |
| } |
| |
| - (void)_doPostLoadOrUnlockTasks |
| { |
| if (!_document) |
| return; |
| |
| [self _updateTitleForDocumentIfAvailable]; |
| [self _evaluateJSForDocument:_document]; |
| |
| // Any remaining work on the document should be done before this call to -layout, |
| // which will hand ownership of the document to the delegate (UIWebPDFView) and |
| // release the placeholder's document. |
| [self layout]; |
| } |
| |
| - (void)finishedLoadingWithDataSource:(WebDataSource *)dataSource |
| { |
| [self dataSourceUpdated:dataSource]; |
| |
| _didFinishLoad = YES; |
| CGDataProviderRef provider = CGDataProviderCreateWithCFData((CFDataRef)[dataSource data]); |
| if (!provider) |
| return; |
| |
| _document = CGPDFDocumentCreateWithProvider(provider); |
| |
| CGDataProviderRelease(provider); |
| |
| [self _doPostLoadOrUnlockTasks]; |
| } |
| |
| - (BOOL)canProvideDocumentSource |
| { |
| return NO; |
| } |
| |
| - (NSString *)documentSource |
| { |
| return nil; |
| } |
| |
| #pragma mark internal stuff |
| |
| - (void)_evaluateJSForDocument:(CGPDFDocumentRef)pdfDocument |
| { |
| if (!pdfDocument || !CGPDFDocumentIsUnlocked(pdfDocument)) |
| return; |
| |
| NSArray *scripts = allScriptsInPDFDocument(pdfDocument); |
| |
| if ([scripts count]) { |
| JSGlobalContextRef ctx = JSGlobalContextCreate(0); |
| JSObjectRef jsPDFDoc = makeJSPDFDoc(ctx, _dataSource); |
| for (NSString *script in scripts) |
| JSEvaluateScript(ctx, OpaqueJSString::tryCreate(script).get(), jsPDFDoc, nullptr, 0, nullptr); |
| JSGlobalContextRelease(ctx); |
| } |
| } |
| |
| - (void)_updateTitleForURL:(NSURL *)URL |
| { |
| NSString *titleFromURL = [URL lastPathComponent]; |
| if (![titleFromURL length] || [titleFromURL isEqualToString:@"/"]) |
| titleFromURL = [[URL _web_hostString] _webkit_decodeHostName]; |
| |
| [self setTitle:titleFromURL]; |
| [[self _frame] _dispatchDidReceiveTitle:titleFromURL]; |
| } |
| |
| - (void)_updateTitleForDocumentIfAvailable |
| { |
| if (!_document || !CGPDFDocumentIsUnlocked(_document)) |
| return; |
| |
| NSString *title = nil; |
| |
| CGPDFDictionaryRef info = CGPDFDocumentGetInfo(_document); |
| CGPDFStringRef value; |
| if (CGPDFDictionaryGetString(info, "Title", &value)) |
| title = (NSString *)CGPDFStringCopyTextString(value); |
| |
| if ([title length]) { |
| [self setTitle:title]; |
| [[self _frame] _dispatchDidReceiveTitle:title]; |
| } |
| |
| [title release]; |
| } |
| |
| - (CGRect)_getPDFPageBounds:(CGPDFPageRef)page |
| { |
| if (!page) |
| return CGRectZero; |
| |
| CGRect bounds = CGPDFPageGetBoxRect(page, kCGPDFCropBox); |
| CGFloat rotation = CGPDFPageGetRotationAngle(page) * (M_PI / 180.0f); |
| if (rotation != 0) |
| bounds = CGRectApplyAffineTransform(bounds, CGAffineTransformMakeRotation(rotation)); |
| |
| bounds.origin.x = roundf(bounds.origin.x); |
| bounds.origin.y = roundf(bounds.origin.y); |
| bounds.size.width = roundf(bounds.size.width); |
| bounds.size.height = roundf(bounds.size.height); |
| |
| return bounds; |
| } |
| |
| - (CGSize)_computePageRects:(CGPDFDocumentRef)pdfDocument |
| { |
| // Do we even need to compute the page rects? |
| if (self.pageRects) |
| return [self bounds].size; |
| |
| if (!pdfDocument) |
| return CGSizeZero; |
| |
| if (!CGPDFDocumentIsUnlocked(pdfDocument)) |
| return CGSizeZero; |
| |
| size_t pageCount = CGPDFDocumentGetNumberOfPages(pdfDocument); |
| if (!pageCount) |
| return CGSizeZero; |
| |
| NSMutableArray *pageRects = [NSMutableArray array]; |
| if (!pageRects) |
| return CGSizeZero; |
| |
| NSMutableArray *pageYOrigins = [NSMutableArray array]; |
| if (!pageYOrigins) |
| return CGSizeZero; |
| |
| // Temporary vector to avoid getting the page rects twice. |
| WTF::Vector<CGRect> pageCropBoxes; |
| |
| // First we need to determine the width of the widest page. |
| CGFloat maxPageWidth = 0.0f; |
| |
| // CG uses page numbers instead of page indices, so 1 based. |
| for (size_t i = 1; i <= pageCount; i++) { |
| CGPDFPageRef page = CGPDFDocumentGetPage(pdfDocument, i); |
| if (!page) { |
| // So if there is a missing page, then effectively the document ends here. |
| // See <rdar://problem/10428152> iOS Mail crashes when opening a PDF |
| pageCount = i - 1; |
| break; |
| } |
| |
| CGRect pageRect = [self _getPDFPageBounds:page]; |
| |
| maxPageWidth = std::max<CGFloat>(maxPageWidth, pageRect.size.width); |
| |
| pageCropBoxes.append(pageRect); |
| } |
| |
| if (!pageCount) |
| return CGSizeZero; |
| |
| CGFloat scalingFactor = _containerSize.width / maxPageWidth; |
| if (_containerSize.width < FLT_EPSILON) |
| scalingFactor = 1.0; |
| |
| // Including the margins, what is the desired width of the content? |
| CGFloat desiredWidth = (maxPageWidth + (PAGE_WIDTH_INSET * 2.0f)) * scalingFactor; |
| CGFloat desiredHeight = PAGE_HEIGHT_INSET; |
| |
| for (size_t i = 0; i < pageCount; i++) { |
| |
| CGRect pageRect = pageCropBoxes[i]; |
| |
| // Apply our scaling to this page's bounds. |
| pageRect.size.width = roundf(pageRect.size.width * scalingFactor); |
| pageRect.size.height = roundf(pageRect.size.height * scalingFactor); |
| |
| // Center this page horizontally and update the starting vertical offset. |
| pageRect.origin.x = roundf((desiredWidth - pageRect.size.width) / 2.0f); |
| pageRect.origin.y = desiredHeight; |
| |
| // Save this page's rect and minimum y-offset. |
| [pageRects addObject:[NSValue _web_valueWithCGRect:pageRect]]; |
| [pageYOrigins addObject:[NSNumber numberWithFloat:CGRectGetMinY(pageRect)]]; |
| |
| // Update the next starting vertical offset. |
| desiredHeight += pageRect.size.height + PAGE_HEIGHT_INSET; |
| } |
| |
| // Save the resulting page rects array and minimum y-offsets array. |
| self.pageRects = pageRects; |
| self.pageYOrigins = pageYOrigins; |
| |
| // Determine the result desired bounds. |
| return CGSizeMake(roundf(desiredWidth), roundf(desiredHeight)); |
| } |
| |
| - (void)didUnlockDocument |
| { |
| [self _doPostLoadOrUnlockTasks]; |
| } |
| |
| - (CGRect)rectForPageNumber:(NSUInteger)pageNumber |
| { |
| // Page number is 1-based, not 0 based. |
| if ((!pageNumber) || (pageNumber > [_pageRects count])) |
| return CGRectNull; |
| |
| return [[_pageRects objectAtIndex:pageNumber - 1] _web_CGRectValue]; |
| } |
| |
| - (void)simulateClickOnLinkToURL:(NSURL *)URL |
| { |
| if (!URL) |
| return; |
| |
| auto event = MouseEvent::create(eventNames().clickEvent, Event::CanBubble::Yes, Event::IsCancelable::Yes, Event::IsComposed::Yes, |
| MonotonicTime::now(), nullptr, 1, { }, { }, { }, { }, 0, 0, nullptr, 0, 0, MouseEvent::IsSimulated::Yes); |
| |
| // Call to the frame loader because this is where our security checks are made. |
| Frame* frame = core([_dataSource webFrame]); |
| FrameLoadRequest frameLoadRequest { *frame->document(), frame->document()->securityOrigin(), { URL }, { }, InitiatedByMainFrame::Unknown }; |
| frameLoadRequest.setReferrerPolicy(ReferrerPolicy::NoReferrer); |
| frame->loader().loadFrameRequest(WTFMove(frameLoadRequest), event.ptr(), nullptr); |
| } |
| |
| @end |
| |
| #endif /* PLATFORM(IOS_FAMILY) */ |