blob: daf22dc71caf529822cab2f76f69ddc52a979c5a [file] [log] [blame]
/*
* Copyright (C) 2006, 2007, 2008, 2010 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 COMPUTER, 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 COMPUTER, 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 "core/html/ImageDocument.h"
#include "bindings/core/v8/ExceptionStatePlaceholder.h"
#include "core/HTMLNames.h"
#include "core/dom/RawDataDocumentParser.h"
#include "core/events/EventListener.h"
#include "core/events/MouseEvent.h"
#include "core/fetch/ImageResource.h"
#include "core/frame/FrameHost.h"
#include "core/frame/FrameView.h"
#include "core/frame/LocalDOMWindow.h"
#include "core/frame/LocalFrame.h"
#include "core/frame/Settings.h"
#include "core/frame/UseCounter.h"
#include "core/frame/VisualViewport.h"
#include "core/html/HTMLBodyElement.h"
#include "core/html/HTMLHeadElement.h"
#include "core/html/HTMLHtmlElement.h"
#include "core/html/HTMLImageElement.h"
#include "core/html/HTMLMetaElement.h"
#include "core/layout/LayoutObject.h"
#include "core/loader/DocumentLoader.h"
#include "core/loader/FrameLoader.h"
#include "core/loader/FrameLoaderClient.h"
#include "wtf/text/StringBuilder.h"
#include <limits>
using namespace std;
namespace blink {
using namespace HTMLNames;
class ImageEventListener : public EventListener {
public:
static PassRefPtrWillBeRawPtr<ImageEventListener> create(ImageDocument* document)
{
return adoptRefWillBeNoop(new ImageEventListener(document));
}
static const ImageEventListener* cast(const EventListener* listener)
{
return listener->type() == ImageEventListenerType
? static_cast<const ImageEventListener*>(listener)
: 0;
}
bool operator==(const EventListener& other) const override;
DEFINE_INLINE_VIRTUAL_TRACE()
{
visitor->trace(m_doc);
EventListener::trace(visitor);
}
private:
ImageEventListener(ImageDocument* document)
: EventListener(ImageEventListenerType)
, m_doc(document)
{
}
virtual void handleEvent(ExecutionContext*, Event*);
RawPtrWillBeMember<ImageDocument> m_doc;
};
class ImageDocumentParser : public RawDataDocumentParser {
public:
static PassRefPtrWillBeRawPtr<ImageDocumentParser> create(ImageDocument* document)
{
return adoptRefWillBeNoop(new ImageDocumentParser(document));
}
ImageDocument* document() const
{
return toImageDocument(RawDataDocumentParser::document());
}
private:
ImageDocumentParser(ImageDocument* document)
: RawDataDocumentParser(document)
{
UseCounter::count(document, UseCounter::ImageDocument);
}
void appendBytes(const char*, size_t) override;
virtual void finish();
};
// --------
static float pageZoomFactor(const Document* document)
{
LocalFrame* frame = document->frame();
return frame ? frame->pageZoomFactor() : 1;
}
static String imageTitle(const String& filename, const IntSize& size)
{
StringBuilder result;
result.append(filename);
result.appendLiteral(" (");
// FIXME: Localize numbers. Safari/OSX shows localized numbers with group
// separaters. For example, "1,920x1,080".
result.appendNumber(size.width());
result.append(static_cast<UChar>(0xD7)); // U+00D7 (multiplication sign)
result.appendNumber(size.height());
result.append(')');
return result.toString();
}
void ImageDocumentParser::appendBytes(const char* data, size_t length)
{
if (!length)
return;
LocalFrame* frame = document()->frame();
Settings* settings = frame->settings();
if (!frame->loader().client()->allowImage(!settings || settings->imagesEnabled(), document()->url()))
return;
if (document()->cachedImage()) {
RELEASE_ASSERT(length <= std::numeric_limits<unsigned>::max());
document()->cachedImage()->appendData(data, length);
}
if (!isDetached())
document()->imageUpdated();
}
void ImageDocumentParser::finish()
{
if (!isStopped() && document()->imageElement() && document()->cachedImage()) {
ImageResource* cachedImage = document()->cachedImage();
DocumentLoader* loader = document()->loader();
cachedImage->setResponse(loader->response());
cachedImage->setLoadFinishTime(loader->timing().responseEnd());
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.
IntSize size = flooredIntSize(cachedImage->imageSize(LayoutObject::shouldRespectImageOrientation(document()->imageElement()->layoutObject()), 1.0f));
if (size.width()) {
// Compute the title, we use the decoded filename of the resource, falling
// back on the (decoded) hostname if there is no path.
String fileName = decodeURLEscapeSequences(document()->url().lastPathComponent());
if (fileName.isEmpty())
fileName = document()->url().host();
document()->setTitle(imageTitle(fileName, size));
if (isDetached())
return;
}
document()->imageUpdated();
}
if (!isDetached())
document()->finishedParsing();
}
// --------
ImageDocument::ImageDocument(const DocumentInit& initializer)
: HTMLDocument(initializer, ImageDocumentClass)
, m_imageElement(nullptr)
, m_imageSizeIsKnown(false)
, m_didShrinkImage(false)
, m_shouldShrinkImage(shouldShrinkToFit())
, m_shrinkToFitMode(frame()->settings()->viewportEnabled() ? Viewport : Desktop)
{
setCompatibilityMode(QuirksMode);
lockCompatibilityMode();
}
PassRefPtrWillBeRawPtr<DocumentParser> ImageDocument::createParser()
{
return ImageDocumentParser::create(this);
}
void ImageDocument::createDocumentStructure()
{
RefPtrWillBeRawPtr<HTMLHtmlElement> rootElement = HTMLHtmlElement::create(*this);
appendChild(rootElement);
rootElement->insertedByParser();
frame()->loader().dispatchDocumentElementAvailable();
frame()->loader().runScriptsAtDocumentElementAvailable();
if (isStopped())
return; // runScriptsAtDocumentElementAvailable can detach the frame.
RefPtrWillBeRawPtr<HTMLHeadElement> head = HTMLHeadElement::create(*this);
RefPtrWillBeRawPtr<HTMLMetaElement> meta = HTMLMetaElement::create(*this);
meta->setAttribute(nameAttr, "viewport");
meta->setAttribute(contentAttr, "width=device-width, minimum-scale=0.1");
head->appendChild(meta);
RefPtrWillBeRawPtr<HTMLBodyElement> body = HTMLBodyElement::create(*this);
body->setAttribute(styleAttr, "margin: 0px;");
frame()->loader().client()->dispatchWillInsertBody();
m_imageElement = HTMLImageElement::create(*this);
m_imageElement->setAttribute(styleAttr, "-webkit-user-select: none");
m_imageElement->setLoadingImageDocument();
m_imageElement->setSrc(url().getString());
body->appendChild(m_imageElement.get());
if (loader() && m_imageElement->cachedImage())
m_imageElement->cachedImage()->responseReceived(loader()->response(), nullptr);
if (shouldShrinkToFit()) {
// Add event listeners
RefPtrWillBeRawPtr<EventListener> listener = ImageEventListener::create(this);
if (LocalDOMWindow* domWindow = this->domWindow())
domWindow->addEventListener("resize", listener, false);
if (m_shrinkToFitMode == Desktop)
m_imageElement->addEventListener("click", listener.release(), false);
}
rootElement->appendChild(head);
rootElement->appendChild(body);
}
float ImageDocument::scale() const
{
if (!m_imageElement || m_imageElement->document() != this)
return 1.0f;
FrameView* view = frame()->view();
if (!view)
return 1;
ASSERT(m_imageElement->cachedImage());
LayoutSize imageSize = m_imageElement->cachedImage()->imageSize(LayoutObject::shouldRespectImageOrientation(m_imageElement->layoutObject()), pageZoomFactor(this));
LayoutSize windowSize = LayoutSize(view->width(), view->height());
float widthScale = windowSize.width().toFloat() / imageSize.width().toFloat();
float heightScale = windowSize.height().toFloat() / imageSize.height().toFloat();
return min(widthScale, heightScale);
}
void ImageDocument::resizeImageToFit(ScaleType type)
{
if (!m_imageElement || m_imageElement->document() != this || (pageZoomFactor(this) > 1 && type == ScaleOnlyUnzoomedDocument))
return;
ASSERT(m_imageElement->cachedImage());
LayoutSize imageSize = m_imageElement->cachedImage()->imageSize(LayoutObject::shouldRespectImageOrientation(m_imageElement->layoutObject()), pageZoomFactor(this));
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::imageClicked(int x, int y)
{
ASSERT(m_shrinkToFitMode == Desktop);
if (!m_imageSizeIsKnown || imageFitsInWindow())
return;
m_shouldShrinkImage = !m_shouldShrinkImage;
if (m_shouldShrinkImage) {
windowSizeChanged(ScaleZoomedDocument);
} else {
restoreImageSize(ScaleZoomedDocument);
updateLayout();
double scale = this->scale();
double scrollX = x / scale - static_cast<double>(frame()->view()->width()) / 2;
double scrollY = y / scale - static_cast<double>(frame()->view()->height()) / 2;
frame()->view()->setScrollPosition(DoublePoint(scrollX, scrollY), ProgrammaticScroll);
}
}
void ImageDocument::imageUpdated()
{
ASSERT(m_imageElement);
if (m_imageSizeIsKnown)
return;
updateLayoutTree();
if (!m_imageElement->cachedImage() || m_imageElement->cachedImage()->imageSize(LayoutObject::shouldRespectImageOrientation(m_imageElement->layoutObject()), pageZoomFactor(this)).isEmpty())
return;
m_imageSizeIsKnown = true;
if (shouldShrinkToFit()) {
// Force resizing of the image
windowSizeChanged(ScaleOnlyUnzoomedDocument);
}
}
void ImageDocument::restoreImageSize(ScaleType type)
{
ASSERT(m_shrinkToFitMode == Desktop);
if (!m_imageElement || !m_imageSizeIsKnown || m_imageElement->document() != this || (pageZoomFactor(this) < 1 && type == ScaleOnlyUnzoomedDocument))
return;
ASSERT(m_imageElement->cachedImage());
LayoutSize imageSize = m_imageElement->cachedImage()->imageSize(LayoutObject::shouldRespectImageOrientation(m_imageElement->layoutObject()), 1.0f);
m_imageElement->setWidth(imageSize.width());
m_imageElement->setHeight(imageSize.height());
if (imageFitsInWindow())
m_imageElement->removeInlineStyleProperty(CSSPropertyCursor);
else
m_imageElement->setInlineStyleProperty(CSSPropertyCursor, CSSValueZoomOut);
m_didShrinkImage = false;
}
bool ImageDocument::imageFitsInWindow() const
{
ASSERT(m_shrinkToFitMode == Desktop);
if (!m_imageElement || m_imageElement->document() != this)
return true;
FrameView* view = frame()->view();
if (!view)
return true;
ASSERT(m_imageElement->cachedImage());
LayoutSize imageSize = m_imageElement->cachedImage()->imageSize(LayoutObject::shouldRespectImageOrientation(m_imageElement->layoutObject()), pageZoomFactor(this));
LayoutSize windowSize = LayoutSize(view->width(), view->height());
return imageSize.width() <= windowSize.width() && imageSize.height() <= windowSize.height();
}
void ImageDocument::windowSizeChanged(ScaleType type)
{
if (!m_imageElement || !m_imageSizeIsKnown || m_imageElement->document() != this)
return;
if (m_shrinkToFitMode == Viewport) {
// For huge images, minimum-scale=0.1 is still too big on small screens.
// Set max-width so that the image will shrink to fit the width of the screen when
// the scale is minimum.
// Don't shrink height to fit because we use width=device-width in viewport meta tag,
// and expect a full-width reading mode for normal-width-huge-height images.
int viewportWidth = frame()->host()->visualViewport().size().width();
m_imageElement->setInlineStyleProperty(CSSPropertyMaxWidth, viewportWidth * 10, CSSPrimitiveValue::UnitType::Pixels);
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(type);
else
resizeImageToFit(type);
} else {
// If the image isn't resized but needs to be, then resize it.
if (!fitsInWindow) {
resizeImageToFit(type);
m_didShrinkImage = true;
}
}
}
ImageResource* ImageDocument::cachedImage()
{
if (!m_imageElement) {
createDocumentStructure();
if (isStopped()) {
m_imageElement = nullptr;
return nullptr;
}
}
return m_imageElement->cachedImage();
}
bool ImageDocument::shouldShrinkToFit() const
{
return frame()->isMainFrame();
}
#if !ENABLE(OILPAN)
void ImageDocument::dispose()
{
m_imageElement = nullptr;
HTMLDocument::dispose();
}
#endif
DEFINE_TRACE(ImageDocument)
{
visitor->trace(m_imageElement);
HTMLDocument::trace(visitor);
}
// --------
void ImageEventListener::handleEvent(ExecutionContext*, Event* event)
{
if (event->type() == EventTypeNames::resize) {
m_doc->windowSizeChanged(ImageDocument::ScaleOnlyUnzoomedDocument);
} else if (event->type() == EventTypeNames::click && event->isMouseEvent()) {
MouseEvent* mouseEvent = toMouseEvent(event);
m_doc->imageClicked(mouseEvent->x(), mouseEvent->y());
}
}
bool ImageEventListener::operator==(const EventListener& listener) const
{
if (const ImageEventListener* imageEventListener = ImageEventListener::cast(&listener))
return m_doc == imageEventListener->m_doc;
return false;
}
} // namespace blink