450 lines
14 KiB
C++
450 lines
14 KiB
C++
/*
|
|
* Copyright (C) 2006-2019 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. ``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
|
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
* 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.
|
|
*/
|
|
|
|
#include "config.h"
|
|
#include "ImageDocument.h"
|
|
|
|
#include "AddEventListenerOptions.h"
|
|
#include "CachedImage.h"
|
|
#include "Chrome.h"
|
|
#include "ChromeClient.h"
|
|
#include "DOMWindow.h"
|
|
#include "DocumentLoader.h"
|
|
#include "EventListener.h"
|
|
#include "EventNames.h"
|
|
#include "Frame.h"
|
|
#include "FrameLoader.h"
|
|
#include "FrameLoaderClient.h"
|
|
#include "FrameView.h"
|
|
#include "HTMLBodyElement.h"
|
|
#include "HTMLHeadElement.h"
|
|
#include "HTMLHtmlElement.h"
|
|
#include "HTMLImageElement.h"
|
|
#include "HTMLNames.h"
|
|
#include "LocalizedStrings.h"
|
|
#include "MIMETypeRegistry.h"
|
|
#include "MouseEvent.h"
|
|
#include "Page.h"
|
|
#include "RawDataDocumentParser.h"
|
|
#include "RenderElement.h"
|
|
#include "Settings.h"
|
|
#include <wtf/IsoMallocInlines.h>
|
|
#include <wtf/text/StringConcatenateNumbers.h>
|
|
|
|
namespace WebCore {
|
|
|
|
WTF_MAKE_ISO_ALLOCATED_IMPL(ImageDocument);
|
|
|
|
using namespace HTMLNames;
|
|
|
|
#if !PLATFORM(IOS_FAMILY)
|
|
class ImageEventListener final : public EventListener {
|
|
public:
|
|
static Ref<ImageEventListener> create(ImageDocument& document) { return adoptRef(*new ImageEventListener(document)); }
|
|
|
|
private:
|
|
ImageEventListener(ImageDocument& document)
|
|
: EventListener(ImageEventListenerType)
|
|
, m_document(document)
|
|
{
|
|
}
|
|
|
|
bool operator==(const EventListener&) const override;
|
|
void handleEvent(ScriptExecutionContext&, Event&) override;
|
|
|
|
ImageDocument& m_document;
|
|
};
|
|
#endif
|
|
|
|
class ImageDocumentParser final : public RawDataDocumentParser {
|
|
public:
|
|
static Ref<ImageDocumentParser> create(ImageDocument& document)
|
|
{
|
|
return adoptRef(*new ImageDocumentParser(document));
|
|
}
|
|
|
|
private:
|
|
ImageDocumentParser(ImageDocument& document)
|
|
: RawDataDocumentParser(document)
|
|
{
|
|
}
|
|
|
|
ImageDocument& document() const;
|
|
|
|
void appendBytes(DocumentWriter&, const uint8_t*, size_t) override;
|
|
void finish() override;
|
|
};
|
|
|
|
class ImageDocumentElement final : public HTMLImageElement {
|
|
WTF_MAKE_ISO_ALLOCATED_INLINE(ImageDocumentElement);
|
|
public:
|
|
static Ref<ImageDocumentElement> create(ImageDocument&);
|
|
|
|
private:
|
|
ImageDocumentElement(ImageDocument& document)
|
|
: HTMLImageElement(imgTag, document)
|
|
, m_imageDocument(&document)
|
|
{
|
|
}
|
|
|
|
virtual ~ImageDocumentElement();
|
|
void didMoveToNewDocument(Document& oldDocument, Document& newDocument) override;
|
|
|
|
ImageDocument* m_imageDocument;
|
|
};
|
|
|
|
inline Ref<ImageDocumentElement> ImageDocumentElement::create(ImageDocument& document)
|
|
{
|
|
return adoptRef(*new ImageDocumentElement(document));
|
|
}
|
|
|
|
// --------
|
|
|
|
HTMLImageElement* ImageDocument::imageElement() const
|
|
{
|
|
return m_imageElement;
|
|
}
|
|
|
|
LayoutSize ImageDocument::imageSize()
|
|
{
|
|
ASSERT(m_imageElement);
|
|
updateStyleIfNeeded();
|
|
return m_imageElement->cachedImage()->imageSizeForRenderer(m_imageElement->renderer(), frame() ? frame()->pageZoomFactor() : 1);
|
|
}
|
|
|
|
void ImageDocument::updateDuringParsing()
|
|
{
|
|
if (!settings().areImagesEnabled())
|
|
return;
|
|
|
|
if (!m_imageElement)
|
|
createDocumentStructure();
|
|
|
|
if (RefPtr<SharedBuffer> buffer = loader()->mainResourceData())
|
|
m_imageElement->cachedImage()->updateBuffer(*buffer);
|
|
|
|
imageUpdated();
|
|
}
|
|
|
|
void ImageDocument::finishedParsing()
|
|
{
|
|
if (!parser()->isStopped() && m_imageElement) {
|
|
CachedImage& cachedImage = *m_imageElement->cachedImage();
|
|
RefPtr<SharedBuffer> data = loader()->mainResourceData();
|
|
|
|
// If this is a multipart image, make a copy of the current part, since the resource data
|
|
// will be overwritten by the next part.
|
|
if (data && loader()->isLoadingMultipartContent())
|
|
data = data->copy();
|
|
|
|
cachedImage.finishLoading(data.get(), { });
|
|
cachedImage.finish();
|
|
|
|
// Report the natural image size in the page title, regardless of zoom level.
|
|
// At a zoom level of 1 the image is guaranteed to have an integer size.
|
|
updateStyleIfNeeded();
|
|
IntSize size = flooredIntSize(cachedImage.imageSizeForRenderer(m_imageElement->renderer(), 1));
|
|
if (size.width()) {
|
|
// Compute the title. We use the decoded filename of the resource, falling
|
|
// back on the hostname if there is no path.
|
|
String name = decodeURLEscapeSequences(url().lastPathComponent());
|
|
if (name.isEmpty())
|
|
name = url().host().toString();
|
|
setTitle(imageTitle(name, size));
|
|
}
|
|
|
|
imageUpdated();
|
|
}
|
|
|
|
HTMLDocument::finishedParsing();
|
|
}
|
|
|
|
inline ImageDocument& ImageDocumentParser::document() const
|
|
{
|
|
// Only used during parsing, so document is guaranteed to be non-null.
|
|
ASSERT(RawDataDocumentParser::document());
|
|
return downcast<ImageDocument>(*RawDataDocumentParser::document());
|
|
}
|
|
|
|
void ImageDocumentParser::appendBytes(DocumentWriter&, const uint8_t*, size_t)
|
|
{
|
|
document().updateDuringParsing();
|
|
}
|
|
|
|
void ImageDocumentParser::finish()
|
|
{
|
|
document().finishedParsing();
|
|
}
|
|
|
|
ImageDocument::ImageDocument(Frame& frame, const URL& url)
|
|
: HTMLDocument(&frame, frame.settings(), url, ImageDocumentClass)
|
|
, m_imageElement(nullptr)
|
|
, m_imageSizeIsKnown(false)
|
|
#if !PLATFORM(IOS_FAMILY)
|
|
, m_didShrinkImage(false)
|
|
#endif
|
|
, m_shouldShrinkImage(frame.settings().shrinksStandaloneImagesToFit() && frame.isMainFrame())
|
|
{
|
|
setCompatibilityMode(DocumentCompatibilityMode::QuirksMode);
|
|
lockCompatibilityMode();
|
|
}
|
|
|
|
Ref<DocumentParser> ImageDocument::createParser()
|
|
{
|
|
return ImageDocumentParser::create(*this);
|
|
}
|
|
|
|
void ImageDocument::createDocumentStructure()
|
|
{
|
|
auto rootElement = HTMLHtmlElement::create(*this);
|
|
appendChild(rootElement);
|
|
rootElement->insertedByParser();
|
|
|
|
frame()->injectUserScripts(UserScriptInjectionTime::DocumentStart);
|
|
|
|
// We need a <head> so that the call to setTitle() later on actually has an <head> to append to <title> to.
|
|
auto head = HTMLHeadElement::create(*this);
|
|
rootElement->appendChild(head);
|
|
|
|
auto body = HTMLBodyElement::create(*this);
|
|
body->setAttribute(styleAttr, "margin: 0px");
|
|
if (MIMETypeRegistry::isPDFMIMEType(document().loader()->responseMIMEType()))
|
|
body->setInlineStyleProperty(CSSPropertyBackgroundColor, "white");
|
|
rootElement->appendChild(body);
|
|
|
|
auto imageElement = ImageDocumentElement::create(*this);
|
|
if (m_shouldShrinkImage)
|
|
imageElement->setAttribute(styleAttr, "-webkit-user-select:none; display:block; margin:auto; padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);");
|
|
else
|
|
imageElement->setAttribute(styleAttr, "-webkit-user-select:none; padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);");
|
|
imageElement->setLoadManually(true);
|
|
imageElement->setSrc(url().string());
|
|
imageElement->cachedImage()->setResponse(loader()->response());
|
|
body->appendChild(imageElement);
|
|
|
|
if (m_shouldShrinkImage) {
|
|
#if PLATFORM(IOS_FAMILY)
|
|
// Set the viewport to be in device pixels (rather than the default of 980).
|
|
processViewport("width=device-width,viewport-fit=cover"_s, ViewportArguments::ImageDocument);
|
|
#else
|
|
auto listener = ImageEventListener::create(*this);
|
|
if (RefPtr<DOMWindow> window = this->domWindow())
|
|
window->addEventListener("resize", listener.copyRef(), false);
|
|
imageElement->addEventListener("click", WTFMove(listener), false);
|
|
#endif
|
|
}
|
|
|
|
m_imageElement = imageElement.ptr();
|
|
}
|
|
|
|
void ImageDocument::imageUpdated()
|
|
{
|
|
ASSERT(m_imageElement);
|
|
|
|
if (m_imageSizeIsKnown)
|
|
return;
|
|
|
|
LayoutSize imageSize = this->imageSize();
|
|
if (imageSize.isEmpty())
|
|
return;
|
|
|
|
m_imageSizeIsKnown = true;
|
|
|
|
if (m_shouldShrinkImage) {
|
|
#if PLATFORM(IOS_FAMILY)
|
|
FloatSize screenSize = page()->chrome().screenSize();
|
|
if (imageSize.width() > screenSize.width())
|
|
processViewport(makeString("width=", imageSize.width().toInt(), ",viewport-fit=cover"), ViewportArguments::ImageDocument);
|
|
|
|
if (page())
|
|
page()->chrome().client().imageOrMediaDocumentSizeChanged(IntSize(imageSize.width(), imageSize.height()));
|
|
#else
|
|
// Call windowSizeChanged for its side effect of sizing the image.
|
|
windowSizeChanged();
|
|
#endif
|
|
}
|
|
}
|
|
|
|
#if !PLATFORM(IOS_FAMILY)
|
|
float ImageDocument::scale()
|
|
{
|
|
if (!m_imageElement)
|
|
return 1;
|
|
|
|
RefPtr<FrameView> view = this->view();
|
|
if (!view)
|
|
return 1;
|
|
|
|
LayoutSize imageSize = this->imageSize();
|
|
|
|
IntSize viewportSize = view->visibleSize();
|
|
float widthScale = viewportSize.width() / imageSize.width().toFloat();
|
|
float heightScale = viewportSize.height() / imageSize.height().toFloat();
|
|
|
|
return std::min(widthScale, heightScale);
|
|
}
|
|
|
|
void ImageDocument::resizeImageToFit()
|
|
{
|
|
if (!m_imageElement)
|
|
return;
|
|
|
|
LayoutSize imageSize = this->imageSize();
|
|
|
|
float scale = this->scale();
|
|
m_imageElement->setWidth(static_cast<int>(imageSize.width() * scale));
|
|
m_imageElement->setHeight(static_cast<int>(imageSize.height() * scale));
|
|
|
|
m_imageElement->setInlineStyleProperty(CSSPropertyCursor, CSSValueZoomIn);
|
|
}
|
|
|
|
void ImageDocument::restoreImageSize()
|
|
{
|
|
if (!m_imageElement || !m_imageSizeIsKnown)
|
|
return;
|
|
|
|
LayoutSize imageSize = this->imageSize();
|
|
m_imageElement->setWidth(imageSize.width().toUnsigned());
|
|
m_imageElement->setHeight(imageSize.height().toUnsigned());
|
|
|
|
if (imageFitsInWindow())
|
|
m_imageElement->removeInlineStyleProperty(CSSPropertyCursor);
|
|
else
|
|
m_imageElement->setInlineStyleProperty(CSSPropertyCursor, CSSValueZoomOut);
|
|
|
|
m_didShrinkImage = false;
|
|
}
|
|
|
|
bool ImageDocument::imageFitsInWindow()
|
|
{
|
|
if (!m_imageElement)
|
|
return true;
|
|
|
|
RefPtr<FrameView> view = this->view();
|
|
if (!view)
|
|
return true;
|
|
|
|
LayoutSize imageSize = this->imageSize();
|
|
IntSize viewportSize = view->visibleSize();
|
|
return imageSize.width() <= viewportSize.width() && imageSize.height() <= viewportSize.height();
|
|
}
|
|
|
|
|
|
void ImageDocument::windowSizeChanged()
|
|
{
|
|
if (!m_imageElement || !m_imageSizeIsKnown)
|
|
return;
|
|
|
|
bool fitsInWindow = imageFitsInWindow();
|
|
|
|
// If the image has been explicitly zoomed in, restore the cursor if the image fits
|
|
// and set it to a zoom out cursor if the image doesn't fit
|
|
if (!m_shouldShrinkImage) {
|
|
if (fitsInWindow)
|
|
m_imageElement->removeInlineStyleProperty(CSSPropertyCursor);
|
|
else
|
|
m_imageElement->setInlineStyleProperty(CSSPropertyCursor, CSSValueZoomOut);
|
|
return;
|
|
}
|
|
|
|
if (m_didShrinkImage) {
|
|
// If the window has been resized so that the image fits, restore the image size,
|
|
// otherwise update the restored image size.
|
|
if (fitsInWindow)
|
|
restoreImageSize();
|
|
else
|
|
resizeImageToFit();
|
|
} else {
|
|
// If the image isn't resized but needs to be, then resize it.
|
|
if (!fitsInWindow) {
|
|
resizeImageToFit();
|
|
m_didShrinkImage = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ImageDocument::imageClicked(int x, int y)
|
|
{
|
|
if (!m_imageSizeIsKnown || imageFitsInWindow())
|
|
return;
|
|
|
|
m_shouldShrinkImage = !m_shouldShrinkImage;
|
|
|
|
if (m_shouldShrinkImage) {
|
|
// Call windowSizeChanged for its side effect of sizing the image.
|
|
windowSizeChanged();
|
|
} else {
|
|
restoreImageSize();
|
|
|
|
updateLayout();
|
|
|
|
if (!view())
|
|
return;
|
|
|
|
float scale = this->scale();
|
|
|
|
IntSize viewportSize = view()->visibleSize();
|
|
int scrollX = static_cast<int>(x / scale - viewportSize.width() / 2.0f);
|
|
int scrollY = static_cast<int>(y / scale - viewportSize.height() / 2.0f);
|
|
|
|
view()->setScrollPosition(IntPoint(scrollX, scrollY));
|
|
}
|
|
}
|
|
|
|
void ImageEventListener::handleEvent(ScriptExecutionContext&, Event& event)
|
|
{
|
|
if (event.type() == eventNames().resizeEvent)
|
|
m_document.windowSizeChanged();
|
|
else if (event.type() == eventNames().clickEvent && is<MouseEvent>(event)) {
|
|
MouseEvent& mouseEvent = downcast<MouseEvent>(event);
|
|
m_document.imageClicked(mouseEvent.offsetX(), mouseEvent.offsetY());
|
|
}
|
|
}
|
|
|
|
bool ImageEventListener::operator==(const EventListener& other) const
|
|
{
|
|
// All ImageEventListener objects compare as equal; OK since there is only one per document.
|
|
return other.type() == ImageEventListenerType;
|
|
}
|
|
#endif
|
|
|
|
// --------
|
|
|
|
ImageDocumentElement::~ImageDocumentElement()
|
|
{
|
|
if (m_imageDocument)
|
|
m_imageDocument->disconnectImageElement();
|
|
}
|
|
|
|
void ImageDocumentElement::didMoveToNewDocument(Document& oldDocument, Document& newDocument)
|
|
{
|
|
if (m_imageDocument) {
|
|
m_imageDocument->disconnectImageElement();
|
|
m_imageDocument = nullptr;
|
|
}
|
|
HTMLImageElement::didMoveToNewDocument(oldDocument, newDocument);
|
|
}
|
|
|
|
}
|