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)

#import "WebPDFViewPlaceholder.h"

#import "WebFrameInternal.h"
#import "WebPDFViewIOS.h"
#import <JavaScriptCore/JSContextRef.h>
#import <JavaScriptCore/JSStringRef.h>
#import <JavaScriptCore/JSStringRefCF.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;

@interface WebPDFView (Secrets)
+ (Class)_representationClassForWebFrame:(WebFrame *)webFrame;
@end

#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);

    NSUInteger scriptCount = [scripts count];
    if (scriptCount) {

        JSGlobalContextRef ctx = JSGlobalContextCreate(0);
        JSObjectRef jsPDFDoc = makeJSPDFDoc(ctx, _dataSource);

        for (NSUInteger i = 0; i < scriptCount; ++i) {
            JSStringRef script = JSStringCreateWithCFString((CFStringRef)[scripts objectAtIndex:i]);
            JSEvaluateScript(ctx, script, jsPDFDoc, 0, 0, 0);
            JSStringRelease(script);
        }

        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;

    // Construct an event to simulate a click.
    RefPtr<Event> event = MouseEvent::create(eventNames().clickEvent, true, true, MonotonicTime::now(), 0, 1, 0, 0, 0, 0,
#if ENABLE(POINTER_LOCK)
        0, 0,
#endif
        false, false, false, false, 0, 0, nullptr, 0, 0, nullptr, true);

    // 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.get(), nullptr);
}

@end

#endif /* PLATFORM(IOS) */