2 * Copyright (C) 2010, 2011 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.
27 #import "PDFViewController.h"
29 #import "DataReference.h"
33 #import "WebEventFactory.h"
34 #import "WebPageGroup.h"
35 #import "WebPageProxy.h"
36 #import "WebPreferences.h"
37 #import <PDFKit/PDFKit.h>
38 #import <WebCore/LocalizedStrings.h>
39 #import <wtf/text/CString.h>
40 #import <wtf/text/WTFString.h>
42 // Redeclarations of PDFKit notifications. We can't use the API since we use a weak link to the framework.
43 #define _webkit_PDFViewDisplayModeChangedNotification @"PDFViewDisplayModeChanged"
44 #define _webkit_PDFViewScaleChangedNotification @"PDFViewScaleChanged"
45 #define _webkit_PDFViewPageChangedNotification @"PDFViewChangedPage"
47 using namespace WebKit;
52 @interface PDFDocument (PDFDocumentDetails)
53 - (NSPrintOperation *)getPrintOperationForPrintInfo:(NSPrintInfo *)printInfo autoRotate:(BOOL)doRotate;
56 extern "C" NSString *_NSPathForSystemFramework(NSString *framework);
58 // MARK: C UTILITY FUNCTIONS
60 static void _applicationInfoForMIMEType(NSString *type, NSString **name, NSImage **image)
67 OSStatus error = LSCopyApplicationForMIMEType((CFStringRef)type, kLSRolesAll, &appURL);
71 NSString *appPath = [(NSURL *)appURL path];
75 *image = [[NSWorkspace sharedWorkspace] iconForFile:appPath];
76 [*image setSize:NSMakeSize(16, 16)];
78 *name = [[NSFileManager defaultManager] displayNameAtPath:appPath];
81 // FIXME 4182876: We can eliminate this function in favor if -isEqual: if [PDFSelection isEqual:] is overridden
82 // to compare contents.
83 static BOOL _PDFSelectionsAreEqual(PDFSelection *selectionA, PDFSelection *selectionB)
85 NSArray *aPages = [selectionA pages];
86 NSArray *bPages = [selectionB pages];
88 if (![aPages isEqual:bPages])
91 NSUInteger count = [aPages count];
92 for (NSUInteger i = 0; i < count; ++i) {
93 NSRect aBounds = [selectionA boundsForPage:[aPages objectAtIndex:i]];
94 NSRect bBounds = [selectionB boundsForPage:[bPages objectAtIndex:i]];
95 if (!NSEqualRects(aBounds, bBounds))
102 @interface WKPDFView : NSView
104 PDFViewController* _pdfViewController;
106 RetainPtr<NSView> _pdfPreviewView;
108 BOOL _ignoreScaleAndDisplayModeAndPageNotifications;
109 BOOL _willUpdatePreferencesSoon;
112 - (id)initWithFrame:(NSRect)frame PDFViewController:(PDFViewController*)pdfViewController;
114 - (PDFView *)pdfView;
115 - (void)setDocument:(PDFDocument *)pdfDocument;
117 - (void)_applyPDFPreferences;
118 - (PDFSelection *)_nextMatchFor:(NSString *)string direction:(BOOL)forward caseSensitive:(BOOL)caseFlag wrap:(BOOL)wrapFlag fromSelection:(PDFSelection *)initialSelection startInSelection:(BOOL)startInSelection;
121 @implementation WKPDFView
123 - (id)initWithFrame:(NSRect)frame PDFViewController:(PDFViewController*)pdfViewController
125 if ((self = [super initWithFrame:frame])) {
126 _pdfViewController = pdfViewController;
128 [self setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
130 Class previewViewClass = PDFViewController::pdfPreviewViewClass();
131 ASSERT(previewViewClass);
133 _pdfPreviewView.adoptNS([[previewViewClass alloc] initWithFrame:frame]);
134 [_pdfPreviewView.get() setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
135 [self addSubview:_pdfPreviewView.get()];
137 _pdfView = [_pdfPreviewView.get() performSelector:@selector(pdfView)];
138 [_pdfView setDelegate:self];
146 _pdfViewController = 0;
154 - (void)setDocument:(PDFDocument *)pdfDocument
156 _ignoreScaleAndDisplayModeAndPageNotifications = YES;
157 [_pdfView setDocument:pdfDocument];
158 [self _applyPDFPreferences];
159 _ignoreScaleAndDisplayModeAndPageNotifications = NO;
162 - (void)_applyPDFPreferences
164 if (!_pdfViewController)
167 WebPreferences *preferences = _pdfViewController->page()->pageGroup()->preferences();
169 CGFloat scaleFactor = preferences->pdfScaleFactor();
171 [_pdfView setAutoScales:YES];
173 [_pdfView setAutoScales:NO];
174 [_pdfView setScaleFactor:scaleFactor];
176 [_pdfView setDisplayMode:preferences->pdfDisplayMode()];
179 - (void)_updatePreferences:(id)ignored
181 _willUpdatePreferencesSoon = NO;
183 if (!_pdfViewController)
186 WebPreferences* preferences = _pdfViewController->page()->pageGroup()->preferences();
188 CGFloat scaleFactor = [_pdfView autoScales] ? 0 : [_pdfView scaleFactor];
189 preferences->setPDFScaleFactor(scaleFactor);
190 preferences->setPDFDisplayMode([_pdfView displayMode]);
193 - (void)_updatePreferencesSoon
195 if (_willUpdatePreferencesSoon)
198 [self performSelector:@selector(_updatePreferences:) withObject:nil afterDelay:0];
199 _willUpdatePreferencesSoon = YES;
202 - (void)_scaleOrDisplayModeOrPageChanged:(NSNotification *)notification
204 ASSERT_ARG(notification, [notification object] == _pdfView);
205 if (!_ignoreScaleAndDisplayModeAndPageNotifications)
206 [self _updatePreferencesSoon];
209 - (void)_openWithFinder:(id)sender
211 _pdfViewController->openPDFInFinder();
214 - (PDFSelection *)_nextMatchFor:(NSString *)string direction:(BOOL)forward caseSensitive:(BOOL)caseFlag wrap:(BOOL)wrapFlag fromSelection:(PDFSelection *)initialSelection startInSelection:(BOOL)startInSelection
216 if (![string length])
221 options |= NSBackwardsSearch;
224 options |= NSCaseInsensitiveSearch;
226 PDFDocument *document = [_pdfView document];
228 PDFSelection *selectionForInitialSearch = [initialSelection copy];
229 if (startInSelection) {
230 // Initially we want to include the selected text in the search. So we must modify the starting search
231 // selection to fit PDFDocument's search requirements: selection must have a length >= 1, begin before
232 // the current selection (if searching forwards) or after (if searching backwards).
233 int initialSelectionLength = [[initialSelection string] length];
235 [selectionForInitialSearch extendSelectionAtStart:1];
236 [selectionForInitialSearch extendSelectionAtEnd:-initialSelectionLength];
238 [selectionForInitialSearch extendSelectionAtEnd:1];
239 [selectionForInitialSearch extendSelectionAtStart:-initialSelectionLength];
242 PDFSelection *foundSelection = [document findString:string fromSelection:selectionForInitialSearch withOptions:options];
243 [selectionForInitialSearch release];
245 // If we first searched in the selection, and we found the selection, search again from just past the selection
246 if (startInSelection && _PDFSelectionsAreEqual(foundSelection, initialSelection))
247 foundSelection = [document findString:string fromSelection:initialSelection withOptions:options];
249 if (!foundSelection && wrapFlag)
250 foundSelection = [document findString:string fromSelection:nil withOptions:options];
252 return foundSelection;
255 - (NSUInteger)_countMatches:(NSString *)string caseSensitive:(BOOL)caseFlag
257 if (![string length])
260 int options = caseFlag ? 0 : NSCaseInsensitiveSearch;
262 return [[[_pdfView document] findString:string withOptions:options] count];
265 // MARK: NSView overrides
267 - (void)viewDidMoveToWindow
272 NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
273 [notificationCenter addObserver:self selector:@selector(_scaleOrDisplayModeOrPageChanged:) name:_webkit_PDFViewScaleChangedNotification object:_pdfView];
274 [notificationCenter addObserver:self selector:@selector(_scaleOrDisplayModeOrPageChanged:) name:_webkit_PDFViewDisplayModeChangedNotification object:_pdfView];
275 [notificationCenter addObserver:self selector:@selector(_scaleOrDisplayModeOrPageChanged:) name:_webkit_PDFViewPageChangedNotification object:_pdfView];
278 - (void)viewWillMoveToWindow:(NSWindow *)newWindow
283 NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
284 [notificationCenter removeObserver:self name:_webkit_PDFViewScaleChangedNotification object:_pdfView];
285 [notificationCenter removeObserver:self name:_webkit_PDFViewDisplayModeChangedNotification object:_pdfView];
286 [notificationCenter removeObserver:self name:_webkit_PDFViewPageChangedNotification object:_pdfView];
289 - (NSView *)hitTest:(NSPoint)point
291 // Override hitTest so we can override menuForEvent.
292 NSEvent *event = [NSApp currentEvent];
293 NSEventType type = [event type];
294 if (type == NSRightMouseDown || (type == NSLeftMouseDown && ([event modifierFlags] & NSControlKeyMask)))
297 return [super hitTest:point];
300 - (NSMenu *)menuForEvent:(NSEvent *)theEvent
302 NSMenu *menu = [[NSMenu alloc] initWithTitle:@""];
304 NSEnumerator *menuItemEnumerator = [[[_pdfView menuForEvent:theEvent] itemArray] objectEnumerator];
305 while (NSMenuItem *item = [menuItemEnumerator nextObject]) {
306 NSMenuItem *itemCopy = [item copy];
307 [menu addItem:itemCopy];
310 if ([item action] != @selector(copy:))
313 // Add in an "Open with <default PDF viewer>" item
314 NSString *appName = nil;
315 NSImage *appIcon = nil;
317 _applicationInfoForMIMEType(@"application/pdf", &appName, &appIcon);
319 appName = WEB_UI_STRING("Finder", "Default application name for Open With context menu");
321 // To match the PDFKit style, we'll add Open with Preview even when there's no document yet to view, and
322 // disable it using validateUserInterfaceItem.
323 NSString *title = [NSString stringWithFormat:WEB_UI_STRING("Open with %@", "context menu item for PDF"), appName];
325 item = [[NSMenuItem alloc] initWithTitle:title action:@selector(_openWithFinder:) keyEquivalent:@""];
327 [item setImage:appIcon];
328 [menu addItem:[NSMenuItem separatorItem]];
333 return [menu autorelease];
336 // MARK: NSUserInterfaceValidations PROTOCOL IMPLEMENTATION
338 - (BOOL)validateUserInterfaceItem:(id <NSValidatedUserInterfaceItem>)item
340 SEL action = [item action];
341 if (action == @selector(_openWithFinder:))
342 return [_pdfView document] != nil;
346 // MARK: PDFView delegate methods
348 - (void)PDFViewWillClickOnLink:(PDFView *)sender withURL:(NSURL *)URL
350 _pdfViewController->linkClicked([URL absoluteString]);
353 - (void)PDFViewOpenPDFInNativeApplication:(PDFView *)sender
355 _pdfViewController->openPDFInFinder();
358 - (void)PDFViewSavePDFToDownloadFolder:(PDFView *)sender
360 _pdfViewController->savePDFToDownloadsFolder();
367 PassOwnPtr<PDFViewController> PDFViewController::create(WKView *wkView)
369 return adoptPtr(new PDFViewController(wkView));
372 PDFViewController::PDFViewController(WKView *wkView)
374 , m_wkPDFView(AdoptNS, [[WKPDFView alloc] initWithFrame:[m_wkView bounds] PDFViewController:this])
375 , m_pdfView([m_wkPDFView.get() pdfView])
376 , m_hasWrittenPDFToDisk(false)
378 [m_wkView addSubview:m_wkPDFView.get()];
381 PDFViewController::~PDFViewController()
383 [m_wkPDFView.get() removeFromSuperview];
384 [m_wkPDFView.get() invalidate];
385 m_wkPDFView = nullptr;
388 WebPageProxy* PDFViewController::page() const
390 return toImpl([m_wkView pageRef]);
393 NSView* PDFViewController::pdfView() const
395 return m_wkPDFView.get();
398 static RetainPtr<CFDataRef> convertPostScriptDataSourceToPDF(const CoreIPC::DataReference& dataReference)
400 // Convert PostScript to PDF using Quartz 2D API
401 // http://developer.apple.com/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_ps_convert/chapter_16_section_1.html
403 CGPSConverterCallbacks callbacks = { 0, 0, 0, 0, 0, 0, 0, 0 };
404 RetainPtr<CGPSConverterRef> converter(AdoptCF, CGPSConverterCreate(0, &callbacks, 0));
407 RetainPtr<NSData> nsData(AdoptNS, [[NSData alloc] initWithBytesNoCopy:const_cast<uint8_t*>(dataReference.data()) length:dataReference.size() freeWhenDone:NO]);
409 RetainPtr<CGDataProviderRef> provider(AdoptCF, CGDataProviderCreateWithCFData((CFDataRef)nsData.get()));
412 RetainPtr<CFMutableDataRef> result(AdoptCF, CFDataCreateMutable(kCFAllocatorDefault, 0));
415 RetainPtr<CGDataConsumerRef> consumer(AdoptCF, CGDataConsumerCreateWithCFData(result.get()));
418 CGPSConverterConvert(converter.get(), provider.get(), consumer.get(), 0);
426 void PDFViewController::setPDFDocumentData(const String& mimeType, const String& suggestedFilename, const CoreIPC::DataReference& dataReference)
428 if (equalIgnoringCase(mimeType, "application/postscript")) {
429 m_pdfData = convertPostScriptDataSourceToPDF(dataReference);
433 // Make sure to copy the data.
434 m_pdfData.adoptCF(CFDataCreate(0, dataReference.data(), dataReference.size()));
437 m_suggestedFilename = suggestedFilename;
439 RetainPtr<PDFDocument> pdfDocument(AdoptNS, [[pdfDocumentClass() alloc] initWithData:(NSData *)m_pdfData.get()]);
440 [m_wkPDFView.get() setDocument:pdfDocument.get()];
443 double PDFViewController::zoomFactor() const
445 return [m_pdfView scaleFactor];
448 void PDFViewController::setZoomFactor(double zoomFactor)
450 [m_pdfView setScaleFactor:zoomFactor];
453 Class PDFViewController::pdfDocumentClass()
455 static Class pdfDocumentClass = [pdfKitBundle() classNamed:@"PDFDocument"];
457 return pdfDocumentClass;
460 Class PDFViewController::pdfPreviewViewClass()
462 static Class pdfPreviewViewClass = [pdfKitBundle() classNamed:@"PDFPreviewView"];
464 return pdfPreviewViewClass;
467 NSBundle* PDFViewController::pdfKitBundle()
469 static NSBundle *pdfKitBundle;
473 NSString *pdfKitPath = [_NSPathForSystemFramework(@"Quartz.framework") stringByAppendingString:@"/Frameworks/PDFKit.framework"];
475 LOG_ERROR("Couldn't find PDFKit.framework");
479 pdfKitBundle = [NSBundle bundleWithPath:pdfKitPath];
480 if (![pdfKitBundle load])
481 LOG_ERROR("Couldn't load PDFKit.framework");
485 NSPrintOperation *PDFViewController::makePrintOperation(NSPrintInfo *printInfo)
487 return [[m_pdfView document] getPrintOperationForPrintInfo:printInfo autoRotate:YES];
490 void PDFViewController::openPDFInFinder()
492 // We don't want to open the PDF until we have a document to write. (see 4892525).
493 if (![m_pdfView document]) {
498 NSString *path = pathToPDFOnDisk();
502 if (!m_hasWrittenPDFToDisk) {
503 // Create a PDF file with the minimal permissions (only accessible to the current user, see 4145714).
504 RetainPtr<NSNumber> permissions(AdoptNS, [[NSNumber alloc] initWithInt:S_IRUSR]);
505 RetainPtr<NSDictionary> fileAttributes(AdoptNS, [[NSDictionary alloc] initWithObjectsAndKeys:permissions.get(), NSFilePosixPermissions, nil]);
507 if (![[NSFileManager defaultManager] createFileAtPath:path contents:(NSData *)m_pdfData.get() attributes:fileAttributes.get()])
510 m_hasWrittenPDFToDisk = true;
513 [[NSWorkspace sharedWorkspace] openFile:path];
516 static void releaseCFData(unsigned char*, const void* data)
518 ASSERT(CFGetTypeID(data) == CFDataGetTypeID());
520 // Balanced by CFRetain in savePDFToDownloadsFolder.
524 void PDFViewController::savePDFToDownloadsFolder()
526 // We don't want to write the file until we have a document to write. (see 5267607).
527 if (![m_pdfView document]) {
534 // Balanced by CFRelease in releaseCFData.
535 CFRetain(m_pdfData.get());
537 RefPtr<WebData> data = WebData::createWithoutCopying(CFDataGetBytePtr(m_pdfData.get()), CFDataGetLength(m_pdfData.get()), releaseCFData, m_pdfData.get());
539 page()->saveDataToFileInDownloadsFolder(m_suggestedFilename.get(), page()->mainFrame()->mimeType(), page()->mainFrame()->url(), data.get());
542 static NSString *temporaryPDFDirectoryPath()
544 static NSString *temporaryPDFDirectoryPath;
546 if (!temporaryPDFDirectoryPath) {
547 NSString *temporaryDirectoryTemplate = [NSTemporaryDirectory() stringByAppendingPathComponent:@"WebKitPDFs-XXXXXX"];
548 CString templateRepresentation = [temporaryDirectoryTemplate fileSystemRepresentation];
550 if (mkdtemp(templateRepresentation.mutableData()))
551 temporaryPDFDirectoryPath = [[[NSFileManager defaultManager] stringWithFileSystemRepresentation:templateRepresentation.data() length:templateRepresentation.length()] copy];
554 return temporaryPDFDirectoryPath;
557 NSString *PDFViewController::pathToPDFOnDisk()
559 if (m_pathToPDFOnDisk)
560 return m_pathToPDFOnDisk.get();
562 NSString *pdfDirectoryPath = temporaryPDFDirectoryPath();
563 if (!pdfDirectoryPath)
566 NSString *path = [pdfDirectoryPath stringByAppendingPathComponent:m_suggestedFilename.get()];
568 NSFileManager *fileManager = [NSFileManager defaultManager];
569 if ([fileManager fileExistsAtPath:path]) {
570 NSString *pathTemplatePrefix = [pdfDirectoryPath stringByAppendingString:@"XXXXXX-"];
571 NSString *pathTemplate = [pathTemplatePrefix stringByAppendingPathComponent:m_suggestedFilename.get()];
572 CString pathTemplateRepresentation = [pathTemplate fileSystemRepresentation];
574 int fd = mkstemps(pathTemplateRepresentation.mutableData(), pathTemplateRepresentation.length() - strlen([pathTemplatePrefix fileSystemRepresentation]) + 1);
579 path = [fileManager stringWithFileSystemRepresentation:pathTemplateRepresentation.data() length:pathTemplateRepresentation.length()];
582 m_pathToPDFOnDisk.adoptNS([path copy]);
586 void PDFViewController::linkClicked(const String& url)
588 NSEvent* nsEvent = [NSApp currentEvent];
590 switch ([nsEvent type]) {
594 event = WebEventFactory::createWebMouseEvent(nsEvent, m_pdfView);
596 // For non mouse-clicks or for keyboard events, pass an empty WebMouseEvent
597 // through. The event is only used by the WebFrameLoaderClient to determine
598 // the modifier keys and which mouse button is down. These queries will be
599 // valid with an empty event.
603 page()->linkClicked(url, event);
606 void PDFViewController::findString(const String& string, FindOptions options, unsigned maxMatchCount)
608 BOOL forward = !(options & FindOptionsBackwards);
609 BOOL caseFlag = !(options & FindOptionsCaseInsensitive);
610 BOOL wrapFlag = options & FindOptionsWrapAround;
612 PDFSelection *selection = [m_wkPDFView.get() _nextMatchFor:string direction:forward caseSensitive:caseFlag wrap:wrapFlag fromSelection:[m_pdfView currentSelection] startInSelection:NO];
614 page()->didFailToFindString(string);
618 NSUInteger matchCount;
619 if (!maxMatchCount) {
620 // If the max was zero, any result means we exceeded the max. We can skip computing the actual count.
621 matchCount = static_cast<unsigned>(kWKMoreThanMaximumMatchCount);
623 matchCount = [m_wkPDFView.get() _countMatches:string caseSensitive:caseFlag];
624 if (matchCount > maxMatchCount)
625 matchCount = static_cast<unsigned>(kWKMoreThanMaximumMatchCount);
628 [m_pdfView setCurrentSelection:selection];
629 [m_pdfView scrollSelectionToVisible:nil];
630 page()->didFindString(string, matchCount);
633 void PDFViewController::countStringMatches(const String& string, FindOptions options, unsigned maxMatchCount)
635 BOOL caseFlag = !(options & FindOptionsCaseInsensitive);
637 NSUInteger matchCount = [m_wkPDFView.get() _countMatches:string caseSensitive:caseFlag];
638 if (matchCount > maxMatchCount)
639 matchCount = maxMatchCount;
640 page()->didCountStringMatches(string, matchCount);
643 } // namespace WebKit