1/*
2 * Copyright (C) 1999 Lars Knoll (knoll@kde.org)
3 * (C) 1999 Antti Koivisto (koivisto@kde.org)
4 * (C) 2001 Dirk Mueller (mueller@kde.org)
5 * (C) 2006 Alexey Proskuryakov (ap@webkit.org)
6 * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2011 Apple Inc. All rights reserved.
7 * Copyright (C) 2008 Torch Mobile Inc. All rights reserved. (http://www.torchmobile.com/)
8 * Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies)
9 * Copyright (C) 2012 Intel Corporation. All rights reserved.
10 *
11 * This library is free software; you can redistribute it and/or
12 * modify it under the terms of the GNU Library General Public
13 * License as published by the Free Software Foundation; either
14 * version 2 of the License, or (at your option) any later version.
15 *
16 * This library is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 * Library General Public License for more details.
20 *
21 * You should have received a copy of the GNU Library General Public License
22 * along with this library; see the file COPYING.LIB. If not, write to
23 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
24 * Boston, MA 02110-1301, USA.
25 *
26 */
27
28#include "config.h"
29#include "ViewportArguments.h"
30
31#include "Document.h"
32#include "Frame.h"
33#include "IntSize.h"
34#include "RuntimeEnabledFeatures.h"
35#include "ScriptableDocumentParser.h"
36#include "Settings.h"
37#include <wtf/text/TextStream.h>
38
39namespace WebCore {
40
41typedef WTF::Function<void(ViewportErrorCode, StringView, StringView)> InternalViewportErrorHandler;
42
43#if PLATFORM(GTK)
44const float ViewportArguments::deprecatedTargetDPI = 160;
45#endif
46
47static const float& compareIgnoringAuto(const float& value1, const float& value2, const float& (*compare) (const float&, const float&))
48{
49 ASSERT(value1 != ViewportArguments::ValueAuto || value2 != ViewportArguments::ValueAuto);
50
51 if (value1 == ViewportArguments::ValueAuto)
52 return value2;
53
54 if (value2 == ViewportArguments::ValueAuto)
55 return value1;
56
57 return compare(value1, value2);
58}
59
60static inline float clampLengthValue(float value)
61{
62 ASSERT(value != ViewportArguments::ValueDeviceWidth);
63 ASSERT(value != ViewportArguments::ValueDeviceHeight);
64
65 // Limits as defined in the css-device-adapt spec.
66 if (value != ViewportArguments::ValueAuto)
67 return std::min<float>(10000, std::max<float>(value, 1));
68 return value;
69}
70
71static inline float clampScaleValue(float value)
72{
73 ASSERT(value != ViewportArguments::ValueDeviceWidth);
74 ASSERT(value != ViewportArguments::ValueDeviceHeight);
75
76 // Limits as defined in the css-device-adapt spec.
77 if (value != ViewportArguments::ValueAuto)
78 return std::min<float>(10, std::max<float>(value, 0.1));
79 return value;
80}
81
82ViewportAttributes ViewportArguments::resolve(const FloatSize& initialViewportSize, const FloatSize& deviceSize, int defaultWidth) const
83{
84 float resultWidth = width;
85 float resultMaxWidth = maxWidth;
86 float resultMinWidth = minWidth;
87 float resultHeight = height;
88 float resultMinHeight = minHeight;
89 float resultMaxHeight = maxHeight;
90 float resultZoom = zoom;
91 float resultMinZoom = minZoom;
92 float resultMaxZoom = maxZoom;
93
94 switch (int(resultWidth)) {
95 case ViewportArguments::ValueDeviceWidth:
96 resultWidth = deviceSize.width();
97 break;
98 case ViewportArguments::ValueDeviceHeight:
99 resultWidth = deviceSize.height();
100 break;
101 }
102
103 switch (int(resultHeight)) {
104 case ViewportArguments::ValueDeviceWidth:
105 resultHeight = deviceSize.width();
106 break;
107 case ViewportArguments::ValueDeviceHeight:
108 resultHeight = deviceSize.height();
109 break;
110 }
111
112 if (type == ViewportArguments::CSSDeviceAdaptation) {
113 switch (int(resultMinWidth)) {
114 case ViewportArguments::ValueDeviceWidth:
115 resultMinWidth = deviceSize.width();
116 break;
117 case ViewportArguments::ValueDeviceHeight:
118 resultMinWidth = deviceSize.height();
119 break;
120 }
121
122 switch (int(resultMaxWidth)) {
123 case ViewportArguments::ValueDeviceWidth:
124 resultMaxWidth = deviceSize.width();
125 break;
126 case ViewportArguments::ValueDeviceHeight:
127 resultMaxWidth = deviceSize.height();
128 break;
129 }
130
131 switch (int(resultMinHeight)) {
132 case ViewportArguments::ValueDeviceWidth:
133 resultMinHeight = deviceSize.width();
134 break;
135 case ViewportArguments::ValueDeviceHeight:
136 resultMinHeight = deviceSize.height();
137 break;
138 }
139
140 switch (int(resultMaxHeight)) {
141 case ViewportArguments::ValueDeviceWidth:
142 resultMaxHeight = deviceSize.width();
143 break;
144 case ViewportArguments::ValueDeviceHeight:
145 resultMaxHeight = deviceSize.height();
146 break;
147 }
148
149 if (resultMinWidth != ViewportArguments::ValueAuto || resultMaxWidth != ViewportArguments::ValueAuto)
150 resultWidth = compareIgnoringAuto(resultMinWidth, compareIgnoringAuto(resultMaxWidth, deviceSize.width(), std::min), std::max);
151
152 if (resultMinHeight != ViewportArguments::ValueAuto || resultMaxHeight != ViewportArguments::ValueAuto)
153 resultHeight = compareIgnoringAuto(resultMinHeight, compareIgnoringAuto(resultMaxHeight, deviceSize.height(), std::min), std::max);
154
155 if (resultMinZoom != ViewportArguments::ValueAuto && resultMaxZoom != ViewportArguments::ValueAuto)
156 resultMaxZoom = std::max(resultMinZoom, resultMaxZoom);
157
158 if (resultZoom != ViewportArguments::ValueAuto)
159 resultZoom = compareIgnoringAuto(resultMinZoom, compareIgnoringAuto(resultMaxZoom, resultZoom, std::min), std::max);
160
161 if (resultWidth == ViewportArguments::ValueAuto && resultZoom == ViewportArguments::ValueAuto)
162 resultWidth = deviceSize.width();
163
164 if (resultWidth == ViewportArguments::ValueAuto && resultHeight == ViewportArguments::ValueAuto)
165 resultWidth = deviceSize.width() / resultZoom;
166
167 if (resultWidth == ViewportArguments::ValueAuto)
168 resultWidth = resultHeight * deviceSize.width() / deviceSize.height();
169
170 if (resultHeight == ViewportArguments::ValueAuto)
171 resultHeight = resultWidth * deviceSize.height() / deviceSize.width();
172
173 if (resultZoom != ViewportArguments::ValueAuto || resultMaxZoom != ViewportArguments::ValueAuto) {
174 resultWidth = compareIgnoringAuto(resultWidth, deviceSize.width() / compareIgnoringAuto(resultZoom, resultMaxZoom, std::min), std::max);
175 resultHeight = compareIgnoringAuto(resultHeight, deviceSize.height() / compareIgnoringAuto(resultZoom, resultMaxZoom, std::min), std::max);
176 }
177
178 resultWidth = std::max<float>(1, resultWidth);
179 resultHeight = std::max<float>(1, resultHeight);
180 }
181
182 if (type != ViewportArguments::CSSDeviceAdaptation && type != ViewportArguments::Implicit) {
183 // Clamp values to a valid range, but not for @viewport since is
184 // not mandated by the specification.
185 resultWidth = clampLengthValue(resultWidth);
186 resultHeight = clampLengthValue(resultHeight);
187 resultZoom = clampScaleValue(resultZoom);
188 resultMinZoom = clampScaleValue(resultMinZoom);
189 resultMaxZoom = clampScaleValue(resultMaxZoom);
190 }
191
192 ViewportAttributes result;
193
194 // Resolve minimum-scale and maximum-scale values according to spec.
195 if (resultMinZoom == ViewportArguments::ValueAuto)
196 result.minimumScale = float(0.25);
197 else
198 result.minimumScale = resultMinZoom;
199
200 if (resultMaxZoom == ViewportArguments::ValueAuto) {
201 result.maximumScale = 5;
202 result.minimumScale = std::min<float>(5, result.minimumScale);
203 } else
204 result.maximumScale = resultMaxZoom;
205 result.maximumScale = std::max(result.minimumScale, result.maximumScale);
206
207 // Resolve initial-scale value.
208 result.initialScale = resultZoom;
209 if (resultZoom == ViewportArguments::ValueAuto) {
210 result.initialScale = initialViewportSize.width() / defaultWidth;
211 if (resultWidth != ViewportArguments::ValueAuto)
212 result.initialScale = initialViewportSize.width() / resultWidth;
213 if (resultHeight != ViewportArguments::ValueAuto) {
214 // if 'auto', the initial-scale will be negative here and thus ignored.
215 result.initialScale = std::max<float>(result.initialScale, initialViewportSize.height() / resultHeight);
216 }
217 }
218
219 // Constrain initial-scale value to minimum-scale/maximum-scale range.
220 result.initialScale = std::min(result.maximumScale, std::max(result.minimumScale, result.initialScale));
221
222 // Resolve width value.
223 if (resultWidth == ViewportArguments::ValueAuto) {
224 if (resultZoom == ViewportArguments::ValueAuto)
225 resultWidth = defaultWidth;
226 else if (resultHeight != ViewportArguments::ValueAuto)
227 resultWidth = resultHeight * (initialViewportSize.width() / initialViewportSize.height());
228 else
229 resultWidth = initialViewportSize.width() / result.initialScale;
230 }
231
232 // Resolve height value.
233 if (resultHeight == ViewportArguments::ValueAuto)
234 resultHeight = resultWidth * (initialViewportSize.height() / initialViewportSize.width());
235
236 if (type == ViewportArguments::ViewportMeta) {
237 // Extend width and height to fill the visual viewport for the resolved initial-scale.
238 resultWidth = std::max<float>(resultWidth, initialViewportSize.width() / result.initialScale);
239 resultHeight = std::max<float>(resultHeight, initialViewportSize.height() / result.initialScale);
240 }
241
242 result.layoutSize.setWidth(resultWidth);
243 result.layoutSize.setHeight(resultHeight);
244
245 // FIXME: This might affect some ports, but is the right thing to do.
246 // Only set initialScale to a value if it was explicitly set.
247 // if (resultZoom == ViewportArguments::ValueAuto)
248 // result.initialScale = ViewportArguments::ValueAuto;
249
250 result.userScalable = userZoom;
251 result.orientation = orientation;
252 result.shrinkToFit = shrinkToFit;
253 result.viewportFit = viewportFit;
254
255 return result;
256}
257
258static FloatSize convertToUserSpace(const FloatSize& deviceSize, float devicePixelRatio)
259{
260 FloatSize result = deviceSize;
261 if (devicePixelRatio != 1)
262 result.scale(1 / devicePixelRatio);
263 return result;
264}
265
266ViewportAttributes computeViewportAttributes(ViewportArguments args, int desktopWidth, int deviceWidth, int deviceHeight, float devicePixelRatio, IntSize visibleViewport)
267{
268 FloatSize initialViewportSize = convertToUserSpace(visibleViewport, devicePixelRatio);
269 FloatSize deviceSize = convertToUserSpace(FloatSize(deviceWidth, deviceHeight), devicePixelRatio);
270
271 return args.resolve(initialViewportSize, deviceSize, desktopWidth);
272}
273
274float computeMinimumScaleFactorForContentContained(const ViewportAttributes& result, const IntSize& visibleViewport, const IntSize& contentsSize)
275{
276 FloatSize viewportSize(visibleViewport);
277 return std::max<float>(result.minimumScale, std::max(viewportSize.width() / contentsSize.width(), viewportSize.height() / contentsSize.height()));
278}
279
280void restrictMinimumScaleFactorToViewportSize(ViewportAttributes& result, IntSize visibleViewport, float devicePixelRatio)
281{
282 FloatSize viewportSize = convertToUserSpace(visibleViewport, devicePixelRatio);
283
284 result.minimumScale = std::max<float>(result.minimumScale, std::max(viewportSize.width() / result.layoutSize.width(), viewportSize.height() / result.layoutSize.height()));
285}
286
287void restrictScaleFactorToInitialScaleIfNotUserScalable(ViewportAttributes& result)
288{
289 if (!result.userScalable)
290 result.maximumScale = result.minimumScale = result.initialScale;
291}
292
293static float numericPrefix(StringView key, StringView value, const InternalViewportErrorHandler& errorHandler, bool* ok = nullptr)
294{
295 size_t parsedLength;
296 float numericValue;
297 if (value.is8Bit())
298 numericValue = charactersToFloat(value.characters8(), value.length(), parsedLength);
299 else
300 numericValue = charactersToFloat(value.characters16(), value.length(), parsedLength);
301 if (!parsedLength) {
302 errorHandler(UnrecognizedViewportArgumentValueError, value, key);
303 if (ok)
304 *ok = false;
305 return 0;
306 }
307 if (parsedLength < value.length())
308 errorHandler(TruncatedViewportArgumentValueError, value, key);
309 if (ok)
310 *ok = true;
311 return numericValue;
312}
313
314static float findSizeValue(StringView key, StringView value, const InternalViewportErrorHandler& errorHandler, bool* valueWasExplicit = nullptr)
315{
316 // 1) Non-negative number values are translated to px lengths.
317 // 2) Negative number values are translated to auto.
318 // 3) device-width and device-height are used as keywords.
319 // 4) Other keywords and unknown values translate to 0.0.
320
321 if (valueWasExplicit)
322 *valueWasExplicit = true;
323
324 if (equalLettersIgnoringASCIICase(value, "device-width"))
325 return ViewportArguments::ValueDeviceWidth;
326
327 if (equalLettersIgnoringASCIICase(value, "device-height"))
328 return ViewportArguments::ValueDeviceHeight;
329
330 float sizeValue = numericPrefix(key, value, errorHandler);
331
332 if (sizeValue < 0) {
333 if (valueWasExplicit)
334 *valueWasExplicit = false;
335 return ViewportArguments::ValueAuto;
336 }
337
338 return sizeValue;
339}
340
341static float findScaleValue(StringView key, StringView value, const InternalViewportErrorHandler& errorHandler)
342{
343 // 1) Non-negative number values are translated to <number> values.
344 // 2) Negative number values are translated to auto.
345 // 3) yes is translated to 1.0.
346 // 4) device-width and device-height are translated to 10.0.
347 // 5) no and unknown values are translated to 0.0
348
349 if (equalLettersIgnoringASCIICase(value, "yes"))
350 return 1;
351 if (equalLettersIgnoringASCIICase(value, "no"))
352 return 0;
353 if (equalLettersIgnoringASCIICase(value, "device-width"))
354 return 10;
355 if (equalLettersIgnoringASCIICase(value, "device-height"))
356 return 10;
357
358 float numericValue = numericPrefix(key, value, errorHandler);
359
360 if (numericValue < 0)
361 return ViewportArguments::ValueAuto;
362
363 if (numericValue > 10.0)
364 errorHandler(MaximumScaleTooLargeError, { }, { });
365
366 return numericValue;
367}
368
369static bool findBooleanValue(StringView key, StringView value, const InternalViewportErrorHandler& errorHandler)
370{
371 // yes and no are used as keywords.
372 // Numbers >= 1, numbers <= -1, device-width and device-height are mapped to yes.
373 // Numbers in the range <-1, 1>, and unknown values, are mapped to no.
374
375 if (equalLettersIgnoringASCIICase(value, "yes"))
376 return true;
377 if (equalLettersIgnoringASCIICase(value, "no"))
378 return false;
379 if (equalLettersIgnoringASCIICase(value, "device-width"))
380 return true;
381 if (equalLettersIgnoringASCIICase(value, "device-height"))
382 return true;
383 return std::abs(numericPrefix(key, value, errorHandler)) >= 1;
384}
385
386static ViewportFit parseViewportFitValue(StringView key, StringView value, const InternalViewportErrorHandler& errorHandler)
387{
388 if (equalLettersIgnoringASCIICase(value, "auto"))
389 return ViewportFit::Auto;
390 if (equalLettersIgnoringASCIICase(value, "contain"))
391 return ViewportFit::Contain;
392 if (equalLettersIgnoringASCIICase(value, "cover"))
393 return ViewportFit::Cover;
394
395 errorHandler(UnrecognizedViewportArgumentValueError, value, key);
396
397 return ViewportFit::Auto;
398}
399
400static const char* viewportErrorMessageTemplate(ViewportErrorCode errorCode)
401{
402 static const char* const errors[] = {
403 "Viewport argument key \"%replacement1\" not recognized and ignored.",
404 "Viewport argument value \"%replacement1\" for key \"%replacement2\" is invalid, and has been ignored.",
405 "Viewport argument value \"%replacement1\" for key \"%replacement2\" was truncated to its numeric prefix.",
406 "Viewport maximum-scale cannot be larger than 10.0. The maximum-scale will be set to 10.0."
407 };
408
409 return errors[errorCode];
410}
411
412static MessageLevel viewportErrorMessageLevel(ViewportErrorCode errorCode)
413{
414 switch (errorCode) {
415 case TruncatedViewportArgumentValueError:
416 return MessageLevel::Warning;
417 case UnrecognizedViewportArgumentKeyError:
418 case UnrecognizedViewportArgumentValueError:
419 case MaximumScaleTooLargeError:
420 return MessageLevel::Error;
421 }
422
423 ASSERT_NOT_REACHED();
424 return MessageLevel::Error;
425}
426
427static String viewportErrorMessage(ViewportErrorCode errorCode, StringView replacement1, StringView replacement2)
428{
429 String message = viewportErrorMessageTemplate(errorCode);
430 if (!replacement1.isNull())
431 message.replace("%replacement1", replacement1.toStringWithoutCopying());
432 // FIXME: This will do the wrong thing if replacement1 contains the substring "%replacement2".
433 if (!replacement2.isNull())
434 message.replace("%replacement2", replacement2.toStringWithoutCopying());
435
436 if ((errorCode == UnrecognizedViewportArgumentValueError || errorCode == TruncatedViewportArgumentValueError) && replacement1.contains(';'))
437 message.append(" Note that ';' is not a separator in viewport values. The list should be comma-separated.");
438
439 return message;
440}
441
442static void reportViewportWarning(Document& document, ViewportErrorCode errorCode, const String& message)
443{
444 // FIXME: Why is this null check needed? Can't addConsoleMessage deal with this?
445 if (!document.frame())
446 return;
447
448 // FIXME: This message should be moved off the console once a solution to https://bugs.webkit.org/show_bug.cgi?id=103274 exists.
449 document.addConsoleMessage(MessageSource::Rendering, viewportErrorMessageLevel(errorCode), message);
450}
451
452void setViewportFeature(ViewportArguments& arguments, StringView key, StringView value, bool viewportFitEnabled, const ViewportErrorHandler& errorHandler)
453{
454 InternalViewportErrorHandler internalErrorHandler = [&errorHandler] (ViewportErrorCode errorCode, StringView replacement1, StringView replacement2) {
455 errorHandler(errorCode, viewportErrorMessage(errorCode, replacement1, replacement2));
456 };
457
458 if (equalLettersIgnoringASCIICase(key, "width"))
459 arguments.width = findSizeValue(key, value, internalErrorHandler, &arguments.widthWasExplicit);
460 else if (equalLettersIgnoringASCIICase(key, "height"))
461 arguments.height = findSizeValue(key, value, internalErrorHandler);
462 else if (equalLettersIgnoringASCIICase(key, "initial-scale"))
463 arguments.zoom = findScaleValue(key, value, internalErrorHandler);
464 else if (equalLettersIgnoringASCIICase(key, "minimum-scale"))
465 arguments.minZoom = findScaleValue(key, value, internalErrorHandler);
466 else if (equalLettersIgnoringASCIICase(key, "maximum-scale"))
467 arguments.maxZoom = findScaleValue(key, value, internalErrorHandler);
468 else if (equalLettersIgnoringASCIICase(key, "user-scalable"))
469 arguments.userZoom = findBooleanValue(key, value, internalErrorHandler);
470#if PLATFORM(IOS_FAMILY)
471 else if (equalLettersIgnoringASCIICase(key, "minimal-ui")) {
472 // FIXME: Ignore silently for now. This code should eventually be removed
473 // so we start giving the warning in the web inspector as for other unimplemented keys.
474 }
475#endif
476 else if (equalLettersIgnoringASCIICase(key, "shrink-to-fit"))
477 arguments.shrinkToFit = findBooleanValue(key, value, internalErrorHandler);
478 else if (equalLettersIgnoringASCIICase(key, "viewport-fit") && viewportFitEnabled)
479 arguments.viewportFit = parseViewportFitValue(key, value, internalErrorHandler);
480 else
481 internalErrorHandler(UnrecognizedViewportArgumentKeyError, key, { });
482}
483
484void setViewportFeature(ViewportArguments& arguments, Document& document, StringView key, StringView value)
485{
486 setViewportFeature(arguments, key, value, document.settings().viewportFitEnabled(), [&](ViewportErrorCode errorCode, const String& message) {
487 reportViewportWarning(document, errorCode, message);
488 });
489}
490
491TextStream& operator<<(TextStream& ts, const ViewportArguments& viewportArguments)
492{
493 TextStream::IndentScope indentScope(ts);
494
495 ts << "\n" << indent << "(width " << viewportArguments.width << ", minWidth " << viewportArguments.minWidth << ", maxWidth " << viewportArguments.maxWidth << ")";
496 ts << "\n" << indent << "(height " << viewportArguments.height << ", minHeight " << viewportArguments.minHeight << ", maxHeight " << viewportArguments.maxHeight << ")";
497 ts << "\n" << indent << "(zoom " << viewportArguments.zoom << ", minZoom " << viewportArguments.minZoom << ", maxZoom " << viewportArguments.maxZoom << ")";
498
499 return ts;
500}
501
502} // namespace WebCore
503