1/*
2 * Copyright (C) 2006, 2007, 2008, 2010 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
20 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
22 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23 */
24
25#include "config.h"
26#include "core/html/ImageDocument.h"
27
28#include "bindings/core/v8/ExceptionStatePlaceholder.h"
29#include "core/HTMLNames.h"
30#include "core/dom/RawDataDocumentParser.h"
31#include "core/events/EventListener.h"
32#include "core/events/MouseEvent.h"
33#include "core/fetch/ImageResource.h"
34#include "core/frame/FrameView.h"
35#include "core/frame/LocalFrame.h"
36#include "core/frame/Settings.h"
37#include "core/html/HTMLBodyElement.h"
38#include "core/html/HTMLHeadElement.h"
39#include "core/html/HTMLHtmlElement.h"
40#include "core/html/HTMLImageElement.h"
41#include "core/html/HTMLMetaElement.h"
42#include "core/loader/DocumentLoader.h"
43#include "core/loader/FrameLoader.h"
44#include "core/loader/FrameLoaderClient.h"
45#include "wtf/text/StringBuilder.h"
46
47using std::min;
48
49namespace blink {
50
51using namespace HTMLNames;
52
53class ImageEventListener : public EventListener {
54public:
55    static PassRefPtr<ImageEventListener> create(ImageDocument* document) { return adoptRef(new ImageEventListener(document)); }
56    static const ImageEventListener* cast(const EventListener* listener)
57    {
58        return listener->type() == ImageEventListenerType
59            ? static_cast<const ImageEventListener*>(listener)
60            : 0;
61    }
62
63    virtual bool operator==(const EventListener& other);
64
65private:
66    ImageEventListener(ImageDocument* document)
67        : EventListener(ImageEventListenerType)
68        , m_doc(document)
69    {
70    }
71
72    virtual void handleEvent(ExecutionContext*, Event*);
73
74    ImageDocument* m_doc;
75};
76
77class ImageDocumentParser : public RawDataDocumentParser {
78public:
79    static PassRefPtrWillBeRawPtr<ImageDocumentParser> create(ImageDocument* document)
80    {
81        return adoptRefWillBeNoop(new ImageDocumentParser(document));
82    }
83
84    ImageDocument* document() const
85    {
86        return toImageDocument(RawDataDocumentParser::document());
87    }
88
89private:
90    ImageDocumentParser(ImageDocument* document)
91        : RawDataDocumentParser(document)
92    {
93    }
94
95    virtual void appendBytes(const char*, size_t) OVERRIDE;
96    virtual void finish();
97};
98
99// --------
100
101static float pageZoomFactor(const Document* document)
102{
103    LocalFrame* frame = document->frame();
104    return frame ? frame->pageZoomFactor() : 1;
105}
106
107static String imageTitle(const String& filename, const IntSize& size)
108{
109    StringBuilder result;
110    result.append(filename);
111    result.appendLiteral(" (");
112    // FIXME: Localize numbers. Safari/OSX shows localized numbers with group
113    // separaters. For example, "1,920x1,080".
114    result.appendNumber(size.width());
115    result.append(static_cast<UChar>(0xD7)); // U+00D7 (multiplication sign)
116    result.appendNumber(size.height());
117    result.append(')');
118    return result.toString();
119}
120
121void ImageDocumentParser::appendBytes(const char* data, size_t length)
122{
123    if (!length)
124        return;
125
126    LocalFrame* frame = document()->frame();
127    Settings* settings = frame->settings();
128    if (!frame->loader().client()->allowImage(!settings || settings->imagesEnabled(), document()->url()))
129        return;
130
131    if (document()->cachedImage())
132        document()->cachedImage()->appendData(data, length);
133    // Make sure the image renderer gets created because we need the renderer
134    // to read the aspect ratio. See crbug.com/320244
135    document()->updateRenderTreeIfNeeded();
136    document()->imageUpdated();
137}
138
139void ImageDocumentParser::finish()
140{
141    if (!isStopped() && document()->imageElement() && document()->cachedImage()) {
142        ImageResource* cachedImage = document()->cachedImage();
143        cachedImage->finish();
144        cachedImage->setResponse(document()->frame()->loader().documentLoader()->response());
145
146        // Report the natural image size in the page title, regardless of zoom level.
147        // At a zoom level of 1 the image is guaranteed to have an integer size.
148        IntSize size = flooredIntSize(cachedImage->imageSizeForRenderer(document()->imageElement()->renderer(), 1.0f));
149        if (size.width()) {
150            // Compute the title, we use the decoded filename of the resource, falling
151            // back on the (decoded) hostname if there is no path.
152            String fileName = decodeURLEscapeSequences(document()->url().lastPathComponent());
153            if (fileName.isEmpty())
154                fileName = document()->url().host();
155            document()->setTitle(imageTitle(fileName, size));
156        }
157
158        document()->imageUpdated();
159    }
160
161    document()->finishedParsing();
162}
163
164// --------
165
166ImageDocument::ImageDocument(const DocumentInit& initializer)
167    : HTMLDocument(initializer, ImageDocumentClass)
168    , m_imageElement(nullptr)
169    , m_imageSizeIsKnown(false)
170    , m_didShrinkImage(false)
171    , m_shouldShrinkImage(shouldShrinkToFit())
172{
173    setCompatibilityMode(QuirksMode);
174    lockCompatibilityMode();
175}
176
177PassRefPtrWillBeRawPtr<DocumentParser> ImageDocument::createParser()
178{
179    return ImageDocumentParser::create(this);
180}
181
182void ImageDocument::createDocumentStructure()
183{
184    RefPtrWillBeRawPtr<HTMLHtmlElement> rootElement = HTMLHtmlElement::create(*this);
185    appendChild(rootElement);
186    rootElement->insertedByParser();
187
188    if (frame())
189        frame()->loader().dispatchDocumentElementAvailable();
190
191    RefPtrWillBeRawPtr<HTMLHeadElement> head = HTMLHeadElement::create(*this);
192    RefPtrWillBeRawPtr<HTMLMetaElement> meta = HTMLMetaElement::create(*this);
193    meta->setAttribute(nameAttr, "viewport");
194    meta->setAttribute(contentAttr, "width=device-width, minimum-scale=0.1");
195    head->appendChild(meta);
196
197    RefPtrWillBeRawPtr<HTMLBodyElement> body = HTMLBodyElement::create(*this);
198    body->setAttribute(styleAttr, "margin: 0px;");
199
200    m_imageElement = HTMLImageElement::create(*this);
201    m_imageElement->setAttribute(styleAttr, "-webkit-user-select: none");
202    m_imageElement->setLoadingImageDocument();
203    m_imageElement->setSrc(url().string());
204    body->appendChild(m_imageElement.get());
205
206    if (shouldShrinkToFit()) {
207        // Add event listeners
208        RefPtr<EventListener> listener = ImageEventListener::create(this);
209        if (LocalDOMWindow* domWindow = this->domWindow())
210            domWindow->addEventListener("resize", listener, false);
211        m_imageElement->addEventListener("click", listener.release(), false);
212    }
213
214    rootElement->appendChild(head);
215    rootElement->appendChild(body);
216}
217
218float ImageDocument::scale() const
219{
220    if (!m_imageElement || m_imageElement->document() != this)
221        return 1.0f;
222
223    FrameView* view = frame()->view();
224    if (!view)
225        return 1;
226
227    LayoutSize imageSize = m_imageElement->cachedImage()->imageSizeForRenderer(m_imageElement->renderer(), pageZoomFactor(this));
228    LayoutSize windowSize = LayoutSize(view->width(), view->height());
229
230    float widthScale = windowSize.width().toFloat() / imageSize.width().toFloat();
231    float heightScale = windowSize.height().toFloat() / imageSize.height().toFloat();
232
233    return min(widthScale, heightScale);
234}
235
236void ImageDocument::resizeImageToFit(ScaleType type)
237{
238    if (!m_imageElement || m_imageElement->document() != this || (pageZoomFactor(this) > 1 && type == ScaleOnlyUnzoomedDocument))
239        return;
240
241    LayoutSize imageSize = m_imageElement->cachedImage()->imageSizeForRenderer(m_imageElement->renderer(), pageZoomFactor(this));
242
243    float scale = this->scale();
244    m_imageElement->setWidth(static_cast<int>(imageSize.width() * scale));
245    m_imageElement->setHeight(static_cast<int>(imageSize.height() * scale));
246
247    m_imageElement->setInlineStyleProperty(CSSPropertyCursor, CSSValueZoomIn);
248}
249
250void ImageDocument::imageClicked(int x, int y)
251{
252    if (!m_imageSizeIsKnown || imageFitsInWindow())
253        return;
254
255    m_shouldShrinkImage = !m_shouldShrinkImage;
256
257    if (m_shouldShrinkImage)
258        windowSizeChanged(ScaleZoomedDocument);
259    else {
260        restoreImageSize(ScaleZoomedDocument);
261
262        updateLayout();
263
264        float scale = this->scale();
265
266        int scrollX = static_cast<int>(x / scale - (float)frame()->view()->width() / 2);
267        int scrollY = static_cast<int>(y / scale - (float)frame()->view()->height() / 2);
268
269        frame()->view()->setScrollPosition(IntPoint(scrollX, scrollY));
270    }
271}
272
273void ImageDocument::imageUpdated()
274{
275    ASSERT(m_imageElement);
276
277    if (m_imageSizeIsKnown)
278        return;
279
280    if (m_imageElement->cachedImage()->imageSizeForRenderer(m_imageElement->renderer(), pageZoomFactor(this)).isEmpty())
281        return;
282
283    m_imageSizeIsKnown = true;
284
285    if (shouldShrinkToFit()) {
286        // Force resizing of the image
287        windowSizeChanged(ScaleOnlyUnzoomedDocument);
288    }
289}
290
291void ImageDocument::restoreImageSize(ScaleType type)
292{
293    if (!m_imageElement || !m_imageSizeIsKnown || m_imageElement->document() != this || (pageZoomFactor(this) < 1 && type == ScaleOnlyUnzoomedDocument))
294        return;
295
296    LayoutSize imageSize = m_imageElement->cachedImage()->imageSizeForRenderer(m_imageElement->renderer(), 1.0f);
297    m_imageElement->setWidth(imageSize.width());
298    m_imageElement->setHeight(imageSize.height());
299
300    if (imageFitsInWindow())
301        m_imageElement->removeInlineStyleProperty(CSSPropertyCursor);
302    else
303        m_imageElement->setInlineStyleProperty(CSSPropertyCursor, CSSValueZoomOut);
304
305    m_didShrinkImage = false;
306}
307
308bool ImageDocument::imageFitsInWindow() const
309{
310    if (!m_imageElement || m_imageElement->document() != this)
311        return true;
312
313    FrameView* view = frame()->view();
314    if (!view)
315        return true;
316
317    LayoutSize imageSize = m_imageElement->cachedImage()->imageSizeForRenderer(m_imageElement->renderer(), pageZoomFactor(this));
318    LayoutSize windowSize = LayoutSize(view->width(), view->height());
319
320    return imageSize.width() <= windowSize.width() && imageSize.height() <= windowSize.height();
321}
322
323void ImageDocument::windowSizeChanged(ScaleType type)
324{
325    if (!m_imageElement || !m_imageSizeIsKnown || m_imageElement->document() != this)
326        return;
327
328    bool fitsInWindow = imageFitsInWindow();
329
330    // If the image has been explicitly zoomed in, restore the cursor if the image fits
331    // and set it to a zoom out cursor if the image doesn't fit
332    if (!m_shouldShrinkImage) {
333        if (fitsInWindow)
334            m_imageElement->removeInlineStyleProperty(CSSPropertyCursor);
335        else
336            m_imageElement->setInlineStyleProperty(CSSPropertyCursor, CSSValueZoomOut);
337        return;
338    }
339
340    if (m_didShrinkImage) {
341        // If the window has been resized so that the image fits, restore the image size
342        // otherwise update the restored image size.
343        if (fitsInWindow)
344            restoreImageSize(type);
345        else
346            resizeImageToFit(type);
347    } else {
348        // If the image isn't resized but needs to be, then resize it.
349        if (!fitsInWindow) {
350            resizeImageToFit(type);
351            m_didShrinkImage = true;
352        }
353    }
354}
355
356ImageResource* ImageDocument::cachedImage()
357{
358    if (!m_imageElement)
359        createDocumentStructure();
360
361    return m_imageElement->cachedImage();
362}
363
364bool ImageDocument::shouldShrinkToFit() const
365{
366    return frame()->settings()->shrinksStandaloneImagesToFit() && frame()->isMainFrame();
367}
368
369#if !ENABLE(OILPAN)
370void ImageDocument::dispose()
371{
372    m_imageElement = nullptr;
373    HTMLDocument::dispose();
374}
375#endif
376
377void ImageDocument::trace(Visitor* visitor)
378{
379    visitor->trace(m_imageElement);
380    HTMLDocument::trace(visitor);
381}
382
383// --------
384
385void ImageEventListener::handleEvent(ExecutionContext*, Event* event)
386{
387    if (event->type() == EventTypeNames::resize)
388        m_doc->windowSizeChanged(ImageDocument::ScaleOnlyUnzoomedDocument);
389    else if (event->type() == EventTypeNames::click && event->isMouseEvent()) {
390        MouseEvent* mouseEvent = toMouseEvent(event);
391        m_doc->imageClicked(mouseEvent->x(), mouseEvent->y());
392    }
393}
394
395bool ImageEventListener::operator==(const EventListener& listener)
396{
397    if (const ImageEventListener* imageEventListener = ImageEventListener::cast(&listener))
398        return m_doc == imageEventListener->m_doc;
399    return false;
400}
401
402}
403