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