WebPDFViewPlaceholder.mm   [plain text]

/*
* 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 <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)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] 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, nullptr, 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 }, { }, LockHistory::No, LockBackForwardList::No, MaybeSendReferrer, AllowNavigationToInvalidURL::Yes, NewFrameOpenerPolicy::Allow, ShouldOpenExternalURLsPolicy::ShouldNotAllow, InitiatedByMainFrame::Unknown };
frame->loader().loadFrameRequest(WTFMove(frameLoadRequest), event.ptr(), nullptr);
}

@end

#endif /* PLATFORM(IOS_FAMILY) */