View Javadoc
1   /*
2    * Copyright (c) 2002-2026 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.css;
16  
17  import static org.htmlunit.css.CssStyleSheet.AUTO;
18  
19  import java.util.function.Function;
20  import java.util.function.IntSupplier;
21  import java.util.regex.Pattern;
22  
23  import org.htmlunit.html.DomElement;
24  import org.htmlunit.html.DomNode;
25  import org.htmlunit.html.HtmlCanvas;
26  import org.htmlunit.html.HtmlHtml;
27  import org.htmlunit.util.StringUtils;
28  
29  /**
30   * Utilities for css value handling.
31   *
32   * @author Ronald Brill
33   */
34  public final class CssPixelValueConverter {
35  
36      private static final Pattern TO_FLOAT_PATTERN = Pattern.compile("(\\d+(?:\\.\\d+)?).*");
37  
38      /**
39       * Disallow instantiation of this class.
40       */
41      private CssPixelValueConverter() {
42          // Empty.
43      }
44  
45      /**
46       * Converts the specified length CSS attribute value into an integer number of pixels. If the
47       * specified CSS attribute value is a percentage, this method uses the specified value getters
48       * to recursively retrieve the base (parent) CSS attribute value.
49       * @param element the element for which the CSS attribute value is to be retrieved
50       * @param styleGetter getter function to retrieve the CSS attribute value from ComputedCssStyleDeclaration
51       * @param defaultValue supplier for the default value
52       * @param windowDefaultValue supplier for the default value for the window
53       * @return the integer number of pixels corresponding to the specified length CSS attribute value
54       * @see #pixelValue(String)
55       */
56      public static int pixelValue(
57              final DomElement element,
58              final Function<ComputedCssStyleDeclaration, String> styleGetter,
59              final IntSupplier defaultValue,
60              final IntSupplier windowDefaultValue) {
61          final ComputedCssStyleDeclaration style =
62                  element.getPage().getEnclosingWindow().getComputedStyle(element, null);
63          final String s = styleGetter.apply(style);
64          return pixelValue(element, s, styleGetter, defaultValue, windowDefaultValue, false);
65      }
66  
67      /**
68       * Returns the specified length CSS attribute value as a pixel length value.
69       * If the specified CSS attribute value is a percentage, this method
70       * uses the specified value getters to recursively retrieve the base (parent) CSS attribute value.
71       * @param element the element for which the CSS attribute value is to be retrieved
72       * @param styleGetter getter function to retrieve the CSS attribute value from ComputedCssStyleDeclaration
73       * @param defaultValue supplier for the default value
74       * @param windowDefaultValue supplier for the default value for the window
75       * @return the specified length CSS attribute value as a pixel length value
76       * @see #pixelValue(DomElement, Function, IntSupplier, IntSupplier)
77       */
78      public static String pixelString(
79              final DomElement element,
80              final Function<ComputedCssStyleDeclaration, String> styleGetter,
81              final IntSupplier defaultValue,
82              final IntSupplier windowDefaultValue) {
83          final ComputedCssStyleDeclaration style =
84                  element.getPage().getEnclosingWindow().getComputedStyle(element, null);
85          final String styleValue = styleGetter.apply(style);
86          if (styleValue.endsWith("px")) {
87              return styleValue;
88          }
89          return pixelValue(element, styleValue, styleGetter, defaultValue, windowDefaultValue, false) + "px";
90      }
91  
92      /**
93       * Converts the specified length string value into an integer number of pixels. This method does
94       * <b>NOT</b> handle percentages correctly; use {@link #pixelString(DomElement, Function, IntSupplier, IntSupplier)}
95       * if you need percentage support.
96       * @param value the length string value to convert to an integer number of pixels
97       * @return the integer number of pixels corresponding to the specified length string value
98       * @see <a href="http://htmlhelp.com/reference/css/units.html">CSS Units</a>
99       * @see #pixelString(DomElement, Function, IntSupplier, IntSupplier)
100      */
101     public static int pixelValue(final String value) {
102         float i = StringUtils.toFloat(TO_FLOAT_PATTERN.matcher(value).replaceAll("$1"), 0);
103         if (value.length() < 2) {
104             return Math.round(i);
105         }
106         if (value.endsWith("px")) {
107             return Math.round(i);
108         }
109 
110         if (value.endsWith("em")) {
111             i = i * 16;
112         }
113         else if (value.endsWith("%")) {
114             i = i * 16 / 100;
115         }
116         else if (value.endsWith("ex")) {
117             i = i * 8;
118         }
119         else if (value.endsWith("cm")) {
120             i = i * 38;
121         }
122         else if (value.endsWith("mm")) {
123             i = i * 4;
124         }
125         else if (value.endsWith("pt")) {
126             i = i * 2;
127         }
128         else if (value.endsWith("pc")) {
129             i = i * 24;
130         }
131         else if (value.endsWith("ch")) {
132             i = i * 8;
133         }
134         else if (value.endsWith("vh")
135                 || value.endsWith("vmin")) {
136             // this matches also
137             // "dvh" "dvmin" "lvh" "lvmin" "svh" "svmin"
138             i = i * 6;
139         }
140         else if (value.endsWith("vw")
141                 || value.endsWith("vmax")) {
142             // this matches also
143             // "dvw" "dvmax" "lvw" "lvmax" "svw" "svmax"
144             i = i * 12;
145         }
146         // placed at the end to handle min before
147         else if (value.endsWith("in")) {
148             i = i * 150;
149         }
150         return Math.round(i);
151     }
152 
153     private static int pixelValue(final DomElement element,
154             final String styleValue,
155             final Function<ComputedCssStyleDeclaration, String> styleGetter,
156             final IntSupplier defaultValue,
157             final IntSupplier windowDefaultValue,
158             final boolean percentMode) {
159         if (styleValue.endsWith("%") || (styleValue.isEmpty() && element instanceof HtmlHtml)) {
160             final float i = StringUtils.toFloat(TO_FLOAT_PATTERN.matcher(styleValue).replaceAll("$1"), 100);
161 
162             final DomNode parent = element.getParentNode();
163             final int absoluteValue;
164             if (parent instanceof DomElement parentElem) {
165                 final ComputedCssStyleDeclaration style =
166                         parentElem.getPage().getEnclosingWindow().getComputedStyle(parentElem, null);
167                 final String parentStyleValue = styleGetter.apply(style);
168                 absoluteValue = pixelValue(parentElem, parentStyleValue, styleGetter,
169                                             defaultValue, windowDefaultValue, true);
170             }
171             else {
172                 absoluteValue = windowDefaultValue.getAsInt();
173             }
174             return Math.round((i / 100f) * absoluteValue);
175         }
176         if (AUTO.equals(styleValue)) {
177             return defaultValue.getAsInt();
178         }
179         if (styleValue.isEmpty()) {
180             if (element instanceof HtmlCanvas) {
181                 return windowDefaultValue.getAsInt();
182             }
183 
184             // if the call was originated from a percent value we have to go up until
185             // we can provide some kind of base value for percent calculation
186             if (percentMode) {
187                 final DomNode parent = element.getParentNode();
188                 if (parent == null || parent instanceof HtmlHtml) {
189                     return windowDefaultValue.getAsInt();
190                 }
191                 final DomElement parentElem = (DomElement) parent;
192                 final ComputedCssStyleDeclaration style =
193                         parentElem.getPage().getEnclosingWindow().getComputedStyle(parentElem, null);
194                 final String parentStyleValue = styleGetter.apply(style);
195                 return pixelValue(parentElem, parentStyleValue, styleGetter, defaultValue, windowDefaultValue, true);
196             }
197 
198             return 0;
199         }
200         return pixelValue(styleValue);
201     }
202 }