gen-refs.py (16000B)
1 # This Source Code Form is subject to the terms of the Mozilla Public 2 # License, v. 2.0. If a copy of the MPL was not distributed with this 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5 # Generates tables of background images which correspond with border images for 6 # creating reftests. Input is the filename containing input defined below (a subset 7 # of the allowed CSS border properties). An html representation of a table is 8 # output to stdout. 9 # 10 # Usage: python gen-refs.py input_filename 11 # 12 # Input must take the form (order is not important, nothing is optional, distance in order top, right, bottom, left): 13 # width: p; 14 # height: p; 15 # border-width: p; 16 # border-image-source: ...; 17 # border-image-slice: p p p p; 18 # note that actually border-image-slice takes numbers without px, which represent pixels anyway (or at least coords) 19 # border-image-width: np np np np; 20 # border-image-repeat: stretch | repeat | round; 21 # border-image-outset: np np np np; 22 # 23 # where: 24 # p ::= n'px' 25 # np ::= n | p 26 # 27 # Assumes there is no intrinsic size for the border-image-source, so uses 28 # the size of the border image area. 29 30 import sys 31 32 33 class Point: 34 def __init__(self, w=0, h=0): 35 self.x = w 36 self.y = h 37 38 39 class Size: 40 def __init__(self, w=0, h=0): 41 self.width = w 42 self.height = h 43 44 45 class Rect: 46 def __init__(self, x=0, y=0, x2=0, y2=0): 47 self.x = x 48 self.y = y 49 self.x2 = x2 50 self.y2 = y2 51 52 def width(self): 53 return self.x2 - self.x 54 55 def height(self): 56 return self.y2 - self.y 57 58 59 class Props: 60 def __init__(self): 61 self.size = Size() 62 63 64 class np: 65 def __init__(self, n, p): 66 self.n = n 67 self.p = p 68 69 def get_absolute(self, ref): 70 if not self.p == 0: 71 return self.p 72 return self.n * ref 73 74 75 def parse_p(tok): 76 if tok[-2:] == "px": 77 return float(tok[:-2]) 78 print("Whoops, not a pixel value", tok) 79 80 81 def parse_np(tok): 82 if tok[-2:] == "px": 83 return np(0, float(tok[:-2])) 84 return np(float(tok), 0) 85 86 87 def parse(filename): 88 f = open(filename, "r") 89 props = Props() 90 for l in f: 91 l = l.strip() 92 if not l[-1] == ";": 93 continue 94 toks = l[:-1].split() 95 if toks[0] == "border-width:": 96 props.width = parse_p(toks[1]) 97 if toks[0] == "height:": 98 props.size.height = parse_p(toks[1]) 99 if toks[0] == "width:": 100 props.size.width = parse_p(toks[1]) 101 if toks[0] == "border-image-source:": 102 props.source = l[l.find(":") + 1 : l.rfind(";")].strip() 103 if toks[0] == "border-image-repeat:": 104 props.repeat = toks[1] 105 if toks[0] == "border-image-slice:": 106 props.slice = map(parse_p, toks[1:5]) 107 if toks[0] == "border-image-width:": 108 props.image_width = map(parse_np, toks[1:5]) 109 if toks[0] == "border-image-outset:": 110 props.outset = map(parse_np, toks[1:5]) 111 f.close() 112 return props 113 114 115 # the result of normalisation is that all sizes are in pixels and the size, 116 # widths, and outset have been normalised to a size and width - the former is 117 # the element's interior, the latter is the width of the drawn border. 118 def normalise(props): 119 result = Props() 120 result.source = props.source 121 result.repeat = props.repeat 122 result.width = map(lambda x: x.get_absolute(props.width), props.image_width) 123 outsets = map(lambda x: x.get_absolute(props.width), props.outset) 124 result.size.width = props.size.width + 2 * props.width + outsets[1] + outsets[3] 125 result.size.height = props.size.height + 2 * props.width + outsets[0] + outsets[2] 126 result.slice = props.slice 127 for i in [0, 2]: 128 if result.slice[i] > result.size.height: 129 result.slice[i] = result.size.height 130 if result.slice[i + 1] > result.size.width: 131 result.slice[i + 1] = result.size.width 132 133 return result 134 135 136 def check_parse(props): 137 if not hasattr(props, "source"): 138 print("missing border-image-source") 139 return False 140 if not hasattr(props.size, "width"): 141 print("missing width") 142 return False 143 if not hasattr(props.size, "height"): 144 print("missing height") 145 return False 146 if not hasattr(props, "width"): 147 print("missing border-width") 148 return False 149 if not hasattr(props, "image_width"): 150 print("missing border-image-width") 151 return False 152 if not hasattr(props, "slice"): 153 print("missing border-image-slice") 154 return False 155 if not hasattr(props, "repeat") or ( 156 props.repeat not in ["stretch", "repeat", "round"] 157 ): 158 print("missing or incorrect border-image-repeat '" + props.repeat + "'") 159 return False 160 if not hasattr(props, "outset"): 161 print("missing border-image-outset") 162 return False 163 164 return True 165 166 167 def check_normalise(props): 168 if not hasattr(props, "source"): 169 print("missing border-image-source") 170 return False 171 if not hasattr(props.size, "width"): 172 print("missing width") 173 return False 174 if not hasattr(props.size, "height"): 175 print("missing height") 176 return False 177 if not hasattr(props, "slice"): 178 print("missing border-image-slice") 179 return False 180 if not hasattr(props, "repeat") or ( 181 props.repeat not in ["stretch", "repeat", "round"] 182 ): 183 print("missing or incorrect border-image-repeat '" + props.repeat + "'") 184 return False 185 186 return True 187 188 189 class Tile: 190 def __init__(self): 191 self.slice = Rect() 192 self.border_width = Rect() 193 194 195 # throughout, we will use arrays for nine-patches, the indices correspond thusly: 196 # 0 1 2 197 # 3 4 5 198 # 6 7 8 199 200 201 # Compute the source tiles' slice and border-width sizes 202 def make_src_tiles(): 203 tiles = [Tile() for i in range(9)] 204 205 rows = [range(3 * i, 3 * (i + 1)) for i in range(3)] 206 cols = [[i, i + 3, i + 6] for i in range(3)] 207 208 row_limits_slice = [ 209 0, 210 props.slice[3], 211 props.size.width - props.slice[1], 212 props.size.width, 213 ] 214 row_limits_width = [ 215 0, 216 props.width[3], 217 props.size.width - props.width[1], 218 props.size.width, 219 ] 220 for r in range(3): 221 for t in [tiles[i] for i in cols[r]]: 222 t.slice.x = row_limits_slice[r] 223 t.slice.x2 = row_limits_slice[r + 1] 224 t.border_width.x = row_limits_width[r] 225 t.border_width.x2 = row_limits_width[r + 1] 226 227 col_limits_slice = [ 228 0, 229 props.slice[0], 230 props.size.height - props.slice[2], 231 props.size.height, 232 ] 233 col_limits_width = [ 234 0, 235 props.width[0], 236 props.size.height - props.width[2], 237 props.size.height, 238 ] 239 for c in range(3): 240 for t in [tiles[i] for i in rows[c]]: 241 t.slice.y = col_limits_slice[c] 242 t.slice.y2 = col_limits_slice[c + 1] 243 t.border_width.y = col_limits_width[c] 244 t.border_width.y2 = col_limits_width[c + 1] 245 246 return tiles 247 248 249 def compute(props): 250 tiles = make_src_tiles() 251 252 # corners scale easy 253 for t in [tiles[i] for i in [0, 2, 6, 8]]: 254 t.scale = Point( 255 t.border_width.width() / t.slice.width(), 256 t.border_width.height() / t.slice.height(), 257 ) 258 # edges are by their secondary dimension 259 for t in [tiles[i] for i in [1, 7]]: 260 t.scale = Point( 261 t.border_width.height() / t.slice.height(), 262 t.border_width.height() / t.slice.height(), 263 ) 264 for t in [tiles[i] for i in [3, 5]]: 265 t.scale = Point( 266 t.border_width.width() / t.slice.width(), 267 t.border_width.width() / t.slice.width(), 268 ) 269 # the middle is scaled by the factors for the top and left edges 270 tiles[4].scale = Point(tiles[1].scale.x, tiles[3].scale.y) 271 272 # the size of a source tile for the middle section 273 src_tile_size = Size( 274 tiles[4].slice.width() * tiles[4].scale.x, 275 tiles[4].slice.height() * tiles[4].scale.y, 276 ) 277 278 # the size of a single destination tile in the central part 279 dest_tile_size = Size() 280 if props.repeat == "stretch": 281 dest_tile_size.width = tiles[4].border_width.width() 282 dest_tile_size.height = tiles[4].border_width.height() 283 for t in [tiles[i] for i in [1, 7]]: 284 t.scale.x = t.border_width.width() / t.slice.width() 285 for t in [tiles[i] for i in [3, 5]]: 286 t.scale.y = t.border_width.height() / t.slice.height() 287 elif props.repeat == "repeat": 288 dest_tile_size = src_tile_size 289 elif props.repeat == "round": 290 dest_tile_size.width = tiles[4].border_width.width() / math.ceil( 291 tiles[4].border_width.width() / src_tile_size.width 292 ) 293 dest_tile_size.height = tiles[4].border_width.height() / math.ceil( 294 tiles[4].border_width.height() / src_tile_size.height 295 ) 296 for t in [tiles[i] for i in [1, 4, 7]]: 297 t.scale.x = dest_tile_size.width / t.slice.width() 298 for t in [tiles[i] for i in [3, 4, 5]]: 299 t.scale.y = dest_tile_size.height / t.slice.height() 300 else: 301 print("Whoops, invalid border-image-repeat value") 302 303 # catch overlapping slices. Its easier to deal with it here than to catch 304 # earlier and have to avoid all the divide by zeroes above 305 for t in tiles: 306 if t.slice.width() < 0: 307 t.scale.x = 0 308 if t.slice.height() < 0: 309 t.scale.y = 0 310 311 tiles_h = int(math.ceil(tiles[4].border_width.width() / dest_tile_size.width) + 2) 312 tiles_v = int(math.ceil(tiles[4].border_width.height() / dest_tile_size.height) + 2) 313 314 # if border-image-repeat: repeat, then we will later center the tiles, that 315 # means we need an extra tile for the two 'half' tiles at either end 316 if props.repeat == "repeat": 317 if tiles_h % 2 == 0: 318 tiles_h += 1 319 if tiles_v % 2 == 0: 320 tiles_v += 1 321 dest_tiles = [Tile() for i in range(tiles_h * tiles_v)] 322 323 # corners 324 corners = [ 325 (0, 0), 326 (tiles_h - 1, 2), 327 (tiles_v * (tiles_h - 1), 6), 328 (tiles_v * tiles_h - 1, 8), 329 ] 330 for d, s in corners: 331 dest_tiles[d].size = Size( 332 tiles[s].scale.x * props.size.width, tiles[s].scale.y * props.size.height 333 ) 334 dest_tiles[d].dest_size = Size( 335 tiles[s].border_width.width(), tiles[s].border_width.height() 336 ) 337 dest_tiles[0].offset = Point(0, 0) 338 dest_tiles[tiles_h - 1].offset = Point( 339 tiles[2].border_width.width() - dest_tiles[tiles_h - 1].size.width, 0 340 ) 341 dest_tiles[tiles_v * (tiles_h - 1)].offset = Point( 342 0, 343 tiles[6].border_width.height() 344 - dest_tiles[tiles_v * (tiles_h - 1)].size.height, 345 ) 346 dest_tiles[tiles_v * tiles_h - 1].offset = Point( 347 tiles[8].border_width.width() - dest_tiles[tiles_h * tiles_v - 1].size.width, 348 tiles[8].border_width.height() - dest_tiles[tiles_h * tiles_v - 1].size.height, 349 ) 350 351 # horizontal edges 352 for i in range(1, tiles_h - 1): 353 dest_tiles[i].size = Size( 354 tiles[1].scale.x * props.size.width, tiles[1].scale.y * props.size.height 355 ) 356 dest_tiles[(tiles_v - 1) * tiles_h + i].size = Size( 357 tiles[7].scale.x * props.size.width, tiles[7].scale.y * props.size.height 358 ) 359 dest_tiles[i].dest_size = Size( 360 dest_tile_size.width, tiles[1].border_width.height() 361 ) 362 dest_tiles[(tiles_v - 1) * tiles_h + i].dest_size = Size( 363 dest_tile_size.width, tiles[7].border_width.height() 364 ) 365 dest_tiles[i].offset = Point( 366 -tiles[1].scale.x * tiles[1].slice.x, -tiles[1].scale.y * tiles[1].slice.y 367 ) 368 dest_tiles[(tiles_v - 1) * tiles_h + i].offset = Point( 369 -tiles[7].scale.x * tiles[7].slice.x, -tiles[7].scale.y * tiles[7].slice.y 370 ) 371 372 # vertical edges 373 for i in range(1, tiles_v - 1): 374 dest_tiles[i * tiles_h].size = Size( 375 tiles[3].scale.x * props.size.width, tiles[3].scale.y * props.size.height 376 ) 377 dest_tiles[(i + 1) * tiles_h - 1].size = Size( 378 tiles[5].scale.x * props.size.width, tiles[5].scale.y * props.size.height 379 ) 380 dest_tiles[i * tiles_h].dest_size = Size( 381 tiles[3].border_width.width(), dest_tile_size.height 382 ) 383 dest_tiles[(i + 1) * tiles_h - 1].dest_size = Size( 384 tiles[5].border_width.width(), dest_tile_size.height 385 ) 386 dest_tiles[i * tiles_h].offset = Point( 387 -tiles[3].scale.x * tiles[3].slice.x, -tiles[3].scale.y * tiles[3].slice.y 388 ) 389 dest_tiles[(i + 1) * tiles_h - 1].offset = Point( 390 -tiles[5].scale.x * tiles[5].slice.x, -tiles[5].scale.y * tiles[5].slice.y 391 ) 392 393 # middle 394 for i in range(1, tiles_v - 1): 395 for j in range(1, tiles_h - 1): 396 dest_tiles[i * tiles_h + j].size = Size( 397 tiles[4].scale.x * props.size.width, 398 tiles[4].scale.y * props.size.height, 399 ) 400 dest_tiles[i * tiles_h + j].offset = Point( 401 -tiles[4].scale.x * tiles[4].slice.x, 402 -tiles[4].scale.y * tiles[4].slice.y, 403 ) 404 dest_tiles[i * tiles_h + j].dest_size = dest_tile_size 405 406 # edge and middle tiles are centered with border-image-repeat: repeat 407 # we need to change the offset to take account of this and change the dest_size 408 # of the tiles at the sides of the edges if they are clipped 409 if props.repeat == "repeat": 410 diff_h = ( 411 (tiles_h - 2) * dest_tile_size.width - tiles[4].border_width.width() 412 ) / 2 413 diff_v = ( 414 (tiles_v - 2) * dest_tile_size.height - tiles[4].border_width.height() 415 ) / 2 416 for i in range(0, tiles_h): 417 dest_tiles[tiles_h + i].dest_size.height -= diff_v 418 dest_tiles[tiles_h + i].offset.y -= diff_v # * tiles[4].scale.y 419 dest_tiles[(tiles_v - 2) * tiles_h + i].dest_size.height -= diff_v 420 for i in range(0, tiles_v): 421 dest_tiles[i * tiles_h + 1].dest_size.width -= diff_h 422 dest_tiles[i * tiles_h + 1].offset.x -= diff_h # * tiles[4].scale.x 423 dest_tiles[(i + 1) * tiles_h - 2].dest_size.width -= diff_h 424 425 # output the table to simulate the border 426 print("<table>") 427 for i in range(tiles_h): 428 print('<col style="width: ' + str(dest_tiles[i].dest_size.width) + 'px;">') 429 for i in range(tiles_v): 430 print( 431 '<tr style="height: ' 432 + str(dest_tiles[i * tiles_h].dest_size.height) 433 + 'px;">' 434 ) 435 for j in range(tiles_h): 436 width = dest_tiles[i * tiles_h + j].size.width 437 height = dest_tiles[i * tiles_h + j].size.height 438 # catch any tiles with negative widths/heights 439 # this happends when the total of the border-image-slices > borde drawing area 440 if width <= 0 or height <= 0: 441 print(' <td style="background: white;"></td>') 442 else: 443 print( 444 ' <td style="background-image: ' 445 + props.source 446 + "; background-size: " 447 + str(width) 448 + "px " 449 + str(height) 450 + "px; background-position: " 451 + str(dest_tiles[i * tiles_h + j].offset.x) 452 + "px " 453 + str(dest_tiles[i * tiles_h + j].offset.y) 454 + 'px;"></td>' 455 ) 456 print("</tr>") 457 print("</table>") 458 459 460 # start here 461 args = sys.argv[1:] 462 if len(args) == 0: 463 print("whoops: no source file") 464 exit(1) 465 466 467 props = parse(args[0]) 468 if not check_parse(props): 469 print(dir(props)) 470 exit(1) 471 props = normalise(props) 472 if not check_normalise(props): 473 exit(1) 474 compute(props)