/* * Copyright (C) 2009-2020 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 "PDFPlugin.h" #if ENABLE(PDFKIT_PLUGIN) #import "ArgumentCoders.h" #import "DataReference.h" #import "FrameInfoData.h" #import "Logging.h" #import "PDFAnnotationTextWidgetDetails.h" #import "PDFContextMenu.h" #import "PDFLayerControllerSPI.h" #import "PDFPluginAnnotation.h" #import "PDFPluginPasswordField.h" #import "PluginView.h" #import "WKAccessibilityWebPageObjectMac.h" #import "WKPageFindMatchesClient.h" #import "WebCoreArgumentCoders.h" #import "WebEventConversion.h" #import "WebFindOptions.h" #import "WebKeyboardEvent.h" #import "WebLoaderStrategy.h" #import "WebMouseEvent.h" #import "WebPage.h" #import "WebPageProxyMessages.h" #import "WebPasteboardProxyMessages.h" #import "WebProcess.h" #import <JavaScriptCore/JSContextRef.h> #import <JavaScriptCore/JSObjectRef.h> #import <JavaScriptCore/OpaqueJSString.h> #import <Quartz/Quartz.h> #import <QuartzCore/QuartzCore.h> #import <WebCore/AXObjectCache.h> #import <WebCore/ArchiveResource.h> #import <WebCore/Chrome.h> #import <WebCore/Cursor.h> #import <WebCore/DictionaryLookup.h> #import <WebCore/DocumentLoader.h> #import <WebCore/EventNames.h> #import <WebCore/FocusController.h> #import <WebCore/FormState.h> #import <WebCore/Frame.h> #import <WebCore/FrameLoader.h> #import <WebCore/FrameView.h> #import <WebCore/GraphicsContext.h> #import <WebCore/HTMLElement.h> #import <WebCore/HTMLFormElement.h> #import <WebCore/HTMLPlugInElement.h> #import <WebCore/LegacyNSPasteboardTypes.h> #import <WebCore/LoaderNSURLExtras.h> #import <WebCore/LocalDefaultSystemAppearance.h> #import <WebCore/LocalizedStrings.h> #import <WebCore/MouseEvent.h> #import <WebCore/PDFDocumentImage.h> #import <WebCore/Page.h> #import <WebCore/Pasteboard.h> #import <WebCore/PlatformScreen.h> #import <WebCore/PluginData.h> #import <WebCore/PluginDocument.h> #import <WebCore/RenderBoxModelObject.h> #import <WebCore/RuntimeEnabledFeatures.h> #import <WebCore/ScrollAnimator.h> #import <WebCore/ScrollbarTheme.h> #import <WebCore/Settings.h> #import <WebCore/WebAccessibilityObjectWrapperMac.h> #import <WebCore/WheelEventTestMonitor.h> #import <pal/spi/cg/CoreGraphicsSPI.h> #import <pal/spi/mac/NSMenuSPI.h> #import <wtf/HexNumber.h> #import <wtf/Scope.h> #import <wtf/UUID.h> #import <wtf/WTFSemaphore.h> #import <wtf/WorkQueue.h> #import <wtf/cocoa/VectorCocoa.h> #import <wtf/text/TextStream.h> #if HAVE(INCREMENTAL_PDF_APIS) @interface PDFDocument () -(instancetype)initWithProvider:(CGDataProviderRef)dataProvider; -(void)preloadDataOfPagesInRange:(NSRange)range onQueue:(dispatch_queue_t)queue completion:(void (^)(NSIndexSet* loadedPageIndexes))completionBlock; @property (readwrite, nonatomic) BOOL hasHighLatencyDataProvider; @end #endif // Set overflow: hidden on the annotation container so <input> elements scrolled out of view don't show // scrollbars on the body. We can't add annotations directly to the body, because overflow: hidden on the body // will break rubber-banding. static const char* annotationStyle = "#annotationContainer {" " overflow: hidden; " " position: absolute; " " pointer-events: none; " " top: 0; " " left: 0; " " right: 0; " " bottom: 0; " " display: -webkit-box; " " -webkit-box-align: center; " " -webkit-box-pack: center; " "} " ".annotation { " " position: absolute; " " pointer-events: auto; " "} " "textarea.annotation { " " resize: none; " "} " "input.annotation[type='password'] { " " position: static; " " width: 200px; " " margin-top: 100px; " "} "; // In non-continuous modes, a single scroll event with a magnitude of >= 20px // will jump to the next or previous page, to match PDFKit behavior. static const int defaultScrollMagnitudeThresholdForPageFlip = 20; // <rdar://problem/61473378> - PDFKit asks for a "way too large" range when the PDF it is loading // incrementally turns out to be non-linearized. // We'll assume any size over 4gb is PDFKit noticing non-linearized data. static const uint32_t nonLinearizedPDFSentinel = std::numeric_limits<uint32_t>::max(); @interface WKPDFPluginAccessibilityObject : NSObject { PDFLayerController *_pdfLayerController; __unsafe_unretained NSObject *_parent; WebKit::PDFPlugin* _pdfPlugin; } @property (assign) PDFLayerController *pdfLayerController; @property (assign) NSObject *parent; @property (assign) WebKit::PDFPlugin* pdfPlugin; - (id)initWithPDFPlugin:(WebKit::PDFPlugin *)plugin; @end @implementation WKPDFPluginAccessibilityObject @synthesize parent = _parent; @synthesize pdfPlugin = _pdfPlugin; - (id)initWithPDFPlugin:(WebKit::PDFPlugin *)plugin { if (!(self = [super init])) return nil; _pdfPlugin = plugin; return self; } - (PDFLayerController *)pdfLayerController { return _pdfLayerController; } - (void)setPdfLayerController:(PDFLayerController *)pdfLayerController { _pdfLayerController = pdfLayerController; [_pdfLayerController setAccessibilityParent:self]; } ALLOW_DEPRECATED_IMPLEMENTATIONS_BEGIN - (BOOL)accessibilityIsIgnored ALLOW_DEPRECATED_IMPLEMENTATIONS_END { return NO; } // This is called by PDFAccessibilityNodes from inside PDFKit to get final bounds. - (NSRect)convertRectToScreenSpace:(NSRect)rect { return _pdfPlugin->convertFromPDFViewToScreen(rect); } ALLOW_DEPRECATED_IMPLEMENTATIONS_BEGIN - (id)accessibilityAttributeValue:(NSString *)attribute ALLOW_DEPRECATED_IMPLEMENTATIONS_END { if ([attribute isEqualToString:NSAccessibilityParentAttribute]) return _parent; if ([attribute isEqualToString:NSAccessibilityTopLevelUIElementAttribute]) return [_parent accessibilityAttributeValue:NSAccessibilityTopLevelUIElementAttribute]; if ([attribute isEqualToString:NSAccessibilityWindowAttribute]) return [_parent accessibilityAttributeValue:NSAccessibilityWindowAttribute]; if ([attribute isEqualToString:NSAccessibilitySizeAttribute]) return [NSValue valueWithSize:_pdfPlugin->boundsOnScreen().size()]; if ([attribute isEqualToString:NSAccessibilityEnabledAttribute]) return [_parent accessibilityAttributeValue:NSAccessibilityEnabledAttribute]; if ([attribute isEqualToString:NSAccessibilityPositionAttribute]) return [NSValue valueWithPoint:_pdfPlugin->boundsOnScreen().location()]; if ([attribute isEqualToString:NSAccessibilityChildrenAttribute]) return @[ _pdfLayerController ]; if ([attribute isEqualToString:NSAccessibilityRoleAttribute]) return NSAccessibilityGroupRole; return 0; } ALLOW_DEPRECATED_IMPLEMENTATIONS_BEGIN - (id)accessibilityAttributeValue:(NSString *)attribute forParameter:(id)parameter ALLOW_DEPRECATED_IMPLEMENTATIONS_END { if ([attribute isEqualToString:NSAccessibilityBoundsForRangeParameterizedAttribute]) { NSRect boundsInPDFViewCoordinates = [[_pdfLayerController accessibilityBoundsForRangeAttributeForParameter:parameter] rectValue]; NSRect boundsInScreenCoordinates = _pdfPlugin->convertFromPDFViewToScreen(boundsInPDFViewCoordinates); return [NSValue valueWithRect:boundsInScreenCoordinates]; } if ([attribute isEqualToString:NSAccessibilityLineForIndexParameterizedAttribute]) return [_pdfLayerController accessibilityLineForIndexAttributeForParameter:parameter]; if ([attribute isEqualToString:NSAccessibilityRangeForLineParameterizedAttribute]) return [_pdfLayerController accessibilityRangeForLineAttributeForParameter:parameter]; if ([attribute isEqualToString:NSAccessibilityStringForRangeParameterizedAttribute]) return [_pdfLayerController accessibilityStringForRangeAttributeForParameter:parameter]; return 0; } - (CPReadingModel *)readingModel { return [_pdfLayerController readingModel]; } ALLOW_DEPRECATED_IMPLEMENTATIONS_BEGIN - (NSArray *)accessibilityAttributeNames ALLOW_DEPRECATED_IMPLEMENTATIONS_END { static NSArray *attributeNames = 0; if (!attributeNames) { attributeNames = @[ NSAccessibilityParentAttribute, NSAccessibilityWindowAttribute, NSAccessibilityTopLevelUIElementAttribute, NSAccessibilityRoleDescriptionAttribute, NSAccessibilitySizeAttribute, NSAccessibilityEnabledAttribute, NSAccessibilityPositionAttribute, NSAccessibilityFocusedAttribute, // PDFLayerController has its own accessibilityChildren. NSAccessibilityChildrenAttribute ]; [attributeNames retain]; } return attributeNames; } ALLOW_DEPRECATED_IMPLEMENTATIONS_BEGIN - (NSArray *)accessibilityActionNames ALLOW_DEPRECATED_IMPLEMENTATIONS_END { return nil; } ALLOW_DEPRECATED_IMPLEMENTATIONS_BEGIN - (void)accessibilityPerformAction:(NSString *)action ALLOW_DEPRECATED_IMPLEMENTATIONS_END { if ([action isEqualToString:NSAccessibilityShowMenuAction]) _pdfPlugin->showContextMenuAtPoint(WebCore::IntRect(WebCore::IntPoint(), _pdfPlugin->size()).center()); } ALLOW_DEPRECATED_IMPLEMENTATIONS_BEGIN - (BOOL)accessibilityIsAttributeSettable:(NSString *)attribute ALLOW_DEPRECATED_IMPLEMENTATIONS_END { return [_pdfLayerController accessibilityIsAttributeSettable:attribute]; } ALLOW_DEPRECATED_IMPLEMENTATIONS_BEGIN - (void)accessibilitySetValue:(id)value forAttribute:(NSString *)attribute ALLOW_DEPRECATED_IMPLEMENTATIONS_END { return [_pdfLayerController accessibilitySetValue:value forAttribute:attribute]; } ALLOW_DEPRECATED_IMPLEMENTATIONS_BEGIN - (NSArray *)accessibilityParameterizedAttributeNames ALLOW_DEPRECATED_IMPLEMENTATIONS_END { return nil; } - (id)accessibilityFocusedUIElement { if (WebKit::PDFPluginAnnotation* activeAnnotation = _pdfPlugin->activeAnnotation()) { if (WebCore::AXObjectCache* existingCache = _pdfPlugin->axObjectCache()) { if (WebCore::AccessibilityObject* object = existingCache->getOrCreate(activeAnnotation->element())) ALLOW_DEPRECATED_DECLARATIONS_BEGIN return [object->wrapper() accessibilityAttributeValue:@"_AXAssociatedPluginParent"]; ALLOW_DEPRECATED_DECLARATIONS_END } } return nil; } - (id)accessibilityAssociatedControlForAnnotation:(PDFAnnotation *)annotation { // Only active annotations seem to have their associated controls available. WebKit::PDFPluginAnnotation* activeAnnotation = _pdfPlugin->activeAnnotation(); if (!activeAnnotation || ![activeAnnotation->annotation() isEqual:annotation]) return nil; WebCore::AXObjectCache* cache = _pdfPlugin->axObjectCache(); if (!cache) return nil; WebCore::AccessibilityObject* object = cache->getOrCreate(activeAnnotation->element()); if (!object) return nil; return object->wrapper(); } - (id)accessibilityHitTest:(NSPoint)point { point = _pdfPlugin->convertFromRootViewToPDFView(WebCore::IntPoint(point)); return [_pdfLayerController accessibilityHitTest:point]; } @end @interface WKPDFPluginScrollbarLayer : CALayer { WebKit::PDFPlugin* _pdfPlugin; } @property (assign) WebKit::PDFPlugin* pdfPlugin; @end @implementation WKPDFPluginScrollbarLayer @synthesize pdfPlugin = _pdfPlugin; - (id)initWithPDFPlugin:(WebKit::PDFPlugin *)plugin { if (!(self = [super init])) return nil; _pdfPlugin = plugin; return self; } - (id<CAAction>)actionForKey:(NSString *)key { return nil; } - (void)drawInContext:(CGContextRef)ctx { _pdfPlugin->paintControlForLayerInContext(self, ctx); } @end @interface WKPDFLayerControllerDelegate : NSObject<PDFLayerControllerDelegate> { WebKit::PDFPlugin* _pdfPlugin; } @property (assign) WebKit::PDFPlugin* pdfPlugin; @end @implementation WKPDFLayerControllerDelegate @synthesize pdfPlugin = _pdfPlugin; - (id)initWithPDFPlugin:(WebKit::PDFPlugin *)plugin { if (!(self = [super init])) return nil; _pdfPlugin = plugin; return self; } - (void)updateScrollPosition:(CGPoint)newPosition { _pdfPlugin->notifyScrollPositionChanged(WebCore::IntPoint(newPosition)); } - (void)writeItemsToPasteboard:(NSArray *)items withTypes:(NSArray *)types { ALLOW_DEPRECATED_DECLARATIONS_BEGIN _pdfPlugin->writeItemsToPasteboard(NSGeneralPboard, items, types); ALLOW_DEPRECATED_DECLARATIONS_END } - (void)showDefinitionForAttributedString:(NSAttributedString *)string atPoint:(CGPoint)point { _pdfPlugin->showDefinitionForAttributedString(string, point); } - (void)performWebSearch:(NSString *)string { _pdfPlugin->performWebSearch(string); } - (void)performSpotlightSearch:(NSString *)string { _pdfPlugin->performSpotlightSearch(string); } - (void)openWithNativeApplication { #if !ENABLE(UI_PROCESS_PDF_HUD) _pdfPlugin->openWithNativeApplication(); #endif } - (void)saveToPDF { #if !ENABLE(UI_PROCESS_PDF_HUD) _pdfPlugin->saveToPDF(); #endif } - (void)pdfLayerController:(PDFLayerController *)pdfLayerController clickedLinkWithURL:(NSURL *)url { _pdfPlugin->clickedLink(url); } - (void)pdfLayerController:(PDFLayerController *)pdfLayerController didChangeActiveAnnotation:(PDFAnnotation *)annotation { _pdfPlugin->setActiveAnnotation(annotation); } - (void)pdfLayerController:(PDFLayerController *)pdfLayerController didChangeContentScaleFactor:(CGFloat)scaleFactor { _pdfPlugin->notifyContentScaleFactorChanged(scaleFactor); } - (void)pdfLayerController:(PDFLayerController *)pdfLayerController didChangeDisplayMode:(int)mode { _pdfPlugin->notifyDisplayModeChanged(mode); } - (void)pdfLayerController:(PDFLayerController *)pdfLayerController didChangeSelection:(PDFSelection *)selection { _pdfPlugin->notifySelectionChanged(selection); } - (void)setMouseCursor:(PDFLayerControllerCursorType)cursorType { _pdfPlugin->notifyCursorChanged(cursorType); } - (void)didChangeAnnotationState { _pdfPlugin->didMutatePDFDocument(); } @end @interface PDFViewLayout - (NSPoint)convertPoint:(NSPoint)point toPage:(PDFPage *)page forScaleFactor:(CGFloat)scaleFactor; - (NSRect)convertRect:(NSRect)rect fromPage:(PDFPage *) page forScaleFactor:(CGFloat) scaleFactor; - (PDFPage *)pageNearestPoint:(NSPoint)point currentPage:(PDFPage *)currentPage; @end namespace WebKit { using namespace WebCore; using namespace HTMLNames; static const char* postScriptMIMEType = "application/postscript"; const uint64_t pdfDocumentRequestID = 1; // PluginController supports loading multiple streams, but we only need one for PDF. static void appendValuesInPDFNameSubtreeToVector(CGPDFDictionaryRef subtree, Vector<CGPDFObjectRef>& values) { CGPDFArrayRef names; if (CGPDFDictionaryGetArray(subtree, "Names", &names)) { size_t nameCount = CGPDFArrayGetCount(names) / 2; for (size_t i = 0; i < nameCount; ++i) { CGPDFObjectRef object; CGPDFArrayGetObject(names, 2 * i + 1, &object); values.append(object); } return; } CGPDFArrayRef kids; if (!CGPDFDictionaryGetArray(subtree, "Kids", &kids)) return; size_t kidCount = CGPDFArrayGetCount(kids); for (size_t i = 0; i < kidCount; ++i) { CGPDFDictionaryRef kid; if (!CGPDFArrayGetDictionary(kids, i, &kid)) continue; appendValuesInPDFNameSubtreeToVector(kid, values); } } static void getAllValuesInPDFNameTree(CGPDFDictionaryRef tree, Vector<CGPDFObjectRef>& allValues) { appendValuesInPDFNameSubtreeToVector(tree, allValues); } static void getAllScriptsInPDFDocument(CGPDFDocumentRef pdfDocument, Vector<RetainPtr<CFStringRef>>& scripts) { if (!pdfDocument) return; CGPDFDictionaryRef pdfCatalog = CGPDFDocumentGetCatalog(pdfDocument); if (!pdfCatalog) return; // Get the dictionary of all document-level name trees. CGPDFDictionaryRef namesDictionary; if (!CGPDFDictionaryGetDictionary(pdfCatalog, "Names", &namesDictionary)) return; // Get the document-level "JavaScript" name tree. CGPDFDictionaryRef javaScriptNameTree; if (!CGPDFDictionaryGetDictionary(namesDictionary, "JavaScript", &javaScriptNameTree)) return; // The names are arbitrary. We are only interested in the values. Vector<CGPDFObjectRef> objects; getAllValuesInPDFNameTree(javaScriptNameTree, objects); size_t objectCount = objects.size(); for (size_t i = 0; i < objectCount; ++i) { CGPDFDictionaryRef javaScriptAction; if (!CGPDFObjectGetValue(reinterpret_cast<CGPDFObjectRef>(objects[i]), kCGPDFObjectTypeDictionary, &javaScriptAction)) continue; // A JavaScript action must have an action type of "JavaScript". const char* actionType; if (!CGPDFDictionaryGetName(javaScriptAction, "S", &actionType) || strcmp(actionType, "JavaScript")) continue; const UInt8* bytes = nullptr; CFIndex length = 0; CGPDFStreamRef stream; CGPDFStringRef string; RetainPtr<CFDataRef> data; if (CGPDFDictionaryGetStream(javaScriptAction, "JS", &stream)) { CGPDFDataFormat format; data = adoptCF(CGPDFStreamCopyData(stream, &format)); if (!data) continue; bytes = CFDataGetBytePtr(data.get()); length = CFDataGetLength(data.get()); } else if (CGPDFDictionaryGetString(javaScriptAction, "JS", &string)) { bytes = CGPDFStringGetBytePtr(string); length = CGPDFStringGetLength(string); } if (!bytes || !length) continue; CFStringEncoding encoding = (length > 1 && bytes[0] == 0xFE && bytes[1] == 0xFF) ? kCFStringEncodingUnicode : kCFStringEncodingUTF8; RetainPtr<CFStringRef> script = adoptCF(CFStringCreateWithBytes(kCFAllocatorDefault, bytes, length, encoding, true)); if (!script) continue; scripts.append(script); } } Ref<PDFPlugin> PDFPlugin::create(WebFrame& frame) { return adoptRef(*new PDFPlugin(frame)); } inline PDFPlugin::PDFPlugin(WebFrame& frame) : Plugin(PDFPluginType) , m_frame(makeWeakPtr(frame)) , m_containerLayer(adoptNS([[CALayer alloc] init])) , m_contentLayer(adoptNS([[CALayer alloc] init])) , m_scrollCornerLayer(adoptNS([[WKPDFPluginScrollbarLayer alloc] initWithPDFPlugin:this])) , m_pdfLayerController(adoptNS([[pdfLayerControllerClass() alloc] init])) , m_pdfLayerControllerDelegate(adoptNS([[WKPDFLayerControllerDelegate alloc] initWithPDFPlugin:this])) #if HAVE(INCREMENTAL_PDF_APIS) , m_streamLoaderClient(adoptRef(*new PDFPluginStreamLoaderClient(*this))) , m_incrementalPDFLoadingEnabled(WebCore::RuntimeEnabledFeatures::sharedFeatures().incrementalPDFLoadingEnabled()) #endif , m_identifier(PDFPluginIdentifier::generate()) { #if ENABLE(UI_PROCESS_PDF_HUD) [m_pdfLayerController setDisplaysPDFHUDController:NO]; #endif m_pdfLayerController.get().delegate = m_pdfLayerControllerDelegate.get(); m_pdfLayerController.get().parentLayer = m_contentLayer.get(); if (supportsForms()) { auto* document = frame.coreFrame()->document(); m_annotationContainer = document->createElement(divTag, false); m_annotationContainer->setAttributeWithoutSynchronization(idAttr, AtomString("annotationContainer", AtomString::ConstructFromLiteral)); auto annotationStyleElement = document->createElement(styleTag, false); annotationStyleElement->setTextContent(annotationStyle); m_annotationContainer->appendChild(annotationStyleElement); document->bodyOrFrameset()->appendChild(*m_annotationContainer); } m_accessibilityObject = adoptNS([[WKPDFPluginAccessibilityObject alloc] initWithPDFPlugin:this]); [m_accessibilityObject setPdfLayerController:m_pdfLayerController.get()]; [m_accessibilityObject setParent:frame.page()->accessibilityRemoteObject()]; [m_containerLayer addSublayer:m_contentLayer.get()]; [m_containerLayer addSublayer:m_scrollCornerLayer.get()]; #if ENABLE(WEBPROCESS_WINDOWSERVER_BLOCKING) if ([m_pdfLayerController respondsToSelector:@selector(setDeviceColorSpace:)]) { auto view = frame.coreFrame()->view(); [m_pdfLayerController setDeviceColorSpace:screenColorSpace(view)]; } #endif #if HAVE(INCREMENTAL_PDF_APIS) if (m_incrementalPDFLoadingEnabled) { m_pdfThread = Thread::create("PDF document thread", [protectedThis = makeRef(*this), this] () mutable { threadEntry(WTFMove(protectedThis)); }); } #endif } PDFPlugin::~PDFPlugin() { #if ENABLE(UI_PROCESS_PDF_HUD) if (auto* page = m_frame ? m_frame->page() : nullptr) page->removePDFHUD(*this); #endif } #if HAVE(INCREMENTAL_PDF_APIS) #if !LOG_DISABLED void PDFPlugin::pdfLog(const String& message) { if (!isMainThread()) { callOnMainThread([this, protectedThis = makeRef(*this), message = message.isolatedCopy()] { pdfLog(message); }); return; } LOG_WITH_STREAM(IncrementalPDF, stream << message); verboseLog(); LOG_WITH_STREAM(IncrementalPDFVerbose, stream << message); } void PDFPlugin::logStreamLoader(WTF::TextStream& stream, WebCore::NetscapePlugInStreamLoader& loader) { ASSERT(isMainThread()); auto* request = byteRangeRequestForLoader(loader); stream << "("; if (!request) { stream << "no range request found) "; return; } stream << request->position() << "-" << request->position() + request->count() - 1 << ") "; } void PDFPlugin::verboseLog() { ASSERT(isMainThread()); TextStream stream; stream << "\n"; if (m_pdfThread) stream << "Initial PDF thread is still in progress\n"; else stream << "Initial PDF thread has completed\n"; stream << "Have completed " << m_completedRangeRequests << " range requests (" << m_completedNetworkRangeRequests << " from the network)\n"; stream << "There are " << m_threadsWaitingOnCallback << " data provider threads waiting on a reply\n"; stream << "There are " << m_outstandingByteRangeRequests.size() << " byte range requests outstanding\n"; stream << "There are " << m_streamLoaderMap.size() << " active network stream loaders: "; for (auto& loader : m_streamLoaderMap.keys()) logStreamLoader(stream, *loader); stream << "\n"; stream << "The main document loader has finished loading " << m_streamedBytes << " bytes, and is"; if (!m_documentFinishedLoading) stream << " not"; stream << " complete"; LOG(IncrementalPDFVerbose, "%s", stream.release().utf8().data()); } #endif // !LOG_DISABLED void PDFPlugin::receivedNonLinearizedPDFSentinel() { m_incrementalPDFLoadingEnabled = false; if (m_hasBeenDestroyed) return; if (!isMainThread()) { #if !LOG_DISABLED pdfLog("Disabling incremental PDF loading on background thread"); #endif callOnMainThread([this, protectedThis = makeRef(*this)] { receivedNonLinearizedPDFSentinel(); }); return; } #if !LOG_DISABLED pdfLog(makeString("Cancelling all ", m_streamLoaderMap.size(), " range request loaders")); #endif for (auto iterator = m_streamLoaderMap.begin(); iterator != m_streamLoaderMap.end(); iterator = m_streamLoaderMap.begin()) { removeOutstandingByteRangeRequest(iterator->value); cancelAndForgetLoader(*iterator->key); } if (!m_documentFinishedLoading || m_pdfDocument) return; m_pdfDocument = adoptNS([[pdfDocumentClass() alloc] initWithData:rawData()]); installPDFDocument(); tryRunScriptsInPDFDocument(); } static size_t dataProviderGetBytesAtPositionCallback(void* info, void* buffer, off_t position, size_t count) { Ref<PDFPlugin> plugin = *((PDFPlugin*)info); if (isMainThread()) { #if !LOG_DISABLED plugin->pdfLog(makeString("Handling request for ", count, " bytes at position ", position, " synchronously on the main thread")); #endif return plugin->getResourceBytesAtPositionMainThread(buffer, position, count); } // It's possible we previously encountered an invalid range and therefore disabled incremental loading, // but PDFDocument is still trying to read data using a different strategy. // Always ignore such requests. if (!plugin->incrementalPDFLoadingEnabled()) return 0; #if !LOG_DISABLED auto debugPluginRef = plugin.copyRef(); debugPluginRef->incrementThreadsWaitingOnCallback(); debugPluginRef->pdfLog(makeString("PDF data provider requesting ", count, " bytes at position ", position)); #endif if (position > nonLinearizedPDFSentinel) { #if !LOG_DISABLED plugin->pdfLog(makeString("Received invalid range request for ", count, " bytes at position ", position, ". PDF is probably not linearized. Falling back to non-incremental loading.")); #endif plugin->receivedNonLinearizedPDFSentinel(); return 0; } WTF::Semaphore dataSemaphore { 0 }; size_t bytesProvided = 0; RunLoop::main().dispatch([plugin = WTFMove(plugin), position, count, buffer, &dataSemaphore, &bytesProvided] { plugin->getResourceBytesAtPosition(count, position, [count, buffer, &dataSemaphore, &bytesProvided](const uint8_t* bytes, size_t bytesCount) { RELEASE_ASSERT(bytesCount <= count); memcpy(buffer, bytes, bytesCount); bytesProvided = bytesCount; dataSemaphore.signal(); }); }); dataSemaphore.wait(); #if !LOG_DISABLED debugPluginRef->decrementThreadsWaitingOnCallback(); debugPluginRef->pdfLog(makeString("PDF data provider received ", bytesProvided, " bytes of requested ", count)); #endif return bytesProvided; } static void dataProviderGetByteRangesCallback(void* info, CFMutableArrayRef buffers, const CFRange* ranges, size_t count) { ASSERT(!isMainThread()); #if !LOG_DISABLED Ref<PDFPlugin> debugPluginRef = *((PDFPlugin*)info); debugPluginRef->incrementThreadsWaitingOnCallback(); TextStream stream; stream << "PDF data provider requesting " << count << " byte ranges ("; for (size_t i = 0; i < count; ++i) { stream << ranges[i].length << " at " << ranges[i].location; if (i < count - 1) stream << ", "; } stream << ")"; debugPluginRef->pdfLog(stream.release()); #endif Ref<PDFPlugin> plugin = *((PDFPlugin*)info); WTF::Semaphore dataSemaphore { 0 }; Vector<RetainPtr<CFDataRef>> dataResults(count); // FIXME: Once we support multi-range requests, make a single request for all ranges instead of <count> individual requests. RunLoop::main().dispatch([plugin = WTFMove(plugin), &dataResults, ranges, count, &dataSemaphore] { for (size_t i = 0; i < count; ++i) { plugin->getResourceBytesAtPosition(ranges[i].length, ranges[i].location, [i, &dataResults, &dataSemaphore](const uint8_t* bytes, size_t bytesCount) { dataResults[i] = adoptCF(CFDataCreate(kCFAllocatorDefault, bytes, bytesCount)); dataSemaphore.signal(); }); } }); for (size_t i = 0; i < count; ++i) dataSemaphore.wait(); #if !LOG_DISABLED debugPluginRef->decrementThreadsWaitingOnCallback(); debugPluginRef->pdfLog(makeString("PDF data provider finished receiving the requested ", count, " byte ranges")); #endif for (auto& result : dataResults) { if (!result) result = adoptCF(CFDataCreate(kCFAllocatorDefault, 0, 0)); CFArrayAppendValue(buffers, result.get()); } } static void dataProviderReleaseInfoCallback(void* info) { ASSERT(!isMainThread()); adoptRef((PDFPlugin*)info); } void PDFPlugin::maybeClearHighLatencyDataProviderFlag() { if (!m_pdfDocument || !m_documentFinishedLoading) return; if ([m_pdfDocument.get() respondsToSelector:@selector(setHasHighLatencyDataProvider:)]) [m_pdfDocument.get() setHasHighLatencyDataProvider:NO]; } void PDFPlugin::threadEntry(Ref<PDFPlugin>&& protectedPlugin) { CGDataProviderDirectAccessRangesCallbacks dataProviderCallbacks { 0, dataProviderGetBytesAtPositionCallback, dataProviderGetByteRangesCallback, dataProviderReleaseInfoCallback, }; auto scopeExit = makeScopeExit([protectedPlugin = WTFMove(protectedPlugin)] () mutable { // Keep the PDFPlugin alive until the end of this function and the end // of the last main thread task submitted by this function. callOnMainThread([protectedPlugin = WTFMove(protectedPlugin)] { }); }); // Balanced by a deref inside of the dataProviderReleaseInfoCallback ref(); RetainPtr<CGDataProviderRef> dataProvider = adoptCF(CGDataProviderCreateMultiRangeDirectAccess(this, kCGDataProviderIndeterminateSize, &dataProviderCallbacks)); CGDataProviderSetProperty(dataProvider.get(), kCGDataProviderHasHighLatency, kCFBooleanTrue); m_backgroundThreadDocument = adoptNS([[pdfDocumentClass() alloc] initWithProvider:dataProvider.get()]); // [PDFDocument initWithProvider:] will return nil in cases where the PDF is non-linearized. // In those cases we'll just keep buffering the entire PDF on the main thread. if (!m_backgroundThreadDocument) { LOG(IncrementalPDF, "Background thread [PDFDocument initWithProvider:] returned nil. PDF is not linearized. Reverting to main thread."); receivedNonLinearizedPDFSentinel(); return; } if (!m_incrementalPDFLoadingEnabled) { m_backgroundThreadDocument = nil; return; } WTF::Semaphore firstPageSemaphore { 0 }; auto firstPageQueue = WorkQueue::create("PDF first page work queue"); [m_backgroundThreadDocument preloadDataOfPagesInRange:NSMakeRange(0, 1) onQueue:firstPageQueue->dispatchQueue() completion:[&firstPageSemaphore, this] (NSIndexSet *) mutable { if (m_incrementalPDFLoadingEnabled) { callOnMainThread([this] { adoptBackgroundThreadDocument(); }); } else m_backgroundThreadDocument = nil; firstPageSemaphore.signal(); }]; firstPageSemaphore.wait(); #if !LOG_DISABLED pdfLog("Finished preloading first page"); #endif } void PDFPlugin::unconditionalCompleteOutstandingRangeRequests() { for (auto& request : m_outstandingByteRangeRequests.values()) request.completeUnconditionally(*this); m_outstandingByteRangeRequests.clear(); } size_t PDFPlugin::getResourceBytesAtPositionMainThread(void* buffer, off_t position, size_t count) { ASSERT(isMainThread()); ASSERT(m_documentFinishedLoading); ASSERT(position >= 0); auto cfLength = m_data ? CFDataGetLength(m_data.get()) : 0; ASSERT(cfLength >= 0); if ((unsigned)position + count > (unsigned)cfLength) { // We could return partial data, but this method should only be called // once the entire buffer is known, and therefore PDFKit should only // be asking for valid ranges. return 0; } memcpy(buffer, CFDataGetBytePtr(m_data.get()) + position, count); return count; } void PDFPlugin::getResourceBytesAtPosition(size_t count, off_t position, CompletionHandler<void(const uint8_t*, size_t)>&& completionHandler) { ASSERT(isMainThread()); ASSERT(position >= 0); ByteRangeRequest request = { static_cast<uint64_t>(position), count, WTFMove(completionHandler) }; if (request.maybeComplete(*this)) return; if (m_documentFinishedLoading) { request.completeUnconditionally(*this); return; } auto identifier = request.identifier(); m_outstandingByteRangeRequests.set(identifier, WTFMove(request)); if (!m_frame) return; auto* coreFrame = m_frame->coreFrame(); if (!coreFrame) return; auto* documentLoader = coreFrame->loader().documentLoader(); if (!documentLoader) return; auto resourceRequest = documentLoader->request(); resourceRequest.setURL(m_sourceURL); resourceRequest.setHTTPHeaderField(HTTPHeaderName::Range, makeString("bytes="_s, position, "-"_s, position + count - 1)); resourceRequest.setCachePolicy(ResourceRequestCachePolicy::DoNotUseAnyCache); #if !LOG_DISABLED pdfLog(makeString("Scheduling a stream loader for request ", identifier, " (", count, " bytes at ", position, ")")); #endif WebProcess::singleton().webLoaderStrategy().schedulePluginStreamLoad(*coreFrame, m_streamLoaderClient, WTFMove(resourceRequest), [this, protectedThis = makeRef(*this), identifier] (RefPtr<WebCore::NetscapePlugInStreamLoader>&& loader) { if (!loader) return; auto iterator = m_outstandingByteRangeRequests.find(identifier); if (iterator == m_outstandingByteRangeRequests.end()) { loader->cancel(loader->cancelledError()); return; } iterator->value.setStreamLoader(loader.get()); m_streamLoaderMap.set(WTFMove(loader), identifier); #if !LOG_DISABLED pdfLog(makeString("There are now ", m_streamLoaderMap.size(), " stream loaders in flight")); #endif }); } void PDFPlugin::adoptBackgroundThreadDocument() { if (m_hasBeenDestroyed) return; ASSERT(!m_pdfDocument); ASSERT(m_backgroundThreadDocument); ASSERT(isMainThread()); #if !LOG_DISABLED pdfLog("Adopting PDFDocument from background thread"); #endif m_pdfDocument = WTFMove(m_backgroundThreadDocument); // If the plugin was manually destroyed, the m_pdfThread might already be gone. if (m_pdfThread) { m_pdfThread->waitForCompletion(); m_pdfThread = nullptr; } // If the plugin is being destroyed, no point in doing any more PDF work if (m_isBeingDestroyed) return; installPDFDocument(); } void PDFPlugin::ByteRangeRequest::clearStreamLoader() { ASSERT(m_streamLoader); m_streamLoader = nullptr; m_accumulatedData.clear(); } void PDFPlugin::ByteRangeRequest::completeWithBytes(const uint8_t* data, size_t count, PDFPlugin& plugin) { #if !LOG_DISABLED ++plugin.m_completedRangeRequests; plugin.pdfLog(makeString("Completing range request ", identifier()," (", m_count," bytes at ", m_position,") with ", count," bytes from the main PDF buffer")); #endif m_completionHandler(data, count); if (m_streamLoader) plugin.forgetLoader(*m_streamLoader); } void PDFPlugin::ByteRangeRequest::completeWithAccumulatedData(PDFPlugin& plugin) { #if !LOG_DISABLED ++plugin.m_completedRangeRequests; ++plugin.m_completedNetworkRangeRequests; plugin.pdfLog(makeString("Completing range request ", identifier()," (", m_count," bytes at ", m_position,") with ", m_accumulatedData.size()," bytes from the network")); #endif auto completionSize = m_accumulatedData.size(); if (completionSize > m_count) { RELEASE_LOG_ERROR(IncrementalPDF, "PDF byte range request got more bytes back from the server than requested. This is likely due to a misconfigured server. Capping result at the requested number of bytes."); completionSize = m_count; } m_completionHandler(m_accumulatedData.data(), completionSize); // Fold this data into the main data buffer so that if something in its range is requested again (which happens quite often) // we do not need to hit the network layer again. plugin.ensureDataBufferLength(m_position + m_accumulatedData.size()); if (m_accumulatedData.size()) { memcpy(CFDataGetMutableBytePtr(plugin.m_data.get()) + m_position, m_accumulatedData.data(), m_accumulatedData.size()); plugin.m_completedRanges.add({ m_position, m_position + m_accumulatedData.size() - 1}); } if (m_streamLoader) plugin.forgetLoader(*m_streamLoader); } bool PDFPlugin::ByteRangeRequest::maybeComplete(PDFPlugin& plugin) { if (!plugin.m_data) return false; if (plugin.m_streamedBytes >= m_position + m_count) { completeWithBytes(CFDataGetBytePtr(plugin.m_data.get()) + m_position, m_count, plugin); return true; } if (plugin.m_completedRanges.contains({ m_position, m_position + m_count - 1 })) { #if !LOG_DISABLED plugin.pdfLog(makeString("Completing request %llu with a previously completed range", identifier())); #endif completeWithBytes(CFDataGetBytePtr(plugin.m_data.get()) + m_position, m_count, plugin); return true; } return false; } void PDFPlugin::ByteRangeRequest::completeUnconditionally(PDFPlugin& plugin) { if (m_position >= plugin.m_streamedBytes || !plugin.m_data) { completeWithBytes(nullptr, 0, plugin); return; } auto count = m_position + m_count > plugin.m_streamedBytes ? plugin.m_streamedBytes - m_position : m_count; completeWithBytes(CFDataGetBytePtr(plugin.m_data.get()) + m_position, count, plugin); } void PDFPlugin::PDFPluginStreamLoaderClient::willSendRequest(NetscapePlugInStreamLoader* loader, ResourceRequest&&, const ResourceResponse&, CompletionHandler<void(ResourceRequest&&)>&&) { if (!m_pdfPlugin) return; // Redirections for range requests are unexpected. m_pdfPlugin->cancelAndForgetLoader(*loader); } void PDFPlugin::PDFPluginStreamLoaderClient::didReceiveResponse(NetscapePlugInStreamLoader* loader, const ResourceResponse& response) { if (!m_pdfPlugin) return; auto* request = m_pdfPlugin->byteRangeRequestForLoader(*loader); if (!request) { m_pdfPlugin->cancelAndForgetLoader(*loader); return; } ASSERT(request->streamLoader() == loader); // Range success! We'll expect to receive the data in future didReceiveData callbacks. if (response.httpStatusCode() == 206) return; // If the response wasn't a successful range response, we don't need this stream loader anymore. // This can happen, for example, if the server doesn't support range requests. // We'll still resolve the ByteRangeRequest later once enough of the full resource has loaded. m_pdfPlugin->cancelAndForgetLoader(*loader); // The server might support range requests and explicitly told us this range was not satisfiable. // In this case, we can reject the ByteRangeRequest right away. if (response.httpStatusCode() == 416 && request) { request->completeWithAccumulatedData(*m_pdfPlugin); m_pdfPlugin->removeOutstandingByteRangeRequest(request->identifier()); } } void PDFPlugin::PDFPluginStreamLoaderClient::didReceiveData(NetscapePlugInStreamLoader* loader, const char* data, int count) { if (!m_pdfPlugin) return; auto* request = m_pdfPlugin->byteRangeRequestForLoader(*loader); if (!request) return; request->addData(reinterpret_cast<const uint8_t*>(data), count); } void PDFPlugin::PDFPluginStreamLoaderClient::didFail(NetscapePlugInStreamLoader* loader, const ResourceError&) { if (!m_pdfPlugin) return; if (m_pdfPlugin->documentFinishedLoading()) { auto identifier = m_pdfPlugin->identifierForLoader(loader); if (identifier) m_pdfPlugin->removeOutstandingByteRangeRequest(identifier); } m_pdfPlugin->forgetLoader(*loader); } void PDFPlugin::PDFPluginStreamLoaderClient::didFinishLoading(NetscapePlugInStreamLoader* loader) { if (!m_pdfPlugin) return; auto* request = m_pdfPlugin->byteRangeRequestForLoader(*loader); if (!request) return; request->completeWithAccumulatedData(*m_pdfPlugin); m_pdfPlugin->removeOutstandingByteRangeRequest(request->identifier()); } PDFPlugin::ByteRangeRequest* PDFPlugin::byteRangeRequestForLoader(NetscapePlugInStreamLoader& loader) { uint64_t identifier = identifierForLoader(&loader); if (!identifier) return nullptr; auto request = m_outstandingByteRangeRequests.find(identifier); if (request == m_outstandingByteRangeRequests.end()) return nullptr; return &(request->value); } void PDFPlugin::forgetLoader(NetscapePlugInStreamLoader& loader) { uint64_t identifier = identifierForLoader(&loader); if (!identifier) { ASSERT(!m_streamLoaderMap.contains(&loader)); return; } if (auto* request = byteRangeRequestForLoader(loader)) { if (request->streamLoader()) { ASSERT(request->streamLoader() == &loader); request->clearStreamLoader(); } } m_streamLoaderMap.remove(&loader); #if !LOG_DISABLED pdfLog(makeString("Forgot stream loader for range request ", identifier,". There are now ", m_streamLoaderMap.size()," stream loaders remaining")); #endif } void PDFPlugin::cancelAndForgetLoader(NetscapePlugInStreamLoader& loader) { auto protectedLoader = makeRef(loader); forgetLoader(loader); loader.cancel(loader.cancelledError()); } #endif // HAVE(INCREMENTAL_PDF_APIS) PluginInfo PDFPlugin::pluginInfo() { PluginInfo info; info.name = builtInPDFPluginName(); info.isApplicationPlugin = true; info.clientLoadPolicy = PluginLoadClientPolicy::Undefined; info.bundleIdentifier = "com.apple.webkit.builtinpdfplugin"_s; MimeClassInfo pdfMimeClassInfo; pdfMimeClassInfo.type = "application/pdf"; pdfMimeClassInfo.desc = pdfDocumentTypeDescription(); pdfMimeClassInfo.extensions.append("pdf"); info.mimes.append(pdfMimeClassInfo); MimeClassInfo textPDFMimeClassInfo; textPDFMimeClassInfo.type = "text/pdf"; textPDFMimeClassInfo.desc = pdfDocumentTypeDescription(); textPDFMimeClassInfo.extensions.append("pdf"); info.mimes.append(textPDFMimeClassInfo); MimeClassInfo postScriptMimeClassInfo; postScriptMimeClassInfo.type = postScriptMIMEType; postScriptMimeClassInfo.desc = postScriptDocumentTypeDescription(); postScriptMimeClassInfo.extensions.append("ps"); info.mimes.append(postScriptMimeClassInfo); return info; } void PDFPlugin::updateScrollbars() { if (m_hasBeenDestroyed) return; bool hadScrollbars = m_horizontalScrollbar || m_verticalScrollbar; if (m_horizontalScrollbar) { if (m_size.width() >= m_pdfDocumentSize.width()) destroyScrollbar(HorizontalScrollbar); } else if (m_size.width() < m_pdfDocumentSize.width()) m_horizontalScrollbar = createScrollbar(HorizontalScrollbar); if (m_verticalScrollbar) { if (m_size.height() >= m_pdfDocumentSize.height()) destroyScrollbar(VerticalScrollbar); } else if (m_size.height() < m_pdfDocumentSize.height()) m_verticalScrollbar = createScrollbar(VerticalScrollbar); IntSize scrollbarSpace = scrollbarIntrusion(); if (m_horizontalScrollbar) { m_horizontalScrollbar->setSteps(Scrollbar::pixelsPerLineStep(), m_firstPageHeight); m_horizontalScrollbar->setProportion(m_size.width() - scrollbarSpace.width(), m_pdfDocumentSize.width()); IntRect scrollbarRect(pluginView()->x(), pluginView()->y() + m_size.height() - m_horizontalScrollbar->height(), m_size.width(), m_horizontalScrollbar->height()); if (m_verticalScrollbar) scrollbarRect.contract(m_verticalScrollbar->width(), 0); m_horizontalScrollbar->setFrameRect(scrollbarRect); } if (m_verticalScrollbar) { m_verticalScrollbar->setSteps(Scrollbar::pixelsPerLineStep(), m_firstPageHeight); m_verticalScrollbar->setProportion(m_size.height() - scrollbarSpace.height(), m_pdfDocumentSize.height()); IntRect scrollbarRect(IntRect(pluginView()->x() + m_size.width() - m_verticalScrollbar->width(), pluginView()->y(), m_verticalScrollbar->width(), m_size.height())); if (m_horizontalScrollbar) scrollbarRect.contract(0, m_horizontalScrollbar->height()); m_verticalScrollbar->setFrameRect(scrollbarRect); } auto* frameView = m_frame ? m_frame->coreFrame()->view() : nullptr; if (!frameView) return; bool hasScrollbars = m_horizontalScrollbar || m_verticalScrollbar; if (hadScrollbars != hasScrollbars) { if (hasScrollbars) frameView->addScrollableArea(this); else frameView->removeScrollableArea(this); frameView->setNeedsLayoutAfterViewConfigurationChange(); } if (m_verticalScrollbarLayer) { m_verticalScrollbarLayer.get().frame = verticalScrollbar()->frameRect(); [m_verticalScrollbarLayer setNeedsDisplay]; } if (m_horizontalScrollbarLayer) { m_horizontalScrollbarLayer.get().frame = horizontalScrollbar()->frameRect(); [m_horizontalScrollbarLayer setNeedsDisplay]; } if (m_scrollCornerLayer) { m_scrollCornerLayer.get().frame = scrollCornerRect(); [m_scrollCornerLayer setNeedsDisplay]; } } PluginView* PDFPlugin::pluginView() { return static_cast<PluginView*>(controller()); } const PluginView* PDFPlugin::pluginView() const { return static_cast<const PluginView*>(controller()); } Ref<Scrollbar> PDFPlugin::createScrollbar(ScrollbarOrientation orientation) { auto widget = Scrollbar::createNativeScrollbar(*this, orientation, ScrollbarControlSize::Regular); if (orientation == HorizontalScrollbar) { m_horizontalScrollbarLayer = adoptNS([[WKPDFPluginScrollbarLayer alloc] initWithPDFPlugin:this]); [m_containerLayer addSublayer:m_horizontalScrollbarLayer.get()]; } else { m_verticalScrollbarLayer = adoptNS([[WKPDFPluginScrollbarLayer alloc] initWithPDFPlugin:this]); [m_containerLayer addSublayer:m_verticalScrollbarLayer.get()]; } didAddScrollbar(widget.ptr(), orientation); if (auto* frame = m_frame ? m_frame->coreFrame() : nullptr) { if (auto* page = frame->page()) { if (page->isMonitoringWheelEvents()) scrollAnimator().setWheelEventTestMonitor(page->wheelEventTestMonitor()); } } // Is it ever possible that the code above and the code below can ever get at different Frames? // Can't we settle on one Frame accessor? if (auto* frame = pluginView()->frame()) { if (auto* frameView = frame->view()) frameView->addChild(widget); } return widget; } void PDFPlugin::destroyScrollbar(ScrollbarOrientation orientation) { RefPtr<Scrollbar>& scrollbar = orientation == HorizontalScrollbar ? m_horizontalScrollbar : m_verticalScrollbar; if (!scrollbar) return; willRemoveScrollbar(scrollbar.get(), orientation); scrollbar->removeFromParent(); scrollbar = nullptr; if (orientation == HorizontalScrollbar) { [m_horizontalScrollbarLayer removeFromSuperlayer]; m_horizontalScrollbarLayer = 0; } else { [m_verticalScrollbarLayer removeFromSuperlayer]; m_verticalScrollbarLayer = 0; } } IntRect PDFPlugin::convertFromScrollbarToContainingView(const Scrollbar& scrollbar, const IntRect& scrollbarRect) const { IntRect rect = scrollbarRect; rect.move(scrollbar.location() - pluginView()->location()); return pluginView()->frame()->view()->convertFromRendererToContainingView(pluginView()->pluginElement()->renderer(), rect); } IntRect PDFPlugin::convertFromContainingViewToScrollbar(const Scrollbar& scrollbar, const IntRect& parentRect) const { IntRect rect = pluginView()->frame()->view()->convertFromContainingViewToRenderer(pluginView()->pluginElement()->renderer(), parentRect); rect.move(pluginView()->location() - scrollbar.location()); return rect; } IntPoint PDFPlugin::convertFromScrollbarToContainingView(const Scrollbar& scrollbar, const IntPoint& scrollbarPoint) const { IntPoint point = scrollbarPoint; point.move(scrollbar.location() - pluginView()->location()); return pluginView()->frame()->view()->convertFromRendererToContainingView(pluginView()->pluginElement()->renderer(), point); } IntPoint PDFPlugin::convertFromContainingViewToScrollbar(const Scrollbar& scrollbar, const IntPoint& parentPoint) const { IntPoint point = pluginView()->frame()->view()->convertFromContainingViewToRenderer(pluginView()->pluginElement()->renderer(), parentPoint); point.move(pluginView()->location() - scrollbar.location()); return point; } String PDFPlugin::debugDescription() const { return makeString("PDFPlugin 0x", hex(reinterpret_cast<uintptr_t>(this), Lowercase)); } bool PDFPlugin::handleScroll(ScrollDirection direction, ScrollGranularity granularity) { return scroll(direction, granularity); } IntRect PDFPlugin::scrollCornerRect() const { if (!m_horizontalScrollbar || !m_verticalScrollbar) return IntRect(); if (m_horizontalScrollbar->isOverlayScrollbar()) { ASSERT(m_verticalScrollbar->isOverlayScrollbar()); return IntRect(); } return IntRect(pluginView()->width() - m_verticalScrollbar->width(), pluginView()->height() - m_horizontalScrollbar->height(), m_verticalScrollbar->width(), m_horizontalScrollbar->height()); } ScrollableArea* PDFPlugin::enclosingScrollableArea() const { // FIXME: Walk up the frame tree and look for a scrollable parent frame or RenderLayer. return nullptr; } IntRect PDFPlugin::scrollableAreaBoundingBox(bool*) const { return pluginView()->frameRect(); } bool PDFPlugin::isActive() const { if (auto* coreFrame = m_frame ? m_frame->coreFrame() : nullptr) { if (auto* page = coreFrame->page()) return page->focusController().isActive(); } return false; } bool PDFPlugin::forceUpdateScrollbarsOnMainThreadForPerformanceTesting() const { if (auto* coreFrame = m_frame ? m_frame->coreFrame() : nullptr) { if (auto* page = coreFrame->page()) return page->settings().scrollingPerformanceTestingEnabled(); } return false; } ScrollPosition PDFPlugin::scrollPosition() const { return IntPoint(m_scrollOffset.width(), m_scrollOffset.height()); } ScrollPosition PDFPlugin::minimumScrollPosition() const { return IntPoint(); } ScrollPosition PDFPlugin::maximumScrollPosition() const { IntSize scrollbarSpace = scrollbarIntrusion(); IntPoint maximumOffset(m_pdfDocumentSize.width() - m_size.width() + scrollbarSpace.width(), m_pdfDocumentSize.height() - m_size.height() + scrollbarSpace.height()); maximumOffset.clampNegativeToZero(); return maximumOffset; } void PDFPlugin::scrollbarStyleChanged(ScrollbarStyle style, bool forceUpdate) { if (!forceUpdate) return; if (m_hasBeenDestroyed) return; // If the PDF was scrolled all the way to bottom right and scrollbars change to overlay style, we don't want to display white rectangles where scrollbars were. IntPoint newScrollOffset = IntPoint(m_scrollOffset).shrunkTo(maximumScrollPosition()); setScrollOffset(newScrollOffset); ScrollableArea::scrollbarStyleChanged(style, forceUpdate); // As size of the content area changes, scrollbars may need to appear or to disappear. updateScrollbars(); } void PDFPlugin::addArchiveResource() { // FIXME: It's a hack to force add a resource to DocumentLoader. PDF documents should just be fetched as CachedResources. // Add just enough data for context menu handling and web archives to work. NSDictionary* headers = @{ @"Content-Disposition": (NSString *)m_suggestedFilename, @"Content-Type" : @"application/pdf" }; RetainPtr<NSURLResponse> response = adoptNS([[NSHTTPURLResponse alloc] initWithURL:m_sourceURL statusCode:200 HTTPVersion:(NSString*)kCFHTTPVersion1_1 headerFields:headers]); ResourceResponse synthesizedResponse(response.get()); auto resource = ArchiveResource::create(SharedBuffer::create(m_data.get()), m_sourceURL, "application/pdf", String(), String(), synthesizedResponse); pluginView()->frame()->document()->loader()->addArchiveResource(resource.releaseNonNull()); } static void jsPDFDocInitialize(JSContextRef ctx, JSObjectRef object) { PDFPlugin* pdfView = static_cast<PDFPlugin*>(JSObjectGetPrivate(object)); pdfView->ref(); } static void jsPDFDocFinalize(JSObjectRef object) { PDFPlugin* pdfView = static_cast<PDFPlugin*>(JSObjectGetPrivate(object)); pdfView->deref(); } JSValueRef PDFPlugin::jsPDFDocPrint(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) { auto* pdfPlugin = static_cast<PDFPlugin*>(JSObjectGetPrivate(thisObject)); auto* frame = pdfPlugin->m_frame.get(); if (!frame) return JSValueMakeUndefined(ctx); auto* coreFrame = frame->coreFrame(); if (!coreFrame) return JSValueMakeUndefined(ctx); auto* page = coreFrame->page(); if (!page) return JSValueMakeUndefined(ctx); page->chrome().print(*coreFrame); return JSValueMakeUndefined(ctx); } JSObjectRef PDFPlugin::makeJSPDFDoc(JSContextRef ctx) { static JSStaticFunction jsPDFDocStaticFunctions[] = { { "print", jsPDFDocPrint, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete }, { 0, 0, 0 }, }; static JSClassDefinition jsPDFDocClassDefinition = { 0, kJSClassAttributeNone, "Doc", 0, 0, jsPDFDocStaticFunctions, jsPDFDocInitialize, jsPDFDocFinalize, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; static JSClassRef jsPDFDocClass = JSClassCreate(&jsPDFDocClassDefinition); return JSObjectMake(ctx, jsPDFDocClass, this); } void PDFPlugin::convertPostScriptDataIfNeeded() { if (!m_isPostScript) return; m_data = PDFDocumentImage::convertPostScriptDataToPDF(WTFMove(m_data)); } void PDFPlugin::documentDataDidFinishLoading() { if (m_hasBeenDestroyed) return; addArchiveResource(); m_documentFinishedLoading = true; #if HAVE(INCREMENTAL_PDF_APIS) #if !LOG_DISABLED pdfLog(makeString("PDF document finished loading with a total of ", m_streamedBytes, " bytes")); #endif if (m_incrementalPDFLoadingEnabled) { // At this point we know all data for the document, and therefore we know how to answer any outstanding range requests. unconditionalCompleteOutstandingRangeRequests(); maybeClearHighLatencyDataProviderFlag(); } else #endif { m_pdfDocument = adoptNS([[pdfDocumentClass() alloc] initWithData:rawData()]); installPDFDocument(); } tryRunScriptsInPDFDocument(); } void PDFPlugin::installPDFDocument() { ASSERT(m_pdfDocument); ASSERT(isMainThread()); LOG(IncrementalPDF, "Installing PDF document"); if (m_hasBeenDestroyed) return; if (!controller()) { RELEASE_LOG(IncrementalPDF, "PDFPlugin::installPDFDocument called - Plug-in has not been destroyed, but there's also no controller."); return; } #if HAVE(INCREMENTAL_PDF_APIS) maybeClearHighLatencyDataProviderFlag(); #endif updatePageAndDeviceScaleFactors(); [m_pdfLayerController setFrameSize:size()]; m_pdfLayerController.get().document = m_pdfDocument.get(); if (handlesPageScaleFactor()) pluginView()->setPageScaleFactor([m_pdfLayerController contentScaleFactor], IntPoint()); notifyScrollPositionChanged(IntPoint([m_pdfLayerController scrollPosition])); calculateSizes(); updateScrollbars(); tryRunScriptsInPDFDocument(); if ([m_pdfDocument isLocked]) createPasswordEntryForm(); if (m_frame && [m_pdfLayerController respondsToSelector:@selector(setURLFragment:)]) [m_pdfLayerController setURLFragment:m_frame->url().fragmentIdentifier().createNSString().get()]; } void PDFPlugin::setSuggestedFilename(const String& suggestedFilename) { m_suggestedFilename = suggestedFilename; if (m_suggestedFilename.isEmpty()) m_suggestedFilename = suggestedFilenameWithMIMEType(nil, "application/pdf"); if (!m_suggestedFilename.endsWithIgnoringASCIICase(".pdf")) m_suggestedFilename.append(".pdf"); } void PDFPlugin::streamDidReceiveResponse(uint64_t streamID, const URL&, uint32_t, uint32_t, const String& mimeType, const String&, const String& suggestedFilename) { ASSERT_UNUSED(streamID, streamID == pdfDocumentRequestID); setSuggestedFilename(suggestedFilename); if (equalIgnoringASCIICase(mimeType, postScriptMIMEType)) m_isPostScript = true; } void PDFPlugin::streamDidReceiveData(uint64_t streamID, const char* bytes, int length) { ASSERT_UNUSED(streamID, streamID == pdfDocumentRequestID); if (!m_data) m_data = adoptCF(CFDataCreateMutable(0, 0)); CFDataAppendBytes(m_data.get(), reinterpret_cast<const UInt8*>(bytes), length); m_streamedBytes += length; } void PDFPlugin::streamDidFinishLoading(uint64_t streamID) { ASSERT_UNUSED(streamID, streamID == pdfDocumentRequestID); convertPostScriptDataIfNeeded(); documentDataDidFinishLoading(); } void PDFPlugin::streamDidFail(uint64_t streamID, bool wasCancelled) { ASSERT_UNUSED(streamID, streamID == pdfDocumentRequestID); m_data.clear(); } void PDFPlugin::manualStreamDidReceiveResponse(const URL& responseURL, uint32_t streamLength, uint32_t lastModifiedTime, const String& mimeType, const String& headers, const String& suggestedFilename) { setSuggestedFilename(suggestedFilename); if (equalIgnoringASCIICase(mimeType, postScriptMIMEType)) m_isPostScript = true; } void PDFPlugin::ensureDataBufferLength(uint64_t targetLength) { if (!m_data) m_data = adoptCF(CFDataCreateMutable(0, 0)); auto currentLength = CFDataGetLength(m_data.get()); ASSERT(currentLength >= 0); if (targetLength > (uint64_t)currentLength) CFDataIncreaseLength(m_data.get(), targetLength - currentLength); } void PDFPlugin::manualStreamDidReceiveData(const char* bytes, int length) { if (!m_data) m_data = adoptCF(CFDataCreateMutable(0, 0)); ensureDataBufferLength(m_streamedBytes + length); memcpy(CFDataGetMutableBytePtr(m_data.get()) + m_streamedBytes, bytes, length); m_streamedBytes += length; #if HAVE(INCREMENTAL_PDF_APIS) // Keep our ranges-lookup-table compact by continuously updating its first range // as the entire document streams in from the network. m_completedRanges.add({ 0, m_streamedBytes - 1 }); if (m_incrementalPDFLoadingEnabled) { HashSet<uint64_t> handledRequests; for (auto& request : m_outstandingByteRangeRequests.values()) { if (request.maybeComplete(*this)) handledRequests.add(request.identifier()); } for (auto identifier : handledRequests) { auto request = m_outstandingByteRangeRequests.take(identifier); if (request.streamLoader()) cancelAndForgetLoader(*request.streamLoader()); } } #endif } void PDFPlugin::manualStreamDidFinishLoading() { convertPostScriptDataIfNeeded(); documentDataDidFinishLoading(); } void PDFPlugin::manualStreamDidFail(bool) { m_data = nullptr; #if HAVE(INCREMENTAL_PDF_APIS) if (m_incrementalPDFLoadingEnabled) unconditionalCompleteOutstandingRangeRequests(); #endif } void PDFPlugin::tryRunScriptsInPDFDocument() { ASSERT(isMainThread()); if (!m_pdfDocument || !m_documentFinishedLoading) return; auto completionHandler = [this, protectedThis = makeRef(*this)] (Vector<RetainPtr<CFStringRef>>&& scripts) mutable { if (scripts.isEmpty()) return; JSGlobalContextRef ctx = JSGlobalContextCreate(nullptr); JSObjectRef jsPDFDoc = makeJSPDFDoc(ctx); for (auto& script : scripts) JSEvaluateScript(ctx, OpaqueJSString::tryCreate(script.get()).get(), jsPDFDoc, nullptr, 1, nullptr); JSGlobalContextRelease(ctx); }; #if HAVE(INCREMENTAL_PDF_APIS) auto scriptUtilityQueue = WorkQueue::create("PDF script utility"); auto& rawQueue = scriptUtilityQueue.get(); RetainPtr<CGPDFDocumentRef> document = [m_pdfDocument documentRef]; rawQueue.dispatch([scriptUtilityQueue = WTFMove(scriptUtilityQueue), completionHandler = WTFMove(completionHandler), document = WTFMove(document)] () mutable { ASSERT(!isMainThread()); Vector<RetainPtr<CFStringRef>> scripts; getAllScriptsInPDFDocument(document.get(), scripts); callOnMainThread([completionHandler = WTFMove(completionHandler), scripts = WTFMove(scripts)] () mutable { completionHandler(WTFMove(scripts)); }); }); #else Vector<RetainPtr<CFStringRef>> scripts; getAllScriptsInPDFDocument([m_pdfDocument documentRef], scripts); completionHandler(WTFMove(scripts)); #endif } void PDFPlugin::createPasswordEntryForm() { if (!supportsForms()) return; m_passwordField = PDFPluginPasswordField::create(m_pdfLayerController.get(), this); m_passwordField->attach(m_annotationContainer.get()); } void PDFPlugin::attemptToUnlockPDF(const String& password) { [m_pdfLayerController attemptToUnlockWithPassword:password]; if (![m_pdfDocument isLocked]) { m_passwordField = nullptr; calculateSizes(); updateScrollbars(); } } void PDFPlugin::updatePageAndDeviceScaleFactors() { if (!controller()) return; double newScaleFactor = controller()->contentsScaleFactor(); if (!handlesPageScaleFactor()) { if (auto* page = m_frame ? m_frame->page() : nullptr) newScaleFactor *= page->pageScaleFactor(); } if (newScaleFactor != [m_pdfLayerController deviceScaleFactor]) [m_pdfLayerController setDeviceScaleFactor:newScaleFactor]; } void PDFPlugin::contentsScaleFactorChanged(float) { updatePageAndDeviceScaleFactors(); } IntRect PDFPlugin::frameForHUD() const { return convertFromPDFViewToRootView(IntRect(IntPoint(), size())); } void PDFPlugin::calculateSizes() { if ([m_pdfDocument isLocked]) { m_firstPageHeight = 0; setPDFDocumentSize(IntSize(0, 0)); return; } m_firstPageHeight = [m_pdfDocument pageCount] ? static_cast<unsigned>(CGCeiling([[m_pdfDocument pageAtIndex:0] boundsForBox:kPDFDisplayBoxCropBox].size.height)) : 0; setPDFDocumentSize(IntSize([m_pdfLayerController contentSizeRespectingZoom])); #if ENABLE(UI_PROCESS_PDF_HUD) if (!m_frame || !m_frame->page()) return; m_frame->page()->updatePDFHUDLocation(*this, frameForHUD()); #endif } bool PDFPlugin::initialize(const Parameters& parameters) { m_sourceURL = parameters.url; if (!parameters.shouldUseManualLoader && !parameters.url.isEmpty()) controller()->loadURL(pdfDocumentRequestID, "GET", parameters.url.string(), String(), HTTPHeaderMap(), Vector<uint8_t>(), false); controller()->didInitializePlugin(); return true; } void PDFPlugin::willDetachRenderer() { if (!m_frame || !m_frame->coreFrame()) return; if (auto* frameView = m_frame->coreFrame()->view()) frameView->removeScrollableArea(this); } void PDFPlugin::destroy() { m_hasBeenDestroyed = true; m_documentFinishedLoading = true; #if HAVE(INCREMENTAL_PDF_APIS) // By clearing out the resource data and handling all outstanding range requests, // we can force the PDFThread to complete quickly if (m_pdfThread) { m_data = nullptr; unconditionalCompleteOutstandingRangeRequests(); m_pdfThread->waitForCompletion(); } #endif m_pdfLayerController.get().delegate = 0; if (auto* frameView = m_frame && m_frame->coreFrame() ? m_frame->coreFrame()->view() : nullptr) frameView->removeScrollableArea(this); m_activeAnnotation = nullptr; m_annotationContainer = nullptr; destroyScrollbar(HorizontalScrollbar); destroyScrollbar(VerticalScrollbar); [m_scrollCornerLayer removeFromSuperlayer]; [m_contentLayer removeFromSuperlayer]; } void PDFPlugin::updateControlTints(GraphicsContext& graphicsContext) { ASSERT(graphicsContext.invalidatingControlTints()); if (m_horizontalScrollbar) m_horizontalScrollbar->invalidate(); if (m_verticalScrollbar) m_verticalScrollbar->invalidate(); invalidateScrollCorner(scrollCornerRect()); } void PDFPlugin::paintControlForLayerInContext(CALayer *layer, CGContextRef context) { #if PLATFORM(MAC) auto* page = m_frame && m_frame->coreFrame() ? m_frame->coreFrame()->page() : nullptr; if (!page) return; LocalDefaultSystemAppearance localAppearance(page->useDarkAppearance()); #endif GraphicsContext graphicsContext(context); GraphicsContextStateSaver stateSaver(graphicsContext); graphicsContext.setIsCALayerContext(true); if (layer == m_scrollCornerLayer) { IntRect scrollCornerRect = this->scrollCornerRect(); graphicsContext.translate(-scrollCornerRect.x(), -scrollCornerRect.y()); ScrollbarTheme::theme().paintScrollCorner(graphicsContext, scrollCornerRect); return; } Scrollbar* scrollbar = nullptr; if (layer == m_verticalScrollbarLayer) scrollbar = verticalScrollbar(); else if (layer == m_horizontalScrollbarLayer) scrollbar = horizontalScrollbar(); if (!scrollbar) return; graphicsContext.translate(-scrollbar->x(), -scrollbar->y()); scrollbar->paint(graphicsContext, scrollbar->frameRect()); } RefPtr<ShareableBitmap> PDFPlugin::snapshot() { if (size().isEmpty()) return nullptr; float contentsScaleFactor = controller()->contentsScaleFactor(); IntSize backingStoreSize = size(); backingStoreSize.scale(contentsScaleFactor); auto bitmap = ShareableBitmap::createShareable(backingStoreSize, { }); if (!bitmap) return nullptr; auto context = bitmap->createGraphicsContext(); if (!context) return nullptr; context->scale(FloatSize(contentsScaleFactor, -contentsScaleFactor)); context->translate(-m_scrollOffset.width(), -m_pdfDocumentSize.height() + m_scrollOffset.height()); [m_pdfLayerController snapshotInContext:context->platformContext()]; return bitmap; } PlatformLayer* PDFPlugin::pluginLayer() { return m_containerLayer.get(); } IntPoint PDFPlugin::convertFromPluginToPDFView(const IntPoint& point) const { return IntPoint(point.x(), size().height() - point.y()); } IntPoint PDFPlugin::convertFromRootViewToPlugin(const IntPoint& point) const { return m_rootViewToPluginTransform.mapPoint(point); } IntPoint PDFPlugin::convertFromPDFViewToRootView(const IntPoint& point) const { IntPoint pointInPluginCoordinates(point.x(), size().height() - point.y()); return m_rootViewToPluginTransform.inverse().valueOr(AffineTransform()).mapPoint(pointInPluginCoordinates); } IntRect PDFPlugin::convertFromPDFViewToRootView(const IntRect& rect) const { IntRect rectInPluginCoordinates(rect.x(), rect.y(), rect.width(), rect.height()); return m_rootViewToPluginTransform.inverse().valueOr(AffineTransform()).mapRect(rectInPluginCoordinates); } IntPoint PDFPlugin::convertFromRootViewToPDFView(const IntPoint& point) const { IntPoint pointInPluginCoordinates = m_rootViewToPluginTransform.mapPoint(point); return IntPoint(pointInPluginCoordinates.x(), size().height() - pointInPluginCoordinates.y()); } FloatRect PDFPlugin::convertFromPDFViewToScreen(const FloatRect& rect) const { auto* coreFrame = m_frame ? m_frame->coreFrame() : nullptr; if (!coreFrame) return { }; auto* frameView = coreFrame->view(); if (!frameView) return { }; FloatRect updatedRect = rect; updatedRect.setLocation(convertFromPDFViewToRootView(IntPoint(updatedRect.location()))); auto* page = coreFrame->page(); if (!page) return { }; return page->chrome().rootViewToScreen(enclosingIntRect(updatedRect)); } IntRect PDFPlugin::boundsOnScreen() const { auto* frameView = m_frame ? m_frame->coreFrame()->view() : nullptr; if (!frameView) return { }; FloatRect bounds = FloatRect(FloatPoint(), size()); FloatRect rectInRootViewCoordinates = m_rootViewToPluginTransform.inverse().valueOr(AffineTransform()).mapRect(bounds); return frameView->contentsToScreen(enclosingIntRect(rectInRootViewCoordinates)); } void PDFPlugin::visibilityDidChange(bool visible) { #if ENABLE(UI_PROCESS_PDF_HUD) if (!m_frame) return; if (visible) m_frame->page()->createPDFHUD(*this, frameForHUD()); else m_frame->page()->removePDFHUD(*this); #else UNUSED_PARAM(visible); #endif } void PDFPlugin::geometryDidChange(const IntSize& pluginSize, const IntRect&, const AffineTransform& pluginToRootViewTransform) { if (size() == pluginSize && pluginView()->pageScaleFactor() == [m_pdfLayerController contentScaleFactor]) return; m_size = pluginSize; m_rootViewToPluginTransform = pluginToRootViewTransform.inverse().valueOr(AffineTransform()); [m_pdfLayerController setFrameSize:pluginSize]; [CATransaction begin]; [CATransaction setDisableActions:YES]; CATransform3D transform = CATransform3DMakeScale(1, -1, 1); transform = CATransform3DTranslate(transform, 0, -pluginSize.height(), 0); if (handlesPageScaleFactor()) { CGFloat magnification = pluginView()->pageScaleFactor() - [m_pdfLayerController contentScaleFactor]; // FIXME: Instead of m_lastMousePositionInPluginCoordinates, we should use the zoom origin from PluginView::setPageScaleFactor. if (magnification) [m_pdfLayerController magnifyWithMagnification:magnification atPoint:convertFromPluginToPDFView(m_lastMousePositionInPluginCoordinates) immediately:NO]; } else { // If we don't handle page scale ourselves, we need to respect our parent page's // scale, which may have changed. updatePageAndDeviceScaleFactors(); } calculateSizes(); updateScrollbars(); if (m_activeAnnotation) m_activeAnnotation->updateGeometry(); [m_contentLayer setSublayerTransform:transform]; [CATransaction commit]; } void PDFPlugin::frameDidFinishLoading(uint64_t) { ASSERT_NOT_REACHED(); } void PDFPlugin::frameDidFail(uint64_t, bool) { ASSERT_NOT_REACHED(); } void PDFPlugin::didEvaluateJavaScript(uint64_t, const WTF::String&) { ASSERT_NOT_REACHED(); } static NSUInteger modifierFlagsFromWebEvent(const WebEvent& event) { return (event.shiftKey() ? NSEventModifierFlagShift : 0) | (event.controlKey() ? NSEventModifierFlagControl : 0) | (event.altKey() ? NSEventModifierFlagOption : 0) | (event.metaKey() ? NSEventModifierFlagCommand : 0); } static bool getEventTypeFromWebEvent(const WebEvent& event, NSEventType& eventType) { switch (event.type()) { case WebEvent::KeyDown: eventType = NSEventTypeKeyDown; return true; case WebEvent::KeyUp: eventType = NSEventTypeKeyUp; return true; case WebEvent::MouseDown: switch (static_cast<const WebMouseEvent&>(event).button()) { case WebMouseEvent::LeftButton: eventType = NSEventTypeLeftMouseDown; return true; case WebMouseEvent::RightButton: eventType = NSEventTypeRightMouseDown; return true; default: return false; } case WebEvent::MouseUp: switch (static_cast<const WebMouseEvent&>(event).button()) { case WebMouseEvent::LeftButton: eventType = NSEventTypeLeftMouseUp; return true; case WebMouseEvent::RightButton: eventType = NSEventTypeRightMouseUp; return true; default: return false; } case WebEvent::MouseMove: switch (static_cast<const WebMouseEvent&>(event).button()) { case WebMouseEvent::LeftButton: eventType = NSEventTypeLeftMouseDragged; return true; case WebMouseEvent::RightButton: eventType = NSEventTypeRightMouseDragged; return true; case WebMouseEvent::NoButton: eventType = NSEventTypeMouseMoved; return true; default: return false; } default: return false; } } NSEvent *PDFPlugin::nsEventForWebMouseEvent(const WebMouseEvent& event) { m_lastMousePositionInPluginCoordinates = convertFromRootViewToPlugin(event.position()); IntPoint positionInPDFViewCoordinates(convertFromPluginToPDFView(m_lastMousePositionInPluginCoordinates)); NSEventType eventType; if (!getEventTypeFromWebEvent(event, eventType)) return 0; NSUInteger modifierFlags = modifierFlagsFromWebEvent(event); return [NSEvent mouseEventWithType:eventType location:positionInPDFViewCoordinates modifierFlags:modifierFlags timestamp:0 windowNumber:0 context:nil eventNumber:0 clickCount:event.clickCount() pressure:0]; } bool PDFPlugin::handleMouseEvent(const WebMouseEvent& event) { PlatformMouseEvent platformEvent = platform(event); IntPoint mousePosition = convertFromRootViewToPlugin(event.position()); m_lastMouseEvent = event; RefPtr<Scrollbar> targetScrollbar; RefPtr<Scrollbar> targetScrollbarForLastMousePosition; if (m_verticalScrollbarLayer) { IntRect verticalScrollbarFrame(m_verticalScrollbarLayer.get().frame); if (verticalScrollbarFrame.contains(mousePosition)) targetScrollbar = verticalScrollbar(); if (verticalScrollbarFrame.contains(m_lastMousePositionInPluginCoordinates)) targetScrollbarForLastMousePosition = verticalScrollbar(); } if (m_horizontalScrollbarLayer) { IntRect horizontalScrollbarFrame(m_horizontalScrollbarLayer.get().frame); if (horizontalScrollbarFrame.contains(mousePosition)) targetScrollbar = horizontalScrollbar(); if (horizontalScrollbarFrame.contains(m_lastMousePositionInPluginCoordinates)) targetScrollbarForLastMousePosition = horizontalScrollbar(); } if (m_scrollCornerLayer && IntRect(m_scrollCornerLayer.get().frame).contains(mousePosition)) return false; if ([m_pdfDocument isLocked]) return false; // Right-clicks and Control-clicks always call handleContextMenuEvent as well. if (event.button() == WebMouseEvent::RightButton || (event.button() == WebMouseEvent::LeftButton && event.controlKey())) return true; NSEvent *nsEvent = nsEventForWebMouseEvent(event); switch (event.type()) { case WebEvent::MouseMove: mouseMovedInContentArea(); if (targetScrollbar) { if (!targetScrollbarForLastMousePosition) { targetScrollbar->mouseEntered(); return true; } return targetScrollbar->mouseMoved(platformEvent); } if (!targetScrollbar && targetScrollbarForLastMousePosition) targetScrollbarForLastMousePosition->mouseExited(); switch (event.button()) { case WebMouseEvent::LeftButton: [m_pdfLayerController mouseDragged:nsEvent]; return true; case WebMouseEvent::RightButton: case WebMouseEvent::MiddleButton: return false; case WebMouseEvent::NoButton: [m_pdfLayerController mouseMoved:nsEvent]; return true; } break; case WebEvent::MouseDown: switch (event.button()) { case WebMouseEvent::LeftButton: if (targetScrollbar) return targetScrollbar->mouseDown(platformEvent); [m_pdfLayerController mouseDown:nsEvent]; return true; case WebMouseEvent::RightButton: [m_pdfLayerController rightMouseDown:nsEvent]; return true; case WebMouseEvent::MiddleButton: case WebMouseEvent::NoButton: return false; } break; case WebEvent::MouseUp: switch (event.button()) { case WebMouseEvent::LeftButton: if (targetScrollbar) return targetScrollbar->mouseUp(platformEvent); [m_pdfLayerController mouseUp:nsEvent]; return true; case WebMouseEvent::RightButton: case WebMouseEvent::MiddleButton: case WebMouseEvent::NoButton: return false; } break; default: break; } return false; } bool PDFPlugin::handleMouseEnterEvent(const WebMouseEvent& event) { mouseEnteredContentArea(); return false; } bool PDFPlugin::handleMouseLeaveEvent(const WebMouseEvent&) { mouseExitedContentArea(); return false; } bool PDFPlugin::showContextMenuAtPoint(const IntPoint& point) { auto* frameView = m_frame ? m_frame->coreFrame()->view() : nullptr; if (!frameView) return false; IntPoint contentsPoint = frameView->contentsToRootView(point); WebMouseEvent event(WebEvent::MouseDown, WebMouseEvent::RightButton, 0, contentsPoint, contentsPoint, 0, 0, 0, 1, OptionSet<WebEvent::Modifier> { }, WallTime::now(), WebCore::ForceAtClick); return handleContextMenuEvent(event); } bool PDFPlugin::handleContextMenuEvent(const WebMouseEvent& event) { if (!m_frame || !m_frame->coreFrame()) return false; auto* webPage = m_frame->page(); if (!webPage) return false; auto* frameView = m_frame->coreFrame()->view(); if (!frameView) return false; IntPoint point = frameView->contentsToScreen(IntRect(frameView->windowToContents(event.position()), IntSize())).location(); NSUserInterfaceLayoutDirection uiLayoutDirection = webPage->userInterfaceLayoutDirection() == UserInterfaceLayoutDirection::LTR ? NSUserInterfaceLayoutDirectionLeftToRight : NSUserInterfaceLayoutDirectionRightToLeft; NSMenu *nsMenu = [m_pdfLayerController menuForEvent:nsEventForWebMouseEvent(event) withUserInterfaceLayoutDirection:uiLayoutDirection]; if (!nsMenu) return false; Optional<int> openInPreviewIndex; Vector<PDFContextMenuItem> items; auto itemCount = [nsMenu numberOfItems]; for (int i = 0; i < itemCount; i++) { auto item = [nsMenu itemAtIndex:i]; if ([item submenu]) continue; if ([NSStringFromSelector(item.action) isEqualToString:@"openWithPreview"]) openInPreviewIndex = i; PDFContextMenuItem menuItem { String([item title]), !![item isEnabled], !![item isSeparatorItem], static_cast<int>([item state]), !![item action], i }; items.append(WTFMove(menuItem)); } PDFContextMenu contextMenu { point, WTFMove(items), WTFMove(openInPreviewIndex) }; Optional<int> selectedIndex = -1; webPage->sendSync(Messages::WebPageProxy::ShowPDFContextMenu(contextMenu, m_identifier), Messages::WebPageProxy::ShowPDFContextMenu::Reply(selectedIndex)); if (selectedIndex && *selectedIndex >= 0 && *selectedIndex < itemCount) [nsMenu performActionForItemAtIndex:*selectedIndex]; return true; } bool PDFPlugin::handleKeyboardEvent(const WebKeyboardEvent& event) { NSEventType eventType; if (!getEventTypeFromWebEvent(event, eventType)) return false; NSUInteger modifierFlags = modifierFlagsFromWebEvent(event); NSEvent *fakeEvent = [NSEvent keyEventWithType:eventType location:NSZeroPoint modifierFlags:modifierFlags timestamp:0 windowNumber:0 context:0 characters:event.text() charactersIgnoringModifiers:event.unmodifiedText() isARepeat:event.isAutoRepeat() keyCode:event.nativeVirtualKeyCode()]; switch (event.type()) { case WebEvent::KeyDown: return [m_pdfLayerController keyDown:fakeEvent]; default: return false; } return false; } bool PDFPlugin::handleEditingCommand(const String& commandName, const String& argument) { if (commandName == "copy") [m_pdfLayerController copySelection]; else if (commandName == "selectAll") [m_pdfLayerController selectAll]; else if (commandName == "takeFindStringFromSelection") { NSString *string = [m_pdfLayerController currentSelection].string; ALLOW_DEPRECATED_DECLARATIONS_BEGIN if (string.length) writeItemsToPasteboard(NSFindPboard, @[ [string dataUsingEncoding:NSUTF8StringEncoding] ], @[ NSPasteboardTypeString ]); ALLOW_DEPRECATED_DECLARATIONS_END } return true; } bool PDFPlugin::isEditingCommandEnabled(const String& commandName) { if (commandName == "copy" || commandName == "takeFindStringFromSelection") return [m_pdfLayerController currentSelection]; if (commandName == "selectAll") return true; return false; } void PDFPlugin::setScrollOffset(const ScrollOffset& offset) { m_scrollOffset = IntSize(offset.x(), offset.y()); [CATransaction begin]; [m_pdfLayerController setScrollPosition:offset]; if (m_activeAnnotation) m_activeAnnotation->updateGeometry(); [CATransaction commit]; } void PDFPlugin::invalidateScrollbarRect(Scrollbar& scrollbar, const IntRect& rect) { if (&scrollbar == horizontalScrollbar()) [m_horizontalScrollbarLayer setNeedsDisplay]; else if (&scrollbar == verticalScrollbar()) [m_verticalScrollbarLayer setNeedsDisplay]; } void PDFPlugin::invalidateScrollCornerRect(const IntRect& rect) { [m_scrollCornerLayer setNeedsDisplay]; } bool PDFPlugin::isFullFramePlugin() const { // <object> or <embed> plugins will appear to be in their parent frame, so we have to // check whether our frame's widget is exactly our PluginView. if (!m_frame || !m_frame->coreFrame()) return false; auto* document = m_frame->coreFrame()->document(); if (!document) return false; return document->isPluginDocument() && static_cast<PluginDocument*>(document)->pluginWidget() == pluginView(); } bool PDFPlugin::handlesPageScaleFactor() const { return m_frame && m_frame->isMainFrame() && isFullFramePlugin(); } void PDFPlugin::clickedLink(NSURL *url) { URL coreURL = url; if (coreURL.protocolIsJavaScript()) return; auto* frame = m_frame ? m_frame->coreFrame() : nullptr; if (!frame) return; RefPtr<Event> coreEvent; if (m_lastMouseEvent.type() != WebEvent::NoType) coreEvent = MouseEvent::create(eventNames().clickEvent, &frame->windowProxy(), platform(m_lastMouseEvent), 0, 0); frame->loader().changeLocation(coreURL, emptyString(), coreEvent.get(), ReferrerPolicy::NoReferrer, ShouldOpenExternalURLsPolicy::ShouldAllow); } void PDFPlugin::setActiveAnnotation(PDFAnnotation *annotation) { if (!supportsForms()) return; if (m_activeAnnotation) m_activeAnnotation->commit(); if (annotation) { ALLOW_DEPRECATED_DECLARATIONS_BEGIN if ([annotation isKindOfClass:pdfAnnotationTextWidgetClass()] && static_cast<PDFAnnotationTextWidget *>(annotation).isReadOnly) { m_activeAnnotation = nullptr; return; } ALLOW_DEPRECATED_DECLARATIONS_END m_activeAnnotation = PDFPluginAnnotation::create(annotation, m_pdfLayerController.get(), this); m_activeAnnotation->attach(m_annotationContainer.get()); } else m_activeAnnotation = nullptr; } bool PDFPlugin::supportsForms() { // FIXME: We support forms for full-main-frame and <iframe> PDFs, but not <embed> or <object>, because those cases do not have their own Document into which to inject form elements. return isFullFramePlugin(); } void PDFPlugin::notifyContentScaleFactorChanged(CGFloat scaleFactor) { if (handlesPageScaleFactor()) pluginView()->setPageScaleFactor(scaleFactor, IntPoint()); calculateSizes(); updateScrollbars(); } void PDFPlugin::notifyDisplayModeChanged(int) { calculateSizes(); updateScrollbars(); } RefPtr<SharedBuffer> PDFPlugin::liveResourceData() const { NSData *pdfData = liveData(); if (!pdfData) return nullptr; return SharedBuffer::create(pdfData); } bool PDFPlugin::pluginHandlesContentOffsetForAccessibilityHitTest() const { // The PDF plugin handles the scroll view offset natively as part of the layer conversions. return true; } #if ENABLE(UI_PROCESS_PDF_HUD) void PDFPlugin::zoomIn() { [m_pdfLayerController zoomIn:nil]; } void PDFPlugin::zoomOut() { [m_pdfLayerController zoomOut:nil]; } void PDFPlugin::save(CompletionHandler<void(const String&, const URL&, const IPC::DataReference&)>&& completionHandler) { NSData *data = liveData(); URL url; if (m_frame) url = m_frame->url(); completionHandler(m_suggestedFilename, url, IPC:: DataReference(static_cast<const uint8_t*>(data.bytes), data.length)); } void PDFPlugin::openWithPreview(CompletionHandler<void(const String&, FrameInfoData&&, const IPC::DataReference&, const String&)>&& completionHandler) { NSData *data = liveData(); FrameInfoData frameInfo; if (m_frame) frameInfo = m_frame->info(); completionHandler(m_suggestedFilename, WTFMove(frameInfo), IPC:: DataReference { static_cast<const uint8_t*>(data.bytes), data.length }, createCanonicalUUIDString()); } #else // ENABLE(UI_PROCESS_PDF_HUD) void PDFPlugin::saveToPDF() { // FIXME: We should probably notify the user that they can't save before the document is finished loading. // PDFViewController does an NSBeep(), but that seems insufficient. if (!m_documentFinishedLoading) return; NSData *data = liveData(); if (!m_frame || !m_frame->page()) return; m_frame->page()->savePDFToFileInDownloadsFolder(m_suggestedFilename, m_frame->url(), static_cast<const unsigned char *>([data bytes]), [data length]); } void PDFPlugin::openWithNativeApplication() { if (!m_frame || !m_frame->page()) return; if (!m_temporaryPDFUUID) { // FIXME: We should probably notify the user that they can't save before the document is finished loading. // PDFViewController does an NSBeep(), but that seems insufficient. if (!m_documentFinishedLoading) return; NSData *data = liveData(); m_temporaryPDFUUID = createCanonicalUUIDString(); ASSERT(m_temporaryPDFUUID); m_frame->page()->savePDFToTemporaryFolderAndOpenWithNativeApplication(m_suggestedFilename, m_frame->info(), static_cast<const unsigned char *>([data bytes]), [data length], m_temporaryPDFUUID); return; } m_frame->page()->send(Messages::WebPageProxy::OpenPDFFromTemporaryFolderWithNativeApplication(m_frame->info(), m_temporaryPDFUUID)); } #endif // ENABLE(UI_PROCESS_PDF_HUD) void PDFPlugin::writeItemsToPasteboard(NSString *pasteboardName, NSArray *items, NSArray *types) { auto pasteboardTypes = makeVector<String>(types); int64_t newChangeCount; auto& webProcess = WebProcess::singleton(); webProcess.parentProcessConnection()->sendSync(Messages::WebPasteboardProxy::SetPasteboardTypes(pasteboardName, pasteboardTypes), Messages::WebPasteboardProxy::SetPasteboardTypes::Reply(newChangeCount), 0); for (NSUInteger i = 0, count = items.count; i < count; ++i) { NSString *type = [types objectAtIndex:i]; NSData *data = [items objectAtIndex:i]; // We don't expect the data for any items to be empty, but aren't completely sure. // Avoid crashing in the SharedMemory constructor in release builds if we're wrong. ASSERT(data.length); if (!data.length) continue; if ([type isEqualToString:legacyStringPasteboardType()] || [type isEqualToString:NSPasteboardTypeString]) { RetainPtr<NSString> plainTextString = adoptNS([[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); webProcess.parentProcessConnection()->sendSync(Messages::WebPasteboardProxy::SetPasteboardStringForType(pasteboardName, type, plainTextString.get()), Messages::WebPasteboardProxy::SetPasteboardStringForType::Reply(newChangeCount), 0); } else { auto buffer = SharedBuffer::create(data); SharedMemory::Handle handle; RefPtr<SharedMemory> sharedMemory = SharedMemory::allocate(buffer->size()); memcpy(sharedMemory->data(), buffer->data(), buffer->size()); sharedMemory->createHandle(handle, SharedMemory::Protection::ReadOnly); webProcess.parentProcessConnection()->sendSync(Messages::WebPasteboardProxy::SetPasteboardBufferForType(pasteboardName, type, SharedMemory::IPCHandle { WTFMove(handle), buffer->size() }), Messages::WebPasteboardProxy::SetPasteboardBufferForType::Reply(newChangeCount), 0); } } } void PDFPlugin::showDefinitionForAttributedString(NSAttributedString *string, CGPoint point) { DictionaryPopupInfo dictionaryPopupInfo; dictionaryPopupInfo.origin = convertFromPDFViewToRootView(IntPoint(point)); dictionaryPopupInfo.attributedString = string; NSRect rangeRect; rangeRect.origin = NSMakePoint(point.x, point.y); CGFloat scaleFactor = PDFPlugin::scaleFactor(); rangeRect.size.height = string.size.height * scaleFactor; rangeRect.size.width = string.size.width * scaleFactor; rangeRect.origin.y -= rangeRect.size.height; TextIndicatorData dataForSelection; dataForSelection.selectionRectInRootViewCoordinates = rangeRect; dataForSelection.textBoundingRectInRootViewCoordinates = rangeRect; dataForSelection.contentImageScaleFactor = scaleFactor; dataForSelection.presentationTransition = TextIndicatorPresentationTransition::FadeIn; dictionaryPopupInfo.textIndicator = dataForSelection; if (!m_frame || !m_frame->page()) return; m_frame->page()->send(Messages::WebPageProxy::DidPerformDictionaryLookup(dictionaryPopupInfo)); } unsigned PDFPlugin::countFindMatches(const String& target, WebCore::FindOptions options, unsigned /*maxMatchCount*/) { // FIXME: Why is it OK to ignore the passed-in maximum match count? if (!target.length()) return 0; NSStringCompareOptions nsOptions = options.contains(WebCore::CaseInsensitive) ? NSCaseInsensitiveSearch : 0; return [[m_pdfDocument findString:target withOptions:nsOptions] count]; } PDFSelection *PDFPlugin::nextMatchForString(const String& target, BOOL searchForward, BOOL caseSensitive, BOOL wrapSearch, PDFSelection *initialSelection, BOOL startInSelection) { if (!target.length()) return nil; NSStringCompareOptions options = 0; if (!searchForward) options |= NSBackwardsSearch; if (!caseSensitive) options |= NSCaseInsensitiveSearch; RetainPtr<PDFSelection> selectionForInitialSearch = adoptNS([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 (searchForward) { [selectionForInitialSearch extendSelectionAtStart:1]; [selectionForInitialSearch extendSelectionAtEnd:-initialSelectionLength]; } else { [selectionForInitialSearch extendSelectionAtEnd:1]; [selectionForInitialSearch extendSelectionAtStart:-initialSelectionLength]; } } PDFSelection *foundSelection = [m_pdfDocument findString:target fromSelection:selectionForInitialSearch.get() withOptions:options]; // If we first searched in the selection, and we found the selection, search again from just past the selection. if (startInSelection && [foundSelection isEqual:initialSelection]) foundSelection = [m_pdfDocument findString:target fromSelection:initialSelection withOptions:options]; if (!foundSelection && wrapSearch) { auto emptySelection = adoptNS([[pdfSelectionClass() alloc] initWithDocument:m_pdfDocument.get()]); foundSelection = [m_pdfDocument findString:target fromSelection:emptySelection.get() withOptions:options]; } return foundSelection; } bool PDFPlugin::findString(const String& target, WebCore::FindOptions options, unsigned maxMatchCount) { bool searchForward = !options.contains(WebCore::Backwards); bool caseSensitive = !options.contains(WebCore::CaseInsensitive); bool wrapSearch = options.contains(WebCore::WrapAround); // If the max was zero, any result means we exceeded the max, so we can skip computing the actual count. // FIXME: How can always returning true without searching if passed a max of 0 be right? // Even if it is right, why not put that special case inside countFindMatches instead of here? bool foundMatch = !maxMatchCount || countFindMatches(target, options, maxMatchCount); if (target.isEmpty()) { auto searchSelection = [m_pdfLayerController searchSelection]; [m_pdfLayerController findString:target caseSensitive:caseSensitive highlightMatches:YES]; [m_pdfLayerController setSearchSelection:searchSelection]; m_lastFoundString = emptyString(); return false; } if (m_lastFoundString == target) { auto selection = nextMatchForString(target, searchForward, caseSensitive, wrapSearch, [m_pdfLayerController searchSelection], NO); if (!selection) return false; [m_pdfLayerController setSearchSelection:selection]; [m_pdfLayerController gotoSelection:selection]; } else { [m_pdfLayerController findString:target caseSensitive:caseSensitive highlightMatches:YES]; m_lastFoundString = target; } return foundMatch; } bool PDFPlugin::performDictionaryLookupAtLocation(const WebCore::FloatPoint& point) { IntPoint localPoint = convertFromRootViewToPlugin(roundedIntPoint(point)); PDFSelection* lookupSelection = [m_pdfLayerController getSelectionForWordAtPoint:convertFromPluginToPDFView(localPoint)]; if ([[lookupSelection string] length]) [m_pdfLayerController searchInDictionaryWithSelection:lookupSelection]; return true; } void PDFPlugin::focusNextAnnotation() { [m_pdfLayerController activateNextAnnotation:false]; } void PDFPlugin::focusPreviousAnnotation() { [m_pdfLayerController activateNextAnnotation:true]; } void PDFPlugin::notifySelectionChanged(PDFSelection *) { if (!m_frame || !m_frame->page()) return; m_frame->page()->didChangeSelection(); } static const WebCore::Cursor& coreCursor(PDFLayerControllerCursorType type) { switch (type) { case kPDFLayerControllerCursorTypeHand: return WebCore::handCursor(); case kPDFLayerControllerCursorTypeIBeam: return WebCore::iBeamCursor(); case kPDFLayerControllerCursorTypePointer: default: return WebCore::pointerCursor(); } } void PDFPlugin::notifyCursorChanged(uint64_t type) { if (!m_frame || !m_frame->page()) return; m_frame->page()->send(Messages::WebPageProxy::SetCursor(coreCursor(static_cast<PDFLayerControllerCursorType>(type)))); } String PDFPlugin::getSelectionString() const { return [[m_pdfLayerController currentSelection] string]; } String PDFPlugin::getSelectionForWordAtPoint(const WebCore::FloatPoint& point) const { IntPoint pointInView = convertFromPluginToPDFView(convertFromRootViewToPlugin(roundedIntPoint(point))); PDFSelection *selectionForWord = [m_pdfLayerController getSelectionForWordAtPoint:pointInView]; [m_pdfLayerController setCurrentSelection:selectionForWord]; return [selectionForWord string]; } bool PDFPlugin::existingSelectionContainsPoint(const WebCore::FloatPoint& locationInViewCoordinates) const { PDFSelection *currentSelection = [m_pdfLayerController currentSelection]; if (!currentSelection) return false; IntPoint pointInPDFView = convertFromPluginToPDFView(convertFromRootViewToPlugin(roundedIntPoint(locationInViewCoordinates))); PDFSelection *selectionForWord = [m_pdfLayerController getSelectionForWordAtPoint:pointInPDFView]; NSUInteger currentPageIndex = [m_pdfLayerController currentPageIndex]; NSArray *selectionRects = [m_pdfLayerController rectsForSelectionInLayoutSpace:currentSelection]; if (!selectionRects || !selectionRects.count) return false; if (currentPageIndex >= selectionRects.count) currentPageIndex = selectionRects.count - 1; NSArray *wordSelectionRects = [m_pdfLayerController rectsForSelectionInLayoutSpace:selectionForWord]; if (!wordSelectionRects || !wordSelectionRects.count) return false; NSValue *selectionBounds = [selectionRects objectAtIndex:currentPageIndex]; NSValue *wordSelectionBounds = [wordSelectionRects objectAtIndex:0]; NSRect selectionBoundsRect = selectionBounds.rectValue; NSRect wordSelectionBoundsRect = wordSelectionBounds.rectValue; return NSIntersectsRect(wordSelectionBoundsRect, selectionBoundsRect); } static NSPoint pointInLayoutSpaceForPointInWindowSpace(PDFLayerController* pdfLayerController, NSPoint pointInView) { CGPoint point = NSPointToCGPoint(pointInView); CGPoint scrollOffset = [pdfLayerController scrollPosition]; CGFloat scaleFactor = [pdfLayerController contentScaleFactor]; scrollOffset.y = [pdfLayerController contentSizeRespectingZoom].height - NSRectToCGRect([pdfLayerController frame]).size.height - scrollOffset.y; CGPoint newPoint = CGPointMake(scrollOffset.x + point.x, scrollOffset.y + point.y); newPoint.x /= scaleFactor; newPoint.y /= scaleFactor; return NSPointFromCGPoint(newPoint); } std::tuple<String, PDFSelection *, NSDictionary *> PDFPlugin::lookupTextAtLocation(const WebCore::FloatPoint& locationInViewCoordinates, WebHitTestResultData& data) const { auto selection = [m_pdfLayerController currentSelection]; if (existingSelectionContainsPoint(locationInViewCoordinates)) return { selection.string, selection, nil }; IntPoint pointInPDFView = convertFromPluginToPDFView(convertFromRootViewToPlugin(roundedIntPoint(locationInViewCoordinates))); selection = [m_pdfLayerController getSelectionForWordAtPoint:pointInPDFView]; if (!selection) return { emptyString(), nil, nil }; NSPoint pointInLayoutSpace = pointInLayoutSpaceForPointInWindowSpace(m_pdfLayerController.get(), pointInPDFView); PDFPage *currentPage = [[m_pdfLayerController layout] pageNearestPoint:pointInLayoutSpace currentPage:[m_pdfLayerController currentPage]]; NSPoint pointInPageSpace = [[m_pdfLayerController layout] convertPoint:pointInLayoutSpace toPage:currentPage forScaleFactor:1.0]; for (PDFAnnotation *annotation in currentPage.annotations) { if (![annotation isKindOfClass:pdfAnnotationLinkClass()]) continue; NSRect bounds = annotation.bounds; if (!NSPointInRect(pointInPageSpace, bounds)) continue; ALLOW_DEPRECATED_DECLARATIONS_BEGIN PDFAnnotationLink *linkAnnotation = (PDFAnnotationLink *)annotation; ALLOW_DEPRECATED_DECLARATIONS_END NSURL *url = linkAnnotation.URL; if (!url) continue; data.absoluteLinkURL = url.absoluteString; data.linkLabel = selection.string; return { selection.string, selection, nil }; } auto [lookupText, options] = DictionaryLookup::stringForPDFSelection(selection); if (!lookupText.length) return { emptyString(), selection, nil }; [m_pdfLayerController setCurrentSelection:selection]; return { lookupText, selection, options }; } static NSRect rectInViewSpaceForRectInLayoutSpace(PDFLayerController* pdfLayerController, NSRect layoutSpaceRect) { CGRect newRect = NSRectToCGRect(layoutSpaceRect); CGFloat scaleFactor = pdfLayerController.contentScaleFactor; CGPoint scrollOffset = pdfLayerController.scrollPosition; scrollOffset.y = pdfLayerController.contentSizeRespectingZoom.height - NSRectToCGRect(pdfLayerController.frame).size.height - scrollOffset.y; newRect.origin.x *= scaleFactor; newRect.origin.y *= scaleFactor; newRect.size.width *= scaleFactor; newRect.size.height *= scaleFactor; newRect.origin.x -= scrollOffset.x; newRect.origin.y -= scrollOffset.y; return NSRectFromCGRect(newRect); } WebCore::AXObjectCache* PDFPlugin::axObjectCache() const { if (!m_frame || !m_frame->coreFrame() || !m_frame->coreFrame()->document()) return nullptr; return m_frame->coreFrame()->document()->axObjectCache(); } WebCore::FloatRect PDFPlugin::rectForSelectionInRootView(PDFSelection *selection) const { PDFPage *currentPage = nil; NSArray *pages = selection.pages; if (pages.count) currentPage = (PDFPage *)[pages objectAtIndex:0]; if (!currentPage) currentPage = [m_pdfLayerController currentPage]; NSRect rectInPageSpace = [selection boundsForPage:currentPage]; NSRect rectInLayoutSpace = [[m_pdfLayerController layout] convertRect:rectInPageSpace fromPage:currentPage forScaleFactor:1.0]; NSRect rectInView = rectInViewSpaceForRectInLayoutSpace(m_pdfLayerController.get(), rectInLayoutSpace); rectInView.origin = convertFromPDFViewToRootView(IntPoint(rectInView.origin)); return rectInView; } CGFloat PDFPlugin::scaleFactor() const { return [m_pdfLayerController contentScaleFactor]; } void PDFPlugin::performWebSearch(NSString *string) { if (!m_frame || !m_frame->page()) return; m_frame->page()->send(Messages::WebPageProxy::SearchTheWeb(string)); } void PDFPlugin::performSpotlightSearch(NSString *string) { if (!m_frame || !m_frame->page()) return; m_frame->page()->send(Messages::WebPageProxy::SearchWithSpotlight(string)); } bool PDFPlugin::handleWheelEvent(const WebWheelEvent& event) { PDFDisplayMode displayMode = [m_pdfLayerController displayMode]; if (displayMode == kPDFDisplaySinglePageContinuous || displayMode == kPDFDisplayTwoUpContinuous) return ScrollableArea::handleWheelEventForScrolling(platform(event), { }); NSUInteger currentPageIndex = [m_pdfLayerController currentPageIndex]; bool inFirstPage = !currentPageIndex; bool inLastPage = [m_pdfLayerController lastPageIndex] == currentPageIndex; bool atScrollTop = !scrollPosition().y(); bool atScrollBottom = scrollPosition().y() == maximumScrollPosition().y(); bool inMomentumScroll = event.momentumPhase() != WebWheelEvent::PhaseNone; int scrollMagnitudeThresholdForPageFlip = defaultScrollMagnitudeThresholdForPageFlip; // Imprecise input devices should have a lower threshold so that "clicky" scroll wheels can flip pages. if (!event.hasPreciseScrollingDeltas()) scrollMagnitudeThresholdForPageFlip = 0; if (atScrollBottom && !inLastPage && event.delta().height() < 0) { if (event.delta().height() <= -scrollMagnitudeThresholdForPageFlip && !inMomentumScroll) [m_pdfLayerController gotoNextPage]; return true; } if (atScrollTop && !inFirstPage && event.delta().height() > 0) { if (event.delta().height() >= scrollMagnitudeThresholdForPageFlip && !inMomentumScroll) { [CATransaction begin]; [m_pdfLayerController gotoPreviousPage]; scrollToOffsetWithoutAnimation(maximumScrollPosition()); [CATransaction commit]; } return true; } return ScrollableArea::handleWheelEventForScrolling(platform(event), { }); } NSData *PDFPlugin::liveData() const { if (m_activeAnnotation) m_activeAnnotation->commit(); // Save data straight from the resource instead of PDFKit if the document is // untouched by the user, so that PDFs which PDFKit can't display will still be downloadable. if (m_pdfDocumentWasMutated) return [m_pdfDocument dataRepresentation]; return rawData(); } id PDFPlugin::accessibilityAssociatedPluginParentForElement(WebCore::Element* element) const { if (!m_activeAnnotation) return nil; if (m_activeAnnotation->element() != element) return nil; return [m_activeAnnotation->annotation() accessibilityNode]; } NSObject *PDFPlugin::accessibilityObject() const { return m_accessibilityObject.get(); } } // namespace WebKit #endif // ENABLE(PDFKIT_PLUGIN)