1/*
2 * Copyright (C) 2006-2019 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 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 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 "ImageDocument.h"
27
28#include "CachedImage.h"
29#include "Chrome.h"
30#include "ChromeClient.h"
31#include "CustomHeaderFields.h"
32#include "DOMWindow.h"
33#include "DocumentLoader.h"
34#include "EventListener.h"
35#include "EventNames.h"
36#include "Frame.h"
37#include "FrameLoader.h"
38#include "FrameLoaderClient.h"
39#include "FrameView.h"
40#include "HTMLBodyElement.h"
41#include "HTMLHeadElement.h"
42#include "HTMLHtmlElement.h"
43#include "HTMLImageElement.h"
44#include "HTMLNames.h"
45#include "LocalizedStrings.h"
46#include "MIMETypeRegistry.h"
47#include "MouseEvent.h"
48#include "Page.h"
49#include "RawDataDocumentParser.h"
50#include "RenderElement.h"
51#include "Settings.h"
52#include <wtf/IsoMallocInlines.h>
53#include <wtf/text/StringConcatenateNumbers.h>
54
55namespace WebCore {
56
57WTF_MAKE_ISO_ALLOCATED_IMPL(ImageDocument);
58
59using namespace HTMLNames;
60
61#if !PLATFORM(IOS_FAMILY)
62class ImageEventListener final : public EventListener {
63public:
64 static Ref<ImageEventListener> create(ImageDocument& document) { return adoptRef(*new ImageEventListener(document)); }
65
66private:
67 ImageEventListener(ImageDocument& document)
68 : EventListener(ImageEventListenerType)
69 , m_document(document)
70 {
71 }
72
73 bool operator==(const EventListener&) const override;
74 void handleEvent(ScriptExecutionContext&, Event&) override;
75
76 ImageDocument& m_document;
77};
78#endif
79
80class ImageDocumentParser final : public RawDataDocumentParser {
81public:
82 static Ref<ImageDocumentParser> create(ImageDocument& document)
83 {
84 return adoptRef(*new ImageDocumentParser(document));
85 }
86
87private:
88 ImageDocumentParser(ImageDocument& document)
89 : RawDataDocumentParser(document)
90 {
91 }
92
93 ImageDocument& document() const;
94
95 void appendBytes(DocumentWriter&, const char*, size_t) override;
96 void finish() override;
97};
98
99class ImageDocumentElement final : public HTMLImageElement {
100 WTF_MAKE_ISO_ALLOCATED_INLINE(ImageDocumentElement);
101public:
102 static Ref<ImageDocumentElement> create(ImageDocument&);
103
104private:
105 ImageDocumentElement(ImageDocument& document)
106 : HTMLImageElement(imgTag, document)
107 , m_imageDocument(&document)
108 {
109 }
110
111 virtual ~ImageDocumentElement();
112 void didMoveToNewDocument(Document& oldDocument, Document& newDocument) override;
113
114 ImageDocument* m_imageDocument;
115};
116
117inline Ref<ImageDocumentElement> ImageDocumentElement::create(ImageDocument& document)
118{
119 return adoptRef(*new ImageDocumentElement(document));
120}
121
122// --------
123
124HTMLImageElement* ImageDocument::imageElement() const
125{
126 return m_imageElement;
127}
128
129LayoutSize ImageDocument::imageSize()
130{
131 ASSERT(m_imageElement);
132 updateStyleIfNeeded();
133 return m_imageElement->cachedImage()->imageSizeForRenderer(m_imageElement->renderer(), frame() ? frame()->pageZoomFactor() : 1);
134}
135
136void ImageDocument::updateDuringParsing()
137{
138 if (!settings().areImagesEnabled())
139 return;
140
141 if (!m_imageElement)
142 createDocumentStructure();
143
144 if (RefPtr<SharedBuffer> buffer = loader()->mainResourceData())
145 m_imageElement->cachedImage()->updateBuffer(*buffer);
146
147 imageUpdated();
148}
149
150void ImageDocument::finishedParsing()
151{
152 if (!parser()->isStopped() && m_imageElement) {
153 CachedImage& cachedImage = *m_imageElement->cachedImage();
154 RefPtr<SharedBuffer> data = loader()->mainResourceData();
155
156 // If this is a multipart image, make a copy of the current part, since the resource data
157 // will be overwritten by the next part.
158 if (data && loader()->isLoadingMultipartContent())
159 data = data->copy();
160
161 cachedImage.finishLoading(data.get());
162 cachedImage.finish();
163
164 // Report the natural image size in the page title, regardless of zoom level.
165 // At a zoom level of 1 the image is guaranteed to have an integer size.
166 updateStyleIfNeeded();
167 IntSize size = flooredIntSize(cachedImage.imageSizeForRenderer(m_imageElement->renderer(), 1));
168 if (size.width()) {
169 // Compute the title. We use the decoded filename of the resource, falling
170 // back on the hostname if there is no path.
171 String name = decodeURLEscapeSequences(url().lastPathComponent());
172 if (name.isEmpty())
173 name = url().host().toString();
174 setTitle(imageTitle(name, size));
175 }
176
177 imageUpdated();
178 }
179
180 HTMLDocument::finishedParsing();
181}
182
183inline ImageDocument& ImageDocumentParser::document() const
184{
185 // Only used during parsing, so document is guaranteed to be non-null.
186 ASSERT(RawDataDocumentParser::document());
187 return downcast<ImageDocument>(*RawDataDocumentParser::document());
188}
189
190void ImageDocumentParser::appendBytes(DocumentWriter&, const char*, size_t)
191{
192 document().updateDuringParsing();
193}
194
195void ImageDocumentParser::finish()
196{
197 document().finishedParsing();
198}
199
200ImageDocument::ImageDocument(Frame& frame, const URL& url)
201 : HTMLDocument(&frame, url, ImageDocumentClass)
202 , m_imageElement(nullptr)
203 , m_imageSizeIsKnown(false)
204#if !PLATFORM(IOS_FAMILY)
205 , m_didShrinkImage(false)
206#endif
207 , m_shouldShrinkImage(frame.settings().shrinksStandaloneImagesToFit() && frame.isMainFrame())
208{
209 setCompatibilityMode(DocumentCompatibilityMode::QuirksMode);
210 lockCompatibilityMode();
211}
212
213Ref<DocumentParser> ImageDocument::createParser()
214{
215 return ImageDocumentParser::create(*this);
216}
217
218void ImageDocument::createDocumentStructure()
219{
220 auto rootElement = HTMLHtmlElement::create(*this);
221 appendChild(rootElement);
222 rootElement->insertedByParser();
223
224 frame()->injectUserScripts(InjectAtDocumentStart);
225
226 // We need a <head> so that the call to setTitle() later on actually has an <head> to append to <title> to.
227 auto head = HTMLHeadElement::create(*this);
228 rootElement->appendChild(head);
229
230 auto body = HTMLBodyElement::create(*this);
231 body->setAttribute(styleAttr, "margin: 0px");
232 if (MIMETypeRegistry::isPDFMIMEType(document().loader()->responseMIMEType()))
233 body->setInlineStyleProperty(CSSPropertyBackgroundColor, "white");
234 rootElement->appendChild(body);
235
236 auto imageElement = ImageDocumentElement::create(*this);
237 if (m_shouldShrinkImage)
238 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);");
239 else
240 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);");
241 imageElement->setLoadManually(true);
242 imageElement->setSrc(url().string());
243 imageElement->cachedImage()->setResponse(loader()->response());
244 body->appendChild(imageElement);
245
246 if (m_shouldShrinkImage) {
247#if PLATFORM(IOS_FAMILY)
248 // Set the viewport to be in device pixels (rather than the default of 980).
249 processViewport("width=device-width,viewport-fit=cover"_s, ViewportArguments::ImageDocument);
250#else
251 auto listener = ImageEventListener::create(*this);
252 if (RefPtr<DOMWindow> window = this->domWindow())
253 window->addEventListener("resize", listener.copyRef(), false);
254 imageElement->addEventListener("click", WTFMove(listener), false);
255#endif
256 }
257
258 m_imageElement = imageElement.ptr();
259}
260
261void ImageDocument::imageUpdated()
262{
263 ASSERT(m_imageElement);
264
265 if (m_imageSizeIsKnown)
266 return;
267
268 LayoutSize imageSize = this->imageSize();
269 if (imageSize.isEmpty())
270 return;
271
272 m_imageSizeIsKnown = true;
273
274 if (m_shouldShrinkImage) {
275#if PLATFORM(IOS_FAMILY)
276 FloatSize screenSize = page()->chrome().screenSize();
277 if (imageSize.width() > screenSize.width())
278 processViewport(makeString("width=", imageSize.width().toInt(), ",viewport-fit=cover"), ViewportArguments::ImageDocument);
279
280 if (page())
281 page()->chrome().client().imageOrMediaDocumentSizeChanged(IntSize(imageSize.width(), imageSize.height()));
282#else
283 // Call windowSizeChanged for its side effect of sizing the image.
284 windowSizeChanged();
285#endif
286 }
287}
288
289#if !PLATFORM(IOS_FAMILY)
290float ImageDocument::scale()
291{
292 if (!m_imageElement)
293 return 1;
294
295 RefPtr<FrameView> view = this->view();
296 if (!view)
297 return 1;
298
299 LayoutSize imageSize = this->imageSize();
300
301 IntSize viewportSize = view->visibleSize();
302 float widthScale = viewportSize.width() / imageSize.width().toFloat();
303 float heightScale = viewportSize.height() / imageSize.height().toFloat();
304
305 return std::min(widthScale, heightScale);
306}
307
308void ImageDocument::resizeImageToFit()
309{
310 if (!m_imageElement)
311 return;
312
313 LayoutSize imageSize = this->imageSize();
314
315 float scale = this->scale();
316 m_imageElement->setWidth(static_cast<int>(imageSize.width() * scale));
317 m_imageElement->setHeight(static_cast<int>(imageSize.height() * scale));
318
319 m_imageElement->setInlineStyleProperty(CSSPropertyCursor, CSSValueZoomIn);
320}
321
322void ImageDocument::restoreImageSize()
323{
324 if (!m_imageElement || !m_imageSizeIsKnown)
325 return;
326
327 LayoutSize imageSize = this->imageSize();
328 m_imageElement->setWidth(imageSize.width().toUnsigned());
329 m_imageElement->setHeight(imageSize.height().toUnsigned());
330
331 if (imageFitsInWindow())
332 m_imageElement->removeInlineStyleProperty(CSSPropertyCursor);
333 else
334 m_imageElement->setInlineStyleProperty(CSSPropertyCursor, CSSValueZoomOut);
335
336 m_didShrinkImage = false;
337}
338
339bool ImageDocument::imageFitsInWindow()
340{
341 if (!m_imageElement)
342 return true;
343
344 RefPtr<FrameView> view = this->view();
345 if (!view)
346 return true;
347
348 LayoutSize imageSize = this->imageSize();
349 IntSize viewportSize = view->visibleSize();
350 return imageSize.width() <= viewportSize.width() && imageSize.height() <= viewportSize.height();
351}
352
353
354void ImageDocument::windowSizeChanged()
355{
356 if (!m_imageElement || !m_imageSizeIsKnown)
357 return;
358
359 bool fitsInWindow = imageFitsInWindow();
360
361 // If the image has been explicitly zoomed in, restore the cursor if the image fits
362 // and set it to a zoom out cursor if the image doesn't fit
363 if (!m_shouldShrinkImage) {
364 if (fitsInWindow)
365 m_imageElement->removeInlineStyleProperty(CSSPropertyCursor);
366 else
367 m_imageElement->setInlineStyleProperty(CSSPropertyCursor, CSSValueZoomOut);
368 return;
369 }
370
371 if (m_didShrinkImage) {
372 // If the window has been resized so that the image fits, restore the image size,
373 // otherwise update the restored image size.
374 if (fitsInWindow)
375 restoreImageSize();
376 else
377 resizeImageToFit();
378 } else {
379 // If the image isn't resized but needs to be, then resize it.
380 if (!fitsInWindow) {
381 resizeImageToFit();
382 m_didShrinkImage = true;
383 }
384 }
385}
386
387void ImageDocument::imageClicked(int x, int y)
388{
389 if (!m_imageSizeIsKnown || imageFitsInWindow())
390 return;
391
392 m_shouldShrinkImage = !m_shouldShrinkImage;
393
394 if (m_shouldShrinkImage) {
395 // Call windowSizeChanged for its side effect of sizing the image.
396 windowSizeChanged();
397 } else {
398 restoreImageSize();
399
400 updateLayout();
401
402 if (!view())
403 return;
404
405 float scale = this->scale();
406
407 IntSize viewportSize = view()->visibleSize();
408 int scrollX = static_cast<int>(x / scale - viewportSize.width() / 2.0f);
409 int scrollY = static_cast<int>(y / scale - viewportSize.height() / 2.0f);
410
411 view()->setScrollPosition(IntPoint(scrollX, scrollY));
412 }
413}
414
415void ImageEventListener::handleEvent(ScriptExecutionContext&, Event& event)
416{
417 if (event.type() == eventNames().resizeEvent)
418 m_document.windowSizeChanged();
419 else if (event.type() == eventNames().clickEvent && is<MouseEvent>(event)) {
420 MouseEvent& mouseEvent = downcast<MouseEvent>(event);
421 m_document.imageClicked(mouseEvent.offsetX(), mouseEvent.offsetY());
422 }
423}
424
425bool ImageEventListener::operator==(const EventListener& other) const
426{
427 // All ImageEventListener objects compare as equal; OK since there is only one per document.
428 return other.type() == ImageEventListenerType;
429}
430#endif
431
432// --------
433
434ImageDocumentElement::~ImageDocumentElement()
435{
436 if (m_imageDocument)
437 m_imageDocument->disconnectImageElement();
438}
439
440void ImageDocumentElement::didMoveToNewDocument(Document& oldDocument, Document& newDocument)
441{
442 if (m_imageDocument) {
443 m_imageDocument->disconnectImageElement();
444 m_imageDocument = nullptr;
445 }
446 HTMLImageElement::didMoveToNewDocument(oldDocument, newDocument);
447}
448
449}
450