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 "Chrome.h"
32#include "Console.h"
33#include "DOMWindow.h"
34#include "Document.h"
35#include "Frame.h"
36#include "IntSize.h"
37#include "Page.h"
38#include "ScriptableDocumentParser.h"
39#include <wtf/text/WTFString.h>
40
41using namespace std;
42
43namespace WebCore {
44
45#if PLATFORM(BLACKBERRY) || PLATFORM(EFL) || PLATFORM(GTK) || PLATFORM(QT)
46const float ViewportArguments::deprecatedTargetDPI = 160;
47#endif
48
49static const float& compareIgnoringAuto(const float& value1, const float& value2, const float& (*compare) (const float&, const float&))
50{
51    ASSERT(value1 != ViewportArguments::ValueAuto || value2 != ViewportArguments::ValueAuto);
52
53    if (value1 == ViewportArguments::ValueAuto)
54        return value2;
55
56    if (value2 == ViewportArguments::ValueAuto)
57        return value1;
58
59    return compare(value1, value2);
60}
61
62static inline float clampLengthValue(float value)
63{
64    ASSERT(value != ViewportArguments::ValueDeviceWidth);
65    ASSERT(value != ViewportArguments::ValueDeviceHeight);
66
67    // Limits as defined in the css-device-adapt spec.
68    if (value != ViewportArguments::ValueAuto)
69        return min(float(10000), max(value, float(1)));
70    return value;
71}
72
73static inline float clampScaleValue(float value)
74{
75    ASSERT(value != ViewportArguments::ValueDeviceWidth);
76    ASSERT(value != ViewportArguments::ValueDeviceHeight);
77
78    // Limits as defined in the css-device-adapt spec.
79    if (value != ViewportArguments::ValueAuto)
80        return min(float(10), max(value, float(0.1)));
81    return value;
82}
83
84ViewportAttributes ViewportArguments::resolve(const FloatSize& initialViewportSize, const FloatSize& deviceSize, int defaultWidth) const
85{
86    float resultWidth = width;
87    float resultMaxWidth = maxWidth;
88    float resultMinWidth = minWidth;
89    float resultHeight = height;
90    float resultMinHeight = minHeight;
91    float resultMaxHeight = maxHeight;
92    float resultZoom = zoom;
93    float resultMinZoom = minZoom;
94    float resultMaxZoom = maxZoom;
95    float resultUserZoom = userZoom;
96
97    switch (int(resultWidth)) {
98    case ViewportArguments::ValueDeviceWidth:
99        resultWidth = deviceSize.width();
100        break;
101    case ViewportArguments::ValueDeviceHeight:
102        resultWidth = deviceSize.height();
103        break;
104    }
105
106    switch (int(resultHeight)) {
107    case ViewportArguments::ValueDeviceWidth:
108        resultHeight = deviceSize.width();
109        break;
110    case ViewportArguments::ValueDeviceHeight:
111        resultHeight = deviceSize.height();
112        break;
113    }
114
115    if (type == ViewportArguments::CSSDeviceAdaptation) {
116        switch (int(resultMinWidth)) {
117        case ViewportArguments::ValueDeviceWidth:
118            resultMinWidth = deviceSize.width();
119            break;
120        case ViewportArguments::ValueDeviceHeight:
121            resultMinWidth = deviceSize.height();
122            break;
123        }
124
125        switch (int(resultMaxWidth)) {
126        case ViewportArguments::ValueDeviceWidth:
127            resultMaxWidth = deviceSize.width();
128            break;
129        case ViewportArguments::ValueDeviceHeight:
130            resultMaxWidth = deviceSize.height();
131            break;
132        }
133
134        switch (int(resultMinHeight)) {
135        case ViewportArguments::ValueDeviceWidth:
136            resultMinHeight = deviceSize.width();
137            break;
138        case ViewportArguments::ValueDeviceHeight:
139            resultMinHeight = deviceSize.height();
140            break;
141        }
142
143        switch (int(resultMaxHeight)) {
144        case ViewportArguments::ValueDeviceWidth:
145            resultMaxHeight = deviceSize.width();
146            break;
147        case ViewportArguments::ValueDeviceHeight:
148            resultMaxHeight = deviceSize.height();
149            break;
150        }
151
152        if (resultMinWidth != ViewportArguments::ValueAuto || resultMaxWidth != ViewportArguments::ValueAuto)
153            resultWidth = compareIgnoringAuto(resultMinWidth, compareIgnoringAuto(resultMaxWidth, deviceSize.width(), min), max);
154
155        if (resultMinHeight != ViewportArguments::ValueAuto || resultMaxHeight != ViewportArguments::ValueAuto)
156            resultHeight = compareIgnoringAuto(resultMinHeight, compareIgnoringAuto(resultMaxHeight, deviceSize.height(), min), max);
157
158        if (resultMinZoom != ViewportArguments::ValueAuto && resultMaxZoom != ViewportArguments::ValueAuto)
159            resultMaxZoom = max(resultMinZoom, resultMaxZoom);
160
161        if (resultZoom != ViewportArguments::ValueAuto)
162            resultZoom = compareIgnoringAuto(resultMinZoom, compareIgnoringAuto(resultMaxZoom, resultZoom, min), max);
163
164        if (resultWidth == ViewportArguments::ValueAuto && resultZoom == ViewportArguments::ValueAuto)
165            resultWidth = deviceSize.width();
166
167        if (resultWidth == ViewportArguments::ValueAuto && resultHeight == ViewportArguments::ValueAuto)
168            resultWidth = deviceSize.width() / resultZoom;
169
170        if (resultWidth == ViewportArguments::ValueAuto)
171            resultWidth = resultHeight * deviceSize.width() / deviceSize.height();
172
173        if (resultHeight == ViewportArguments::ValueAuto)
174            resultHeight = resultWidth * deviceSize.height() / deviceSize.width();
175
176        if (resultZoom != ViewportArguments::ValueAuto || resultMaxZoom != ViewportArguments::ValueAuto) {
177            resultWidth = compareIgnoringAuto(resultWidth, deviceSize.width() / compareIgnoringAuto(resultZoom, resultMaxZoom, min), max);
178            resultHeight = compareIgnoringAuto(resultHeight, deviceSize.height() / compareIgnoringAuto(resultZoom, resultMaxZoom, min), max);
179        }
180
181        resultWidth = max<float>(1, resultWidth);
182        resultHeight = max<float>(1, resultHeight);
183    }
184
185    if (type != ViewportArguments::CSSDeviceAdaptation && type != ViewportArguments::Implicit) {
186        // Clamp values to a valid range, but not for @viewport since is
187        // not mandated by the specification.
188        resultWidth = clampLengthValue(resultWidth);
189        resultHeight = clampLengthValue(resultHeight);
190        resultZoom = clampScaleValue(resultZoom);
191        resultMinZoom = clampScaleValue(resultMinZoom);
192        resultMaxZoom = clampScaleValue(resultMaxZoom);
193    }
194
195    ViewportAttributes result;
196
197    // Resolve minimum-scale and maximum-scale values according to spec.
198    if (resultMinZoom == ViewportArguments::ValueAuto)
199        result.minimumScale = float(0.25);
200    else
201        result.minimumScale = resultMinZoom;
202
203    if (resultMaxZoom == ViewportArguments::ValueAuto) {
204        result.maximumScale = float(5.0);
205        result.minimumScale = min(float(5.0), result.minimumScale);
206    } else
207        result.maximumScale = resultMaxZoom;
208    result.maximumScale = max(result.minimumScale, result.maximumScale);
209
210    // Resolve initial-scale value.
211    result.initialScale = resultZoom;
212    if (resultZoom == ViewportArguments::ValueAuto) {
213        result.initialScale = initialViewportSize.width() / defaultWidth;
214        if (resultWidth != ViewportArguments::ValueAuto)
215            result.initialScale = initialViewportSize.width() / resultWidth;
216        if (resultHeight != ViewportArguments::ValueAuto) {
217            // if 'auto', the initial-scale will be negative here and thus ignored.
218            result.initialScale = max<float>(result.initialScale, initialViewportSize.height() / resultHeight);
219        }
220    }
221
222    // Constrain initial-scale value to minimum-scale/maximum-scale range.
223    result.initialScale = min(result.maximumScale, max(result.minimumScale, result.initialScale));
224
225    // Resolve width value.
226    if (resultWidth == ViewportArguments::ValueAuto) {
227        if (resultZoom == ViewportArguments::ValueAuto)
228            resultWidth = defaultWidth;
229        else if (resultHeight != ViewportArguments::ValueAuto)
230            resultWidth = resultHeight * (initialViewportSize.width() / initialViewportSize.height());
231        else
232            resultWidth = initialViewportSize.width() / result.initialScale;
233    }
234
235    // Resolve height value.
236    if (resultHeight == ViewportArguments::ValueAuto)
237        resultHeight = resultWidth * (initialViewportSize.height() / initialViewportSize.width());
238
239    if (type == ViewportArguments::ViewportMeta) {
240        // Extend width and height to fill the visual viewport for the resolved initial-scale.
241        resultWidth = max<float>(resultWidth, initialViewportSize.width() / result.initialScale);
242        resultHeight = max<float>(resultHeight, initialViewportSize.height() / result.initialScale);
243    }
244
245    result.layoutSize.setWidth(resultWidth);
246    result.layoutSize.setHeight(resultHeight);
247
248    // FIXME: This might affect some ports, but is the right thing to do.
249    // Only set initialScale to a value if it was explicitly set.
250    // if (resultZoom == ViewportArguments::ValueAuto)
251    //    result.initialScale = ViewportArguments::ValueAuto;
252
253    result.userScalable = resultUserZoom;
254    result.orientation = orientation;
255
256    return result;
257}
258
259static FloatSize convertToUserSpace(const FloatSize& deviceSize, float devicePixelRatio)
260{
261    FloatSize result = deviceSize;
262    if (devicePixelRatio != 1)
263        result.scale(1 / devicePixelRatio);
264    return result;
265}
266
267ViewportAttributes computeViewportAttributes(ViewportArguments args, int desktopWidth, int deviceWidth, int deviceHeight, float devicePixelRatio, IntSize visibleViewport)
268{
269    FloatSize initialViewportSize = convertToUserSpace(visibleViewport, devicePixelRatio);
270    FloatSize deviceSize = convertToUserSpace(FloatSize(deviceWidth, deviceHeight), devicePixelRatio);
271
272    return args.resolve(initialViewportSize, deviceSize, desktopWidth);
273}
274
275float computeMinimumScaleFactorForContentContained(const ViewportAttributes& result, const IntSize& visibleViewport, const IntSize& contentsSize)
276{
277    FloatSize viewportSize(visibleViewport);
278    return max<float>(result.minimumScale, max(viewportSize.width() / contentsSize.width(), viewportSize.height() / contentsSize.height()));
279}
280
281void restrictMinimumScaleFactorToViewportSize(ViewportAttributes& result, IntSize visibleViewport, float devicePixelRatio)
282{
283    FloatSize viewportSize = convertToUserSpace(visibleViewport, devicePixelRatio);
284
285    result.minimumScale = max<float>(result.minimumScale, max(viewportSize.width() / result.layoutSize.width(), viewportSize.height() / result.layoutSize.height()));
286}
287
288void restrictScaleFactorToInitialScaleIfNotUserScalable(ViewportAttributes& result)
289{
290    if (!result.userScalable)
291        result.maximumScale = result.minimumScale = result.initialScale;
292}
293
294static float numericPrefix(const String& keyString, const String& valueString, Document* document, bool* ok = 0)
295{
296    size_t parsedLength;
297    float value;
298    if (valueString.is8Bit())
299        value = charactersToFloat(valueString.characters8(), valueString.length(), parsedLength);
300    else
301        value = charactersToFloat(valueString.characters16(), valueString.length(), parsedLength);
302    if (!parsedLength) {
303        reportViewportWarning(document, UnrecognizedViewportArgumentValueError, valueString, keyString);
304        if (ok)
305            *ok = false;
306        return 0;
307    }
308    if (parsedLength < valueString.length())
309        reportViewportWarning(document, TruncatedViewportArgumentValueError, valueString, keyString);
310    if (ok)
311        *ok = true;
312    return value;
313}
314
315static float findSizeValue(const String& keyString, const String& valueString, Document* document)
316{
317    // 1) Non-negative number values are translated to px lengths.
318    // 2) Negative number values are translated to auto.
319    // 3) device-width and device-height are used as keywords.
320    // 4) Other keywords and unknown values translate to 0.0.
321
322    if (equalIgnoringCase(valueString, "device-width"))
323        return ViewportArguments::ValueDeviceWidth;
324    if (equalIgnoringCase(valueString, "device-height"))
325        return ViewportArguments::ValueDeviceHeight;
326
327    float value = numericPrefix(keyString, valueString, document);
328
329    if (value < 0)
330        return ViewportArguments::ValueAuto;
331
332    return value;
333}
334
335static float findScaleValue(const String& keyString, const String& valueString, Document* document)
336{
337    // 1) Non-negative number values are translated to <number> values.
338    // 2) Negative number values are translated to auto.
339    // 3) yes is translated to 1.0.
340    // 4) device-width and device-height are translated to 10.0.
341    // 5) no and unknown values are translated to 0.0
342
343    if (equalIgnoringCase(valueString, "yes"))
344        return 1;
345    if (equalIgnoringCase(valueString, "no"))
346        return 0;
347    if (equalIgnoringCase(valueString, "device-width"))
348        return 10;
349    if (equalIgnoringCase(valueString, "device-height"))
350        return 10;
351
352    float value = numericPrefix(keyString, valueString, document);
353
354    if (value < 0)
355        return ViewportArguments::ValueAuto;
356
357    if (value > 10.0)
358        reportViewportWarning(document, MaximumScaleTooLargeError, String(), String());
359
360    return value;
361}
362
363static float findUserScalableValue(const String& keyString, const String& valueString, Document* document)
364{
365    // yes and no are used as keywords.
366    // Numbers >= 1, numbers <= -1, device-width and device-height are mapped to yes.
367    // Numbers in the range <-1, 1>, and unknown values, are mapped to no.
368
369    if (equalIgnoringCase(valueString, "yes"))
370        return 1;
371    if (equalIgnoringCase(valueString, "no"))
372        return 0;
373    if (equalIgnoringCase(valueString, "device-width"))
374        return 1;
375    if (equalIgnoringCase(valueString, "device-height"))
376        return 1;
377
378    float value = numericPrefix(keyString, valueString, document);
379
380    if (fabs(value) < 1)
381        return 0;
382
383    return 1;
384}
385
386void setViewportFeature(const String& keyString, const String& valueString, Document* document, void* data)
387{
388    ViewportArguments* arguments = static_cast<ViewportArguments*>(data);
389
390    if (keyString == "width")
391        arguments->width = findSizeValue(keyString, valueString, document);
392    else if (keyString == "height")
393        arguments->height = findSizeValue(keyString, valueString, document);
394    else if (keyString == "initial-scale")
395        arguments->zoom = findScaleValue(keyString, valueString, document);
396    else if (keyString == "minimum-scale")
397        arguments->minZoom = findScaleValue(keyString, valueString, document);
398    else if (keyString == "maximum-scale")
399        arguments->maxZoom = findScaleValue(keyString, valueString, document);
400    else if (keyString == "user-scalable")
401        arguments->userZoom = findUserScalableValue(keyString, valueString, document);
402    else
403        reportViewportWarning(document, UnrecognizedViewportArgumentKeyError, keyString, String());
404}
405
406static const char* viewportErrorMessageTemplate(ViewportErrorCode errorCode)
407{
408    static const char* const errors[] = {
409        "Viewport argument key \"%replacement1\" not recognized and ignored.",
410        "Viewport argument value \"%replacement1\" for key \"%replacement2\" is invalid, and has been ignored.",
411        "Viewport argument value \"%replacement1\" for key \"%replacement2\" was truncated to its numeric prefix.",
412        "Viewport maximum-scale cannot be larger than 10.0. The maximum-scale will be set to 10.0."
413    };
414
415    return errors[errorCode];
416}
417
418static MessageLevel viewportErrorMessageLevel(ViewportErrorCode errorCode)
419{
420    switch (errorCode) {
421    case TruncatedViewportArgumentValueError:
422        return WarningMessageLevel;
423    case UnrecognizedViewportArgumentKeyError:
424    case UnrecognizedViewportArgumentValueError:
425    case MaximumScaleTooLargeError:
426        return ErrorMessageLevel;
427    }
428
429    ASSERT_NOT_REACHED();
430    return ErrorMessageLevel;
431}
432
433void reportViewportWarning(Document* document, ViewportErrorCode errorCode, const String& replacement1, const String& replacement2)
434{
435    Frame* frame = document->frame();
436    if (!frame)
437        return;
438
439    String message = viewportErrorMessageTemplate(errorCode);
440    if (!replacement1.isNull())
441        message.replace("%replacement1", replacement1);
442    if (!replacement2.isNull())
443        message.replace("%replacement2", replacement2);
444
445    if ((errorCode == UnrecognizedViewportArgumentValueError || errorCode == TruncatedViewportArgumentValueError) && replacement1.find(';') != WTF::notFound)
446        message.append(" Note that ';' is not a separator in viewport values. The list should be comma-separated.");
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(RenderingMessageSource, viewportErrorMessageLevel(errorCode), message);
450}
451
452} // namespace WebCore
453