]>
Commit | Line | Data |
---|---|---|
d4949327 NL |
1 | <?php\r |
2 | \r | |
3 | /**\r | |
4 | * Class for converting between different unit-lengths as specified by\r | |
5 | * CSS.\r | |
6 | */\r | |
7 | class HTMLPurifier_UnitConverter\r | |
8 | {\r | |
9 | \r | |
10 | const ENGLISH = 1;\r | |
11 | const METRIC = 2;\r | |
12 | const DIGITAL = 3;\r | |
13 | \r | |
14 | /**\r | |
15 | * Units information array. Units are grouped into measuring systems\r | |
16 | * (English, Metric), and are assigned an integer representing\r | |
17 | * the conversion factor between that unit and the smallest unit in\r | |
18 | * the system. Numeric indexes are actually magical constants that\r | |
19 | * encode conversion data from one system to the next, with a O(n^2)\r | |
20 | * constraint on memory (this is generally not a problem, since\r | |
21 | * the number of measuring systems is small.)\r | |
22 | */\r | |
23 | protected static $units = array(\r | |
24 | self::ENGLISH => array(\r | |
25 | 'px' => 3, // This is as per CSS 2.1 and Firefox. Your mileage may vary\r | |
26 | 'pt' => 4,\r | |
27 | 'pc' => 48,\r | |
28 | 'in' => 288,\r | |
29 | self::METRIC => array('pt', '0.352777778', 'mm'),\r | |
30 | ),\r | |
31 | self::METRIC => array(\r | |
32 | 'mm' => 1,\r | |
33 | 'cm' => 10,\r | |
34 | self::ENGLISH => array('mm', '2.83464567', 'pt'),\r | |
35 | ),\r | |
36 | );\r | |
37 | \r | |
38 | /**\r | |
39 | * Minimum bcmath precision for output.\r | |
40 | * @type int\r | |
41 | */\r | |
42 | protected $outputPrecision;\r | |
43 | \r | |
44 | /**\r | |
45 | * Bcmath precision for internal calculations.\r | |
46 | * @type int\r | |
47 | */\r | |
48 | protected $internalPrecision;\r | |
49 | \r | |
50 | /**\r | |
51 | * Whether or not BCMath is available.\r | |
52 | * @type bool\r | |
53 | */\r | |
54 | private $bcmath;\r | |
55 | \r | |
56 | public function __construct($output_precision = 4, $internal_precision = 10, $force_no_bcmath = false)\r | |
57 | {\r | |
58 | $this->outputPrecision = $output_precision;\r | |
59 | $this->internalPrecision = $internal_precision;\r | |
60 | $this->bcmath = !$force_no_bcmath && function_exists('bcmul');\r | |
61 | }\r | |
62 | \r | |
63 | /**\r | |
64 | * Converts a length object of one unit into another unit.\r | |
65 | * @param HTMLPurifier_Length $length\r | |
66 | * Instance of HTMLPurifier_Length to convert. You must validate()\r | |
67 | * it before passing it here!\r | |
68 | * @param string $to_unit\r | |
69 | * Unit to convert to.\r | |
70 | * @return HTMLPurifier_Length|bool\r | |
71 | * @note\r | |
72 | * About precision: This conversion function pays very special\r | |
73 | * attention to the incoming precision of values and attempts\r | |
74 | * to maintain a number of significant figure. Results are\r | |
75 | * fairly accurate up to nine digits. Some caveats:\r | |
76 | * - If a number is zero-padded as a result of this significant\r | |
77 | * figure tracking, the zeroes will be eliminated.\r | |
78 | * - If a number contains less than four sigfigs ($outputPrecision)\r | |
79 | * and this causes some decimals to be excluded, those\r | |
80 | * decimals will be added on.\r | |
81 | */\r | |
82 | public function convert($length, $to_unit)\r | |
83 | {\r | |
84 | if (!$length->isValid()) {\r | |
85 | return false;\r | |
86 | }\r | |
87 | \r | |
88 | $n = $length->getN();\r | |
89 | $unit = $length->getUnit();\r | |
90 | \r | |
91 | if ($n === '0' || $unit === false) {\r | |
92 | return new HTMLPurifier_Length('0', false);\r | |
93 | }\r | |
94 | \r | |
95 | $state = $dest_state = false;\r | |
96 | foreach (self::$units as $k => $x) {\r | |
97 | if (isset($x[$unit])) {\r | |
98 | $state = $k;\r | |
99 | }\r | |
100 | if (isset($x[$to_unit])) {\r | |
101 | $dest_state = $k;\r | |
102 | }\r | |
103 | }\r | |
104 | if (!$state || !$dest_state) {\r | |
105 | return false;\r | |
106 | }\r | |
107 | \r | |
108 | // Some calculations about the initial precision of the number;\r | |
109 | // this will be useful when we need to do final rounding.\r | |
110 | $sigfigs = $this->getSigFigs($n);\r | |
111 | if ($sigfigs < $this->outputPrecision) {\r | |
112 | $sigfigs = $this->outputPrecision;\r | |
113 | }\r | |
114 | \r | |
115 | // BCMath's internal precision deals only with decimals. Use\r | |
116 | // our default if the initial number has no decimals, or increase\r | |
117 | // it by how ever many decimals, thus, the number of guard digits\r | |
118 | // will always be greater than or equal to internalPrecision.\r | |
119 | $log = (int)floor(log(abs($n), 10));\r | |
120 | $cp = ($log < 0) ? $this->internalPrecision - $log : $this->internalPrecision; // internal precision\r | |
121 | \r | |
122 | for ($i = 0; $i < 2; $i++) {\r | |
123 | \r | |
124 | // Determine what unit IN THIS SYSTEM we need to convert to\r | |
125 | if ($dest_state === $state) {\r | |
126 | // Simple conversion\r | |
127 | $dest_unit = $to_unit;\r | |
128 | } else {\r | |
129 | // Convert to the smallest unit, pending a system shift\r | |
130 | $dest_unit = self::$units[$state][$dest_state][0];\r | |
131 | }\r | |
132 | \r | |
133 | // Do the conversion if necessary\r | |
134 | if ($dest_unit !== $unit) {\r | |
135 | $factor = $this->div(self::$units[$state][$unit], self::$units[$state][$dest_unit], $cp);\r | |
136 | $n = $this->mul($n, $factor, $cp);\r | |
137 | $unit = $dest_unit;\r | |
138 | }\r | |
139 | \r | |
140 | // Output was zero, so bail out early. Shouldn't ever happen.\r | |
141 | if ($n === '') {\r | |
142 | $n = '0';\r | |
143 | $unit = $to_unit;\r | |
144 | break;\r | |
145 | }\r | |
146 | \r | |
147 | // It was a simple conversion, so bail out\r | |
148 | if ($dest_state === $state) {\r | |
149 | break;\r | |
150 | }\r | |
151 | \r | |
152 | if ($i !== 0) {\r | |
153 | // Conversion failed! Apparently, the system we forwarded\r | |
154 | // to didn't have this unit. This should never happen!\r | |
155 | return false;\r | |
156 | }\r | |
157 | \r | |
158 | // Pre-condition: $i == 0\r | |
159 | \r | |
160 | // Perform conversion to next system of units\r | |
161 | $n = $this->mul($n, self::$units[$state][$dest_state][1], $cp);\r | |
162 | $unit = self::$units[$state][$dest_state][2];\r | |
163 | $state = $dest_state;\r | |
164 | \r | |
165 | // One more loop around to convert the unit in the new system.\r | |
166 | \r | |
167 | }\r | |
168 | \r | |
169 | // Post-condition: $unit == $to_unit\r | |
170 | if ($unit !== $to_unit) {\r | |
171 | return false;\r | |
172 | }\r | |
173 | \r | |
174 | // Useful for debugging:\r | |
175 | //echo "<pre>n";\r | |
176 | //echo "$n\nsigfigs = $sigfigs\nnew_log = $new_log\nlog = $log\nrp = $rp\n</pre>\n";\r | |
177 | \r | |
178 | $n = $this->round($n, $sigfigs);\r | |
179 | if (strpos($n, '.') !== false) {\r | |
180 | $n = rtrim($n, '0');\r | |
181 | }\r | |
182 | $n = rtrim($n, '.');\r | |
183 | \r | |
184 | return new HTMLPurifier_Length($n, $unit);\r | |
185 | }\r | |
186 | \r | |
187 | /**\r | |
188 | * Returns the number of significant figures in a string number.\r | |
189 | * @param string $n Decimal number\r | |
190 | * @return int number of sigfigs\r | |
191 | */\r | |
192 | public function getSigFigs($n)\r | |
193 | {\r | |
194 | $n = ltrim($n, '0+-');\r | |
195 | $dp = strpos($n, '.'); // decimal position\r | |
196 | if ($dp === false) {\r | |
197 | $sigfigs = strlen(rtrim($n, '0'));\r | |
198 | } else {\r | |
199 | $sigfigs = strlen(ltrim($n, '0.')); // eliminate extra decimal character\r | |
200 | if ($dp !== 0) {\r | |
201 | $sigfigs--;\r | |
202 | }\r | |
203 | }\r | |
204 | return $sigfigs;\r | |
205 | }\r | |
206 | \r | |
207 | /**\r | |
208 | * Adds two numbers, using arbitrary precision when available.\r | |
209 | * @param string $s1\r | |
210 | * @param string $s2\r | |
211 | * @param int $scale\r | |
212 | * @return string\r | |
213 | */\r | |
214 | private function add($s1, $s2, $scale)\r | |
215 | {\r | |
216 | if ($this->bcmath) {\r | |
217 | return bcadd($s1, $s2, $scale);\r | |
218 | } else {\r | |
219 | return $this->scale((float)$s1 + (float)$s2, $scale);\r | |
220 | }\r | |
221 | }\r | |
222 | \r | |
223 | /**\r | |
224 | * Multiples two numbers, using arbitrary precision when available.\r | |
225 | * @param string $s1\r | |
226 | * @param string $s2\r | |
227 | * @param int $scale\r | |
228 | * @return string\r | |
229 | */\r | |
230 | private function mul($s1, $s2, $scale)\r | |
231 | {\r | |
232 | if ($this->bcmath) {\r | |
233 | return bcmul($s1, $s2, $scale);\r | |
234 | } else {\r | |
235 | return $this->scale((float)$s1 * (float)$s2, $scale);\r | |
236 | }\r | |
237 | }\r | |
238 | \r | |
239 | /**\r | |
240 | * Divides two numbers, using arbitrary precision when available.\r | |
241 | * @param string $s1\r | |
242 | * @param string $s2\r | |
243 | * @param int $scale\r | |
244 | * @return string\r | |
245 | */\r | |
246 | private function div($s1, $s2, $scale)\r | |
247 | {\r | |
248 | if ($this->bcmath) {\r | |
249 | return bcdiv($s1, $s2, $scale);\r | |
250 | } else {\r | |
251 | return $this->scale((float)$s1 / (float)$s2, $scale);\r | |
252 | }\r | |
253 | }\r | |
254 | \r | |
255 | /**\r | |
256 | * Rounds a number according to the number of sigfigs it should have,\r | |
257 | * using arbitrary precision when available.\r | |
258 | * @param float $n\r | |
259 | * @param int $sigfigs\r | |
260 | * @return string\r | |
261 | */\r | |
262 | private function round($n, $sigfigs)\r | |
263 | {\r | |
264 | $new_log = (int)floor(log(abs($n), 10)); // Number of digits left of decimal - 1\r | |
265 | $rp = $sigfigs - $new_log - 1; // Number of decimal places needed\r | |
266 | $neg = $n < 0 ? '-' : ''; // Negative sign\r | |
267 | if ($this->bcmath) {\r | |
268 | if ($rp >= 0) {\r | |
269 | $n = bcadd($n, $neg . '0.' . str_repeat('0', $rp) . '5', $rp + 1);\r | |
270 | $n = bcdiv($n, '1', $rp);\r | |
271 | } else {\r | |
272 | // This algorithm partially depends on the standardized\r | |
273 | // form of numbers that comes out of bcmath.\r | |
274 | $n = bcadd($n, $neg . '5' . str_repeat('0', $new_log - $sigfigs), 0);\r | |
275 | $n = substr($n, 0, $sigfigs + strlen($neg)) . str_repeat('0', $new_log - $sigfigs + 1);\r | |
276 | }\r | |
277 | return $n;\r | |
278 | } else {\r | |
279 | return $this->scale(round($n, $sigfigs - $new_log - 1), $rp + 1);\r | |
280 | }\r | |
281 | }\r | |
282 | \r | |
283 | /**\r | |
284 | * Scales a float to $scale digits right of decimal point, like BCMath.\r | |
285 | * @param float $r\r | |
286 | * @param int $scale\r | |
287 | * @return string\r | |
288 | */\r | |
289 | private function scale($r, $scale)\r | |
290 | {\r | |
291 | if ($scale < 0) {\r | |
292 | // The f sprintf type doesn't support negative numbers, so we\r | |
293 | // need to cludge things manually. First get the string.\r | |
294 | $r = sprintf('%.0f', (float)$r);\r | |
295 | // Due to floating point precision loss, $r will more than likely\r | |
296 | // look something like 4652999999999.9234. We grab one more digit\r | |
297 | // than we need to precise from $r and then use that to round\r | |
298 | // appropriately.\r | |
299 | $precise = (string)round(substr($r, 0, strlen($r) + $scale), -1);\r | |
300 | // Now we return it, truncating the zero that was rounded off.\r | |
301 | return substr($precise, 0, -1) . str_repeat('0', -$scale + 1);\r | |
302 | }\r | |
303 | return sprintf('%.' . $scale . 'f', (float)$r);\r | |
304 | }\r | |
305 | }\r | |
306 | \r | |
307 | // vim: et sw=4 sts=4\r |