/* * Copyright (C) 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. */ #import "config.h" #import "PDFViewController.h" #import "DataReference.h" #import "WKAPICast.h" #import "WKView.h" #import "WebData.h" #import "WebEventFactory.h" #import "WebPageGroup.h" #import "WebPageProxy.h" #import "WebPreferences.h" #import <PDFKit/PDFKit.h> #import <WebCore/LocalizedStrings.h> #import <wtf/text/WTFString.h> // Redeclarations of PDFKit notifications. We can't use the API since we use a weak link to the framework. #define _webkit_PDFViewDisplayModeChangedNotification @"PDFViewDisplayModeChanged" #define _webkit_PDFViewScaleChangedNotification @"PDFViewScaleChanged" #define _webkit_PDFViewPageChangedNotification @"PDFViewChangedPage" using namespace WebKit; @class PDFDocument; @class PDFView; @interface PDFDocument (PDFDocumentDetails) - (NSPrintOperation *)getPrintOperationForPrintInfo:(NSPrintInfo *)printInfo autoRotate:(BOOL)doRotate; @end extern "C" NSString *_NSPathForSystemFramework(NSString *framework); // MARK: C UTILITY FUNCTIONS static void _applicationInfoForMIMEType(NSString *type, NSString **name, NSImage **image) { ASSERT(name); ASSERT(image); CFURLRef appURL = 0; OSStatus error = LSCopyApplicationForMIMEType((CFStringRef)type, kLSRolesAll, &appURL); if (error != noErr) return; NSString *appPath = [(NSURL *)appURL path]; if (appURL) CFRelease(appURL); *image = [[NSWorkspace sharedWorkspace] iconForFile:appPath]; [*image setSize:NSMakeSize(16, 16)]; *name = [[NSFileManager defaultManager] displayNameAtPath:appPath]; } // FIXME 4182876: We can eliminate this function in favor if -isEqual: if [PDFSelection isEqual:] is overridden // to compare contents. static BOOL _PDFSelectionsAreEqual(PDFSelection *selectionA, PDFSelection *selectionB) { NSArray *aPages = [selectionA pages]; NSArray *bPages = [selectionB pages]; if (![aPages isEqual:bPages]) return NO; NSUInteger count = [aPages count]; for (NSUInteger i = 0; i < count; ++i) { NSRect aBounds = [selectionA boundsForPage:[aPages objectAtIndex:i]]; NSRect bBounds = [selectionB boundsForPage:[bPages objectAtIndex:i]]; if (!NSEqualRects(aBounds, bBounds)) return NO; } return YES; } @interface WKPDFView : NSView { PDFViewController* _pdfViewController; RetainPtr<NSView> _pdfPreviewView; PDFView *_pdfView; BOOL _ignoreScaleAndDisplayModeAndPageNotifications; BOOL _willUpdatePreferencesSoon; } - (id)initWithFrame:(NSRect)frame PDFViewController:(PDFViewController*)pdfViewController; - (void)invalidate; - (PDFView *)pdfView; - (void)setDocument:(PDFDocument *)pdfDocument; - (void)_applyPDFPreferences; - (PDFSelection *)_nextMatchFor:(NSString *)string direction:(BOOL)forward caseSensitive:(BOOL)caseFlag wrap:(BOOL)wrapFlag fromSelection:(PDFSelection *)initialSelection startInSelection:(BOOL)startInSelection; @end @implementation WKPDFView - (id)initWithFrame:(NSRect)frame PDFViewController:(PDFViewController*)pdfViewController { if ((self = [super initWithFrame:frame])) { _pdfViewController = pdfViewController; [self setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; Class previewViewClass = PDFViewController::pdfPreviewViewClass(); ASSERT(previewViewClass); _pdfPreviewView.adoptNS([[previewViewClass alloc] initWithFrame:frame]); [_pdfPreviewView.get() setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; [self addSubview:_pdfPreviewView.get()]; _pdfView = [_pdfPreviewView.get() performSelector:@selector(pdfView)]; [_pdfView setDelegate:self]; } return self; } - (void)invalidate { _pdfViewController = 0; } - (PDFView *)pdfView { return _pdfView; } - (void)setDocument:(PDFDocument *)pdfDocument { _ignoreScaleAndDisplayModeAndPageNotifications = YES; [_pdfView setDocument:pdfDocument]; [self _applyPDFPreferences]; _ignoreScaleAndDisplayModeAndPageNotifications = NO; } - (void)_applyPDFPreferences { if (!_pdfViewController) return; WebPreferences *preferences = _pdfViewController->page()->pageGroup()->preferences(); CGFloat scaleFactor = preferences->pdfScaleFactor(); if (!scaleFactor) [_pdfView setAutoScales:YES]; else { [_pdfView setAutoScales:NO]; [_pdfView setScaleFactor:scaleFactor]; } [_pdfView setDisplayMode:preferences->pdfDisplayMode()]; } - (void)_updatePreferences:(id)ignored { _willUpdatePreferencesSoon = NO; if (!_pdfViewController) return; WebPreferences* preferences = _pdfViewController->page()->pageGroup()->preferences(); CGFloat scaleFactor = [_pdfView autoScales] ? 0 : [_pdfView scaleFactor]; preferences->setPDFScaleFactor(scaleFactor); preferences->setPDFDisplayMode([_pdfView displayMode]); } - (void)_updatePreferencesSoon { if (_willUpdatePreferencesSoon) return; [self performSelector:@selector(_updatePreferences:) withObject:nil afterDelay:0]; _willUpdatePreferencesSoon = YES; } - (void)_scaleOrDisplayModeOrPageChanged:(NSNotification *)notification { ASSERT_ARG(notification, [notification object] == _pdfView); if (!_ignoreScaleAndDisplayModeAndPageNotifications) [self _updatePreferencesSoon]; } - (void)_openWithFinder:(id)sender { _pdfViewController->openPDFInFinder(); } - (PDFSelection *)_nextMatchFor:(NSString *)string direction:(BOOL)forward caseSensitive:(BOOL)caseFlag wrap:(BOOL)wrapFlag fromSelection:(PDFSelection *)initialSelection startInSelection:(BOOL)startInSelection { if (![string length]) return nil; int options = 0; if (!forward) options |= NSBackwardsSearch; if (!caseFlag) options |= NSCaseInsensitiveSearch; PDFDocument *document = [_pdfView document]; PDFSelection *selectionForInitialSearch = [initialSelection copy]; if (startInSelection) { // Initially we want to include the selected text in the search. So we must modify the starting search // selection to fit PDFDocument's search requirements: selection must have a length >= 1, begin before // the current selection (if searching forwards) or after (if searching backwards). int initialSelectionLength = [[initialSelection string] length]; if (forward) { [selectionForInitialSearch extendSelectionAtStart:1]; [selectionForInitialSearch extendSelectionAtEnd:-initialSelectionLength]; } else { [selectionForInitialSearch extendSelectionAtEnd:1]; [selectionForInitialSearch extendSelectionAtStart:-initialSelectionLength]; } } PDFSelection *foundSelection = [document findString:string fromSelection:selectionForInitialSearch withOptions:options]; [selectionForInitialSearch release]; // If we first searched in the selection, and we found the selection, search again from just past the selection if (startInSelection && _PDFSelectionsAreEqual(foundSelection, initialSelection)) foundSelection = [document findString:string fromSelection:initialSelection withOptions:options]; if (!foundSelection && wrapFlag) foundSelection = [document findString:string fromSelection:nil withOptions:options]; return foundSelection; } - (NSUInteger)_countMatches:(NSString *)string caseSensitive:(BOOL)caseFlag { if (![string length]) return 0; int options = caseFlag ? 0 : NSCaseInsensitiveSearch; return [[[_pdfView document] findString:string withOptions:options] count]; } // MARK: NSView overrides - (void)viewDidMoveToWindow { if (![self window]) return; NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; [notificationCenter addObserver:self selector:@selector(_scaleOrDisplayModeOrPageChanged:) name:_webkit_PDFViewScaleChangedNotification object:_pdfView]; [notificationCenter addObserver:self selector:@selector(_scaleOrDisplayModeOrPageChanged:) name:_webkit_PDFViewDisplayModeChangedNotification object:_pdfView]; [notificationCenter addObserver:self selector:@selector(_scaleOrDisplayModeOrPageChanged:) name:_webkit_PDFViewPageChangedNotification object:_pdfView]; } - (void)viewWillMoveToWindow:(NSWindow *)newWindow { if (![self window]) return; NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; [notificationCenter removeObserver:self name:_webkit_PDFViewScaleChangedNotification object:_pdfView]; [notificationCenter removeObserver:self name:_webkit_PDFViewDisplayModeChangedNotification object:_pdfView]; [notificationCenter removeObserver:self name:_webkit_PDFViewPageChangedNotification object:_pdfView]; } - (NSView *)hitTest:(NSPoint)point { // Override hitTest so we can override menuForEvent. NSEvent *event = [NSApp currentEvent]; NSEventType type = [event type]; if (type == NSRightMouseDown || (type == NSLeftMouseDown && ([event modifierFlags] & NSControlKeyMask))) return self; return [super hitTest:point]; } - (NSMenu *)menuForEvent:(NSEvent *)theEvent { NSMenu *menu = [[NSMenu alloc] initWithTitle:@""]; NSEnumerator *menuItemEnumerator = [[[_pdfView menuForEvent:theEvent] itemArray] objectEnumerator]; while (NSMenuItem *item = [menuItemEnumerator nextObject]) { NSMenuItem *itemCopy = [item copy]; [menu addItem:itemCopy]; [itemCopy release]; if ([item action] != @selector(copy:)) continue; // Add in an "Open with <default PDF viewer>" item NSString *appName = nil; NSImage *appIcon = nil; _applicationInfoForMIMEType(@"application/pdf", &appName, &appIcon); if (!appName) appName = WEB_UI_STRING("Finder", "Default application name for Open With context menu"); // To match the PDFKit style, we'll add Open with Preview even when there's no document yet to view, and // disable it using validateUserInterfaceItem. NSString *title = [NSString stringWithFormat:WEB_UI_STRING("Open with %@", "context menu item for PDF"), appName]; item = [[NSMenuItem alloc] initWithTitle:title action:@selector(_openWithFinder:) keyEquivalent:@""]; if (appIcon) [item setImage:appIcon]; [menu addItem:[NSMenuItem separatorItem]]; [menu addItem:item]; [item release]; } return [menu autorelease]; } // MARK: NSUserInterfaceValidations PROTOCOL IMPLEMENTATION - (BOOL)validateUserInterfaceItem:(id <NSValidatedUserInterfaceItem>)item { SEL action = [item action]; if (action == @selector(_openWithFinder:)) return [_pdfView document] != nil; return YES; } // MARK: PDFView delegate methods - (void)PDFViewWillClickOnLink:(PDFView *)sender withURL:(NSURL *)URL { _pdfViewController->linkClicked([URL absoluteString]); } - (void)PDFViewOpenPDFInNativeApplication:(PDFView *)sender { _pdfViewController->openPDFInFinder(); } - (void)PDFViewSavePDFToDownloadFolder:(PDFView *)sender { _pdfViewController->savePDFToDownloadsFolder(); } @end namespace WebKit { PassOwnPtr<PDFViewController> PDFViewController::create(WKView *wkView) { return adoptPtr(new PDFViewController(wkView)); } PDFViewController::PDFViewController(WKView *wkView) : m_wkView(wkView) , m_wkPDFView(AdoptNS, [[WKPDFView alloc] initWithFrame:[m_wkView bounds] PDFViewController:this]) , m_pdfView([m_wkPDFView.get() pdfView]) , m_hasWrittenPDFToDisk(false) { [m_wkView addSubview:m_wkPDFView.get()]; } PDFViewController::~PDFViewController() { [m_wkPDFView.get() removeFromSuperview]; [m_wkPDFView.get() invalidate]; m_wkPDFView = nullptr; } WebPageProxy* PDFViewController::page() const { return toImpl([m_wkView pageRef]); } NSView* PDFViewController::pdfView() const { return m_wkPDFView.get(); } static RetainPtr<CFDataRef> convertPostScriptDataSourceToPDF(const CoreIPC::DataReference& dataReference) { // Convert PostScript to PDF using Quartz 2D API // http://developer.apple.com/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_ps_convert/chapter_16_section_1.html CGPSConverterCallbacks callbacks = { 0, 0, 0, 0, 0, 0, 0, 0 }; RetainPtr<CGPSConverterRef> converter(AdoptCF, CGPSConverterCreate(0, &callbacks, 0)); ASSERT(converter); RetainPtr<NSData> nsData(AdoptNS, [[NSData alloc] initWithBytesNoCopy:const_cast<uint8_t*>(dataReference.data()) length:dataReference.size() freeWhenDone:NO]); RetainPtr<CGDataProviderRef> provider(AdoptCF, CGDataProviderCreateWithCFData((CFDataRef)nsData.get())); ASSERT(provider); RetainPtr<CFMutableDataRef> result(AdoptCF, CFDataCreateMutable(kCFAllocatorDefault, 0)); ASSERT(result); RetainPtr<CGDataConsumerRef> consumer(AdoptCF, CGDataConsumerCreateWithCFData(result.get())); ASSERT(consumer); CGPSConverterConvert(converter.get(), provider.get(), consumer.get(), 0); if (!result) return 0; return result; } void PDFViewController::setPDFDocumentData(const String& mimeType, const String& suggestedFilename, const CoreIPC::DataReference& dataReference) { if (equalIgnoringCase(mimeType, "application/postscript")) { m_pdfData = convertPostScriptDataSourceToPDF(dataReference); if (!m_pdfData) return; } else { // Make sure to copy the data. m_pdfData.adoptCF(CFDataCreate(0, dataReference.data(), dataReference.size())); } m_suggestedFilename = suggestedFilename; RetainPtr<PDFDocument> pdfDocument(AdoptNS, [[pdfDocumentClass() alloc] initWithData:(NSData *)m_pdfData.get()]); [m_wkPDFView.get() setDocument:pdfDocument.get()]; } double PDFViewController::zoomFactor() const { return [m_pdfView scaleFactor]; } void PDFViewController::setZoomFactor(double zoomFactor) { [m_pdfView setScaleFactor:zoomFactor]; } Class PDFViewController::pdfDocumentClass() { static Class pdfDocumentClass = [pdfKitBundle() classNamed:@"PDFDocument"]; return pdfDocumentClass; } Class PDFViewController::pdfPreviewViewClass() { static Class pdfPreviewViewClass = [pdfKitBundle() classNamed:@"PDFPreviewView"]; return pdfPreviewViewClass; } NSBundle* PDFViewController::pdfKitBundle() { static NSBundle *pdfKitBundle; if (pdfKitBundle) return pdfKitBundle; NSString *pdfKitPath = [_NSPathForSystemFramework(@"Quartz.framework") stringByAppendingString:@"/Frameworks/PDFKit.framework"]; if (!pdfKitPath) { LOG_ERROR("Couldn't find PDFKit.framework"); return nil; } pdfKitBundle = [NSBundle bundleWithPath:pdfKitPath]; if (![pdfKitBundle load]) LOG_ERROR("Couldn't load PDFKit.framework"); return pdfKitBundle; } NSPrintOperation *PDFViewController::makePrintOperation(NSPrintInfo *printInfo) { return [[m_pdfView document] getPrintOperationForPrintInfo:printInfo autoRotate:YES]; } void PDFViewController::openPDFInFinder() { // We don't want to open the PDF until we have a document to write. (see 4892525). if (![m_pdfView document]) { NSBeep(); return; } NSString *path = pathToPDFOnDisk(); if (!path) return; if (!m_hasWrittenPDFToDisk) { // Create a PDF file with the minimal permissions (only accessible to the current user, see 4145714). RetainPtr<NSNumber> permissions(AdoptNS, [[NSNumber alloc] initWithInt:S_IRUSR]); RetainPtr<NSDictionary> fileAttributes(AdoptNS, [[NSDictionary alloc] initWithObjectsAndKeys:permissions.get(), NSFilePosixPermissions, nil]); if (![[NSFileManager defaultManager] createFileAtPath:path contents:(NSData *)m_pdfData.get() attributes:fileAttributes.get()]) return; m_hasWrittenPDFToDisk = true; } [[NSWorkspace sharedWorkspace] openFile:path]; } static void releaseCFData(unsigned char*, const void* data) { ASSERT(CFGetTypeID(data) == CFDataGetTypeID()); // Balanced by CFRetain in savePDFToDownloadsFolder. CFRelease(data); } void PDFViewController::savePDFToDownloadsFolder() { // We don't want to write the file until we have a document to write. (see 5267607). if (![m_pdfView document]) { NSBeep(); return; } ASSERT(m_pdfData); // Balanced by CFRelease in releaseCFData. CFRetain(m_pdfData.get()); RefPtr<WebData> data = WebData::createWithoutCopying(CFDataGetBytePtr(m_pdfData.get()), CFDataGetLength(m_pdfData.get()), releaseCFData, m_pdfData.get()); page()->saveDataToFileInDownloadsFolder(m_suggestedFilename.get(), page()->mainFrame()->mimeType(), page()->mainFrame()->url(), data.get()); } static NSString *temporaryPDFDirectoryPath() { static NSString *temporaryPDFDirectoryPath; if (!temporaryPDFDirectoryPath) { NSString *temporaryDirectoryTemplate = [NSTemporaryDirectory() stringByAppendingPathComponent:@"WebKitPDFs-XXXXXX"]; CString templateRepresentation = [temporaryDirectoryTemplate fileSystemRepresentation]; if (mkdtemp(templateRepresentation.mutableData())) temporaryPDFDirectoryPath = [[[NSFileManager defaultManager] stringWithFileSystemRepresentation:templateRepresentation.data() length:templateRepresentation.length()] copy]; } return temporaryPDFDirectoryPath; } NSString *PDFViewController::pathToPDFOnDisk() { if (m_pathToPDFOnDisk) return m_pathToPDFOnDisk.get(); NSString *pdfDirectoryPath = temporaryPDFDirectoryPath(); if (!pdfDirectoryPath) return nil; NSString *path = [pdfDirectoryPath stringByAppendingPathComponent:m_suggestedFilename.get()]; NSFileManager *fileManager = [NSFileManager defaultManager]; if ([fileManager fileExistsAtPath:path]) { NSString *pathTemplatePrefix = [pdfDirectoryPath stringByAppendingString:@"XXXXXX-"]; NSString *pathTemplate = [pathTemplatePrefix stringByAppendingPathComponent:m_suggestedFilename.get()]; CString pathTemplateRepresentation = [pathTemplate fileSystemRepresentation]; int fd = mkstemps(pathTemplateRepresentation.mutableData(), pathTemplateRepresentation.length() - strlen([pathTemplatePrefix fileSystemRepresentation]) + 1); if (fd < 0) return nil; close(fd); path = [fileManager stringWithFileSystemRepresentation:pathTemplateRepresentation.data() length:pathTemplateRepresentation.length()]; } m_pathToPDFOnDisk.adoptNS([path copy]); return path; } void PDFViewController::linkClicked(const String& url) { NSEvent* nsEvent = [NSApp currentEvent]; WebMouseEvent event; switch ([nsEvent type]) { case NSLeftMouseUp: case NSRightMouseUp: case NSOtherMouseUp: event = WebEventFactory::createWebMouseEvent(nsEvent, m_pdfView); default: // For non mouse-clicks or for keyboard events, pass an empty WebMouseEvent // through. The event is only used by the WebFrameLoaderClient to determine // the modifier keys and which mouse button is down. These queries will be // valid with an empty event. break; } page()->linkClicked(url, event); } void PDFViewController::findString(const String& string, FindOptions options, unsigned maxMatchCount) { BOOL forward = !(options & FindOptionsBackwards); BOOL caseFlag = !(options & FindOptionsCaseInsensitive); BOOL wrapFlag = options & FindOptionsWrapAround; PDFSelection *selection = [m_wkPDFView.get() _nextMatchFor:string direction:forward caseSensitive:caseFlag wrap:wrapFlag fromSelection:[m_pdfView currentSelection] startInSelection:NO]; NSUInteger matchCount = [m_wkPDFView.get() _countMatches:string caseSensitive:caseFlag]; if (matchCount > maxMatchCount) matchCount = maxMatchCount; if (!selection) { page()->didFailToFindString(string); return; } [m_pdfView setCurrentSelection:selection]; [m_pdfView scrollSelectionToVisible:nil]; page()->didFindString(string, matchCount); } void PDFViewController::countStringMatches(const String& string, FindOptions options, unsigned maxMatchCount) { BOOL caseFlag = !(options & FindOptionsCaseInsensitive); NSUInteger matchCount = [m_wkPDFView.get() _countMatches:string caseSensitive:caseFlag]; if (matchCount > maxMatchCount) matchCount = maxMatchCount; page()->didCountStringMatches(string, matchCount); } } // namespace WebKit