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