1# Purpose: read and write AutoCAD CTB files 2# Created: 23.03.2010 for dxfwrite, added to ezdxf package on 2016-03-06 3# Copyright (c) 2010-2019, Manfred Moitzi 4# License: MIT License 5# IMPORTANT: use only standard 7-Bit ascii code 6 7from typing import Union, Tuple, Optional, BinaryIO, TextIO, Iterable, List, Any, Dict 8from abc import abstractmethod 9from io import StringIO 10from array import array 11from struct import pack 12import zlib 13 14END_STYLE_BUTT = 0 15END_STYLE_SQUARE = 1 16END_STYLE_ROUND = 2 17END_STYLE_DIAMOND = 3 18END_STYLE_OBJECT = 4 19 20JOIN_STYLE_MITER = 0 21JOIN_STYLE_BEVEL = 1 22JOIN_STYLE_ROUND = 2 23JOIN_STYLE_DIAMOND = 3 24JOIN_STYLE_OBJECT = 5 25 26FILL_STYLE_SOLID = 64 27FILL_STYLE_CHECKERBOARD = 65 28FILL_STYLE_CROSSHATCH = 66 29FILL_STYLE_DIAMONDS = 67 30FILL_STYLE_HORIZONTAL_BARS = 68 31FILL_STYLE_SLANT_LEFT = 69 32FILL_STYLE_SLANT_RIGHT = 70 33FILL_STYLE_SQUARE_DOTS = 71 34FILL_STYLE_VERICAL_BARS = 72 35FILL_STYLE_OBJECT = 73 36 37DITHERING_ON = 1 # bit coded color_policy 38GRAYSCALE_ON = 2 # bit coded color_policy 39NAMED_COLOR = 4 # bit coded color_policy 40 41AUTOMATIC = 0 42OBJECT_LINEWEIGHT = 0 43OBJECT_LINETYPE = 31 44OBJECT_COLOR = -1 45OBJECT_COLOR2 = -1006632961 46 47STYLE_COUNT = 255 48 49DEFAULT_LINE_WEIGHTS = [ 50 0.00, # 0 51 0.05, # 1 52 0.09, # 2 53 0.10, # 3 54 0.13, # 4 55 0.15, # 5 56 0.18, # 6 57 0.20, # 7 58 0.25, # 8 59 0.30, # 9 60 0.35, # 10 61 0.40, # 11 62 0.45, # 12 63 0.50, # 13 64 0.53, # 14 65 0.60, # 15 66 0.65, # 16 67 0.70, # 17 68 0.80, # 18 69 0.90, # 19 70 1.00, # 20 71 1.06, # 21 72 1.20, # 22 73 1.40, # 23 74 1.58, # 24 75 2.00, # 25 76 2.11, # 26 77] 78 79# color_type: (thx to Rammi) 80 81# Take color from layer, ignore other bytes. 82COLOR_BY_LAYER = 0xc0 83 84# Take color from insertion, ignore other bytes 85COLOR_BY_BLOCK = 0xc1 86 87# RGB value, other bytes are R,G,B. 88COLOR_RGB = 0xc2 89 90# ACI, AutoCAD color index, other bytes are 0,0,index ??? 91COLOR_ACI = 0xc3 92 93 94def color_name(index: int) -> str: 95 return 'Color_%d' % (index + 1) 96 97 98def get_bool(value: Union[str, bool]) -> bool: 99 if isinstance(value, str): 100 upperstr = value.upper() 101 if upperstr == 'TRUE': 102 value = True 103 elif upperstr == 'FALSE': 104 value = False 105 else: 106 raise ValueError("Unknown bool value '%s'." % str(value)) 107 return value 108 109 110class PlotStyle: 111 def __init__(self, index: int, data: dict = None, parent: 'PlotStyleTable' = None): 112 data = data or {} 113 self.parent = parent 114 self.index = int(index) 115 self.name = str(data.get('name', color_name(index))) 116 self.localized_name = str(data.get('localized_name', color_name(index))) 117 self.description = str(data.get('description', "")) 118 # do not set _color, _mode_color or _color_policy directly 119 # use set_color() method, and the properties dithering and grayscale 120 self._color = int(data.get('color', OBJECT_COLOR)) 121 self._color_type = COLOR_RGB 122 if self._color != OBJECT_COLOR: 123 self._mode_color = int(data.get('mode_color', self._color)) 124 self._color_policy = int(data.get('color_policy', DITHERING_ON)) 125 self.physical_pen_number = int(data.get('physical_pen_number', AUTOMATIC)) 126 self.virtual_pen_number = int(data.get('virtual_pen_number', AUTOMATIC)) 127 self.screen = int(data.get('screen', 100)) 128 self.linepattern_size = float(data.get('linepattern_size', 0.5)) 129 self.linetype = int(data.get('linetype', OBJECT_LINETYPE)) # 0 .. 30 130 self.adaptive_linetype = get_bool(data.get('adaptive_linetype', True)) 131 132 # lineweight index 133 self.lineweight = int(data.get('lineweight', OBJECT_LINEWEIGHT)) 134 self.end_style = int(data.get('end_style', END_STYLE_OBJECT)) 135 self.join_style = int(data.get('join_style', JOIN_STYLE_OBJECT)) 136 self.fill_style = int(data.get('fill_style', FILL_STYLE_OBJECT)) 137 138 @property 139 def color(self) -> Optional[Tuple[int, int, int]]: 140 """ Get style color as ``(r, g, b)`` tuple or ``None``, if style has object color. """ 141 if self.has_object_color(): 142 return None # object color 143 else: 144 return int2color(self._mode_color)[:3] 145 146 @color.setter 147 def color(self, rgb: Tuple[int, int, int]) -> None: 148 """ Set color as RGB values. """ 149 r, g, b = rgb 150 # when defining a user-color, `mode_color` represents the real true_color as (r, g, b) tuple and 151 # color_type = COLOR_RGB (0xC2) as highest byte, the `color` value calculated for a user-color is not a 152 # (r, g, b) tuple and has color_type = COLOR_ACI (0xC3) (sometimes), set for `color` the same value as for 153 # `mode_color`, because AutoCAD corrects the `color` value by itself. 154 self._mode_color = mode_color2int(r, g, b, color_type=self._color_type) 155 self._color = self._mode_color 156 157 @property 158 def color_type(self): 159 if self.has_object_color(): 160 return None # object color 161 else: 162 return self._color_type 163 164 @color_type.setter 165 def color_type(self, value: int): 166 self._color_type = value 167 168 def set_object_color(self) -> None: 169 """ Set color to object color. """ 170 self._color = OBJECT_COLOR 171 self._mode_color = OBJECT_COLOR 172 173 def set_lineweight(self, lineweight: float) -> None: 174 """ Set `lineweight` in millimeters. Use ``0.0`` to set lineweight by object. """ 175 self.lineweight = self.parent.get_lineweight_index(lineweight) 176 177 def get_lineweight(self) -> float: 178 """ Returns the lineweight in millimeters or `0.0` for use entity lineweight. """ 179 return self.parent.lineweights[self.lineweight] 180 181 def has_object_color(self) -> bool: 182 """ ``True`` if style has object color. """ 183 return self._color in (OBJECT_COLOR, OBJECT_COLOR2) 184 185 @property 186 def aci(self) -> int: 187 """ :ref:`ACI` in range from ``1`` to ``255``. Has no meaning for named plot styles. (int) """ 188 return self.index + 1 189 190 @property 191 def dithering(self) -> bool: 192 """ Depending on the capabilities of your plotter, dithering approximates the colors with dot patterns. 193 When this option is ``False``, the colors are mapped to the nearest color, resulting in a smaller range of 194 colors when plotting. 195 196 Dithering is available only whether you select the object’s color or assign a plot style color. 197 198 """ 199 return bool(self._color_policy & DITHERING_ON) 200 201 @dithering.setter 202 def dithering(self, status: bool) -> None: 203 if status: 204 self._color_policy |= DITHERING_ON 205 else: 206 self._color_policy &= ~DITHERING_ON 207 208 @property 209 def grayscale(self) -> bool: 210 """ Plot colors in grayscale. (bool) """ 211 return bool(self._color_policy & GRAYSCALE_ON) 212 213 @grayscale.setter 214 def grayscale(self, status: bool) -> None: 215 if status: 216 self._color_policy |= GRAYSCALE_ON 217 else: 218 self._color_policy &= ~GRAYSCALE_ON 219 220 @property 221 def named_color(self) -> bool: 222 return bool(self._color_policy & NAMED_COLOR) 223 224 @named_color.setter 225 def named_color(self, status: bool) -> None: 226 if status: 227 self._color_policy |= NAMED_COLOR 228 else: 229 self._color_policy &= ~NAMED_COLOR 230 231 def write(self, stream: TextIO) -> None: 232 """ Write style data to file-like object `stream`. """ 233 index = self.index 234 stream.write(' %d{\n' % index) 235 stream.write(' name="%s\n' % self.name) 236 stream.write(' localized_name="%s\n' % self.localized_name) 237 stream.write(' description="%s\n' % self.description) 238 stream.write(' color=%d\n' % self._color) 239 if self._color != OBJECT_COLOR: 240 stream.write(' mode_color=%d\n' % self._mode_color) 241 stream.write(' color_policy=%d\n' % self._color_policy) 242 stream.write(' physical_pen_number=%d\n' % self.physical_pen_number) 243 stream.write(' virtual_pen_number=%d\n' % self.virtual_pen_number) 244 stream.write(' screen=%d\n' % self.screen) 245 stream.write(' linepattern_size=%s\n' % str(self.linepattern_size)) 246 stream.write(' linetype=%d\n' % self.linetype) 247 stream.write(' adaptive_linetype=%s\n' % str(bool(self.adaptive_linetype)).upper()) 248 stream.write(' lineweight=%s\n' % str(self.lineweight)) 249 stream.write(' fill_style=%d\n' % self.fill_style) 250 stream.write(' end_style=%d\n' % self.end_style) 251 stream.write(' join_style=%d\n' % self.join_style) 252 stream.write(' }\n') 253 254 255class PlotStyleTable: 256 """ PlotStyle container """ 257 258 def __init__(self, description: str = '', scale_factor: float = 1.0, apply_factor: bool = False): 259 self.description = description 260 self.scale_factor = scale_factor 261 self.apply_factor = apply_factor 262 263 # set custom_lineweight_display_units to 1 for showing lineweight in inch in AutoCAD CTB editor window, but 264 # lineweight is always defined in mm 265 self.custom_lineweight_display_units = 0 266 self.lineweights = array('f', DEFAULT_LINE_WEIGHTS) 267 268 def get_lineweight_index(self, lineweight: float) -> int: 269 """ Get index of `lineweight` in the lineweight table or append `lineweight` to lineweight table. """ 270 try: 271 return self.lineweights.index(lineweight) 272 except ValueError: 273 self.lineweights.append(lineweight) 274 return len(self.lineweights) - 1 275 276 def set_table_lineweight(self, index: int, lineweight: float) -> int: 277 """ 278 Argument `index` is the lineweight table index, not the :ref:`ACI`. 279 280 Args: 281 index: lineweight table index = :attr:`PlotStyle.lineweight` 282 lineweight: in millimeters 283 284 """ 285 try: 286 self.lineweights[index] = lineweight 287 return index 288 except IndexError: 289 self.lineweights.append(lineweight) 290 return len(self.lineweights) - 1 291 292 def get_table_lineweight(self, index: int) -> float: 293 """ 294 Returns lineweight in millimeters of lineweight table entry `index`. 295 296 Args: 297 index: lineweight table index = :attr:`PlotStyle.lineweight` 298 299 Returns: 300 lineweight in mm or ``0.0`` for use entity lineweight 301 302 """ 303 return self.lineweights[index] 304 305 def save(self, filename: str) -> None: 306 """ Save CTB or STB file as `filename` to the file system. """ 307 with open(filename, 'wb') as stream: 308 self.write(stream) 309 310 def write(self, stream: BinaryIO) -> None: 311 """ Compress and write the CTB or STB file to binary `stream`. """ 312 memfile = StringIO() 313 self.write_content(memfile) 314 memfile.write(chr(0)) # end of file 315 body = memfile.getvalue() 316 memfile.close() 317 _compress(stream, body) 318 319 @abstractmethod 320 def write_content(self, stream: TextIO) -> None: 321 pass 322 323 def _write_lineweights(self, stream: TextIO) -> None: 324 """ Write custom lineweight table to text `stream`. """ 325 stream.write('custom_lineweight_table{\n') 326 for index, weight in enumerate(self.lineweights): 327 stream.write(' %d=%.2f\n' % (index, weight)) 328 stream.write('}\n') 329 330 def parse(self, text: str) -> None: 331 """ Parse plot styles from CTB string `text`. """ 332 333 def set_lineweights(lineweights): 334 if lineweights is None: 335 return 336 self.lineweights = array('f', [0.0] * len(lineweights)) 337 for key, value in lineweights.items(): 338 self.lineweights[int(key)] = float(value) 339 340 parser = PlotStyleFileParser(text) 341 self.description = parser.get('description', "") 342 self.scale_factor = float(parser.get('scale_factor', 1.0)) 343 self.apply_factor = get_bool(parser.get('apply_factor', True)) 344 self.custom_lineweight_display_units = int( 345 parser.get('custom_lineweight_display_units', 0)) 346 set_lineweights(parser.get('custom_lineweight_table', None)) 347 self.load_styles(parser.get('plot_style', {})) 348 349 @abstractmethod 350 def load_styles(self, styles): 351 pass 352 353 354class ColorDependentPlotStyles(PlotStyleTable): 355 def __init__(self, description: str = '', scale_factor: float = 1.0, apply_factor: bool = False): 356 super().__init__(description, scale_factor, apply_factor) 357 self._styles = [PlotStyle(index, parent=self) for index in range(STYLE_COUNT)] # type: List[PlotStyle] 358 self._styles.insert(0, PlotStyle(256)) # 1-based array: insert dummy value for index 0 359 360 def __getitem__(self, aci: int) -> PlotStyle: 361 """ Returns :class:`PlotStyle` for :ref:`ACI` `aci`. """ 362 if 0 < aci < 256: 363 return self._styles[aci] 364 else: 365 raise IndexError(aci) 366 367 def __setitem__(self, aci: int, style: PlotStyle): 368 """ Set plot `style` for `aci`. """ 369 if 0 < aci < 256: 370 style.parent = self 371 self._styles[aci] = style 372 else: 373 raise IndexError(aci) 374 375 def __iter__(self) -> Iterable[PlotStyle]: 376 """ Iterable of all plot styles. """ 377 return iter(self._styles[1:]) 378 379 def new_style(self, aci: int, data: dict = None) -> PlotStyle: 380 """ Set `aci` to new attributes defined by `data` dict. 381 382 Args: 383 aci: :ref:`ACI` 384 data: ``dict`` of :class:`PlotStyle` attributes: description, color, physical_pen_number, 385 virtual_pen_number, screen, linepattern_size, linetype, adaptive_linetype, 386 lineweight, end_style, join_style, fill_style 387 388 """ 389 # ctb table index = aci - 1 390 # ctb table starts with index 0, where aci == 0 means BYBLOCK 391 style = PlotStyle(index=aci - 1, data=data) 392 style.color_type = COLOR_RGB 393 self[aci] = style 394 return style 395 396 def get_lineweight(self, aci: int): 397 """ Returns the assigned lineweight for :class:`PlotStyle` `aci` in millimeter. """ 398 style = self[aci] 399 lineweight = style.get_lineweight() 400 if lineweight == 0.0: 401 return None 402 else: 403 return lineweight 404 405 def write_content(self, stream: TextIO) -> None: 406 """ Write the CTB-file to text `stream`. """ 407 self._write_header(stream) 408 self._write_aci_table(stream) 409 self._write_plot_styles(stream) 410 self._write_lineweights(stream) 411 412 def _write_header(self, stream: TextIO) -> None: 413 """ Write header values of CTB-file to text `stream`. """ 414 stream.write('description="%s\n' % self.description) 415 stream.write('aci_table_available=TRUE\n') 416 stream.write('scale_factor=%.1f\n' % self.scale_factor) 417 stream.write('apply_factor=%s\n' % str(self.apply_factor).upper()) 418 stream.write('custom_lineweight_display_units=%s\n' % str( 419 self.custom_lineweight_display_units)) 420 421 def _write_aci_table(self, stream: TextIO) -> None: 422 """ Write AutoCAD Color Index table to text `stream`. """ 423 stream.write('aci_table{\n') 424 for style in self: 425 index = style.index 426 stream.write(' %d="%s\n' % (index, color_name(index))) 427 stream.write('}\n') 428 429 def _write_plot_styles(self, stream: TextIO) -> None: 430 """ Write user styles to text `stream`. """ 431 stream.write('plot_style{\n') 432 for style in self: 433 style.write(stream) 434 stream.write('}\n') 435 436 def load_styles(self, styles): 437 for index, style in styles.items(): 438 index = int(index) 439 style = PlotStyle(index, style) 440 style.color_type = COLOR_RGB 441 aci = index + 1 442 self[aci] = style 443 444 445class NamedPlotStyles(PlotStyleTable): 446 def __init__(self, description: str = '', scale_factor: float = 1.0, apply_factor: bool = False): 447 super().__init__(description, scale_factor, apply_factor) 448 normal = PlotStyle(0, data={ 449 'name': 'Normal', 450 'localized_name': 'Normal', 451 }) 452 self._styles = {'Normal': normal} # type: Dict[str, PlotStyle] 453 454 def __iter__(self) -> Iterable[str]: 455 """ Iterable of all plot style names. """ 456 return self.keys() 457 458 def __getitem__(self, name: str) -> PlotStyle: 459 """ Returns :class:`PlotStyle` by `name`. """ 460 return self._styles[name] 461 462 def __delitem__(self, name: str) -> None: 463 """ Delete plot style `name`. Plot style ``'Normal'`` is not deletable. """ 464 if name != 'Normal': 465 del self._styles[name] 466 else: 467 raise ValueError("Can't delete plot style 'Normal'. ") 468 469 def keys(self) -> Iterable[str]: 470 """ Iterable of all plot style names. """ 471 keys = set(self._styles.keys()) 472 keys.discard('Normal') 473 result = ['Normal'] 474 result.extend(sorted(keys)) 475 return iter(result) 476 477 def items(self) -> Iterable[Tuple[str, PlotStyle]]: 478 """ Iterable of all plot styles as (``name``, class:`PlotStyle`) tuples. """ 479 for key in self.keys(): 480 yield key, self._styles[key] 481 482 def values(self) -> Iterable[PlotStyle]: 483 """ Iterable of all class:`PlotStyle` objects. """ 484 for key, value in self.items(): 485 yield value 486 487 def new_style(self, name: str, data: dict = None, localized_name: str = None) -> PlotStyle: 488 """ Create new class:`PlotStyle` `name` by attribute dict `data`, replaces existing class:`PlotStyle` objects. 489 490 Args: 491 name: plot style name 492 localized_name: name shown in plot style editor, uses `name` if ``None`` 493 data: ``dict`` of :class:`PlotStyle` attributes: description, color, physical_pen_number, 494 virtual_pen_number, screen, linepattern_size, linetype, adaptive_linetype, 495 lineweight, end_style, join_style, fill_style 496 497 """ 498 if name.lower() == 'Normal': 499 raise ValueError("Can't replace or modify plot style 'Normal'. ") 500 data = data or {} 501 data['name'] = name 502 data['localized_name'] = localized_name or name 503 index = len(self._styles) 504 style = PlotStyle(index=index, data=data, parent=self) 505 style.color_type = COLOR_ACI 506 style.named_color = True 507 self._styles[name] = style 508 return style 509 510 def get_lineweight(self, name: str): 511 """ Returns the assigned lineweight for :class:`PlotStyle` `name` in millimeter. """ 512 style = self[name] 513 lineweight = style.get_lineweight() 514 if lineweight == 0.0: 515 return None 516 else: 517 return lineweight 518 519 def write_content(self, stream: TextIO) -> None: 520 """ Write the STB-file to text `stream`. """ 521 self._write_header(stream) 522 self._write_plot_styles(stream) 523 self._write_lineweights(stream) 524 525 def _write_header(self, stream: TextIO) -> None: 526 """ Write header values of CTB-file to text `stream`. """ 527 stream.write('description="%s\n' % self.description) 528 stream.write('aci_table_available=FALSE\n') 529 stream.write('scale_factor=%.1f\n' % self.scale_factor) 530 stream.write('apply_factor=%s\n' % str(self.apply_factor).upper()) 531 stream.write('custom_lineweight_display_units=%s\n' % str( 532 self.custom_lineweight_display_units)) 533 534 def _write_plot_styles(self, stream: TextIO) -> None: 535 """ Write user styles to text `stream`. """ 536 stream.write('plot_style{\n') 537 for index, style in enumerate(self.values()): 538 style.index = index 539 style.write(stream) 540 stream.write('}\n') 541 542 def load_styles(self, styles): 543 for index, style in styles.items(): 544 index = int(index) 545 style = PlotStyle(index, style) 546 style.color_type = COLOR_ACI 547 self._styles[style.name] = style 548 549 550def _read_ctb(stream: BinaryIO) -> ColorDependentPlotStyles: 551 """ Read a CTB-file from from binary `stream`. """ 552 content = _decompress(stream) 553 content = content.decode() 554 styles = ColorDependentPlotStyles() 555 styles.parse(content) 556 return styles 557 558 559def _read_stb(stream: BinaryIO) -> NamedPlotStyles: 560 """ Read a STB-file from from binary `stream`. """ 561 content = _decompress(stream) 562 content = content.decode() 563 styles = NamedPlotStyles() 564 styles.parse(content) 565 return styles 566 567 568def load(filename: str) -> Union[ColorDependentPlotStyles, NamedPlotStyles]: 569 """ Load the CTB or STB file `filename` from file system. """ 570 571 with open(filename, 'rb') as stream: 572 if filename.lower().endswith('.ctb'): 573 table = _read_ctb(stream) 574 elif filename.lower().endswith('.stb'): 575 table = _read_stb(stream) 576 else: 577 raise ValueError('Invalid file type: "{}"'.format(filename)) 578 return table 579 580 581def new_ctb() -> ColorDependentPlotStyles: 582 """ 583 Create a new CTB file. 584 585 .. versionchanged:: 0.10 586 587 renamed from :func:`new` 588 589 """ 590 return ColorDependentPlotStyles() 591 592 593def new_stb() -> NamedPlotStyles: 594 """ Create a new STB file. """ 595 return NamedPlotStyles() 596 597 598def _decompress(stream: BinaryIO) -> bytes: 599 """ Read and decompress the file content from binray `stream`. """ 600 content = stream.read() 601 data = zlib.decompress(content[60:]) # type: bytes 602 return data[:-1] # truncate trailing \nul 603 604 605def _compress(stream: BinaryIO, content: str): 606 """ Compress `content` and write to binary `stream`. """ 607 608 def writestr(s): 609 stream.write(s.encode()) 610 611 content = content.encode() 612 comp_body = zlib.compress(content) 613 adler_chksum = zlib.adler32(comp_body) 614 writestr('PIAFILEVERSION_2.0,CTBVER1,compress\r\npmzlibcodec') 615 stream.write(pack('LLL', adler_chksum, len(content), len(comp_body))) 616 stream.write(comp_body) 617 618 619class PlotStyleFileParser: 620 """ 621 A very simple CTB/STB file parser. CTB/STB files are created by applications, so the file structure should be 622 correct in the most cases. 623 624 """ 625 626 def __init__(self, text: str): 627 """ 628 :param str text: ctb content as string 629 630 """ 631 self.data = {} 632 for element, value in PlotStyleFileParser.iteritems(text): 633 self.data[element] = value 634 635 @staticmethod 636 def iteritems(text: str): 637 """ 638 iterate over all first level (start at col 0) elements 639 640 """ 641 642 def get_name() -> str: 643 """ 644 Get element name of line <line_index>. 645 646 """ 647 line = lines[line_index] 648 if line.endswith('{'): # start of a list like 'plot_style{' 649 name = line[:-1] 650 else: # simple name=value line 651 name = line.split('=', 1)[0] 652 return name.strip() 653 654 def get_mapping() -> dict: 655 """ 656 Get mapping of elements enclosed by { }. 657 658 e. g. lineweigths, plot_styles, aci_table 659 660 """ 661 nonlocal line_index 662 663 def end_of_list(): 664 return lines[line_index].endswith('}') 665 666 data = dict() 667 while not end_of_list(): 668 name = get_name() 669 value = get_value() # get value or sub-list 670 data[name] = value 671 line_index += 1 672 return data # skip '}' - end of list 673 674 def get_value() -> Union[str, dict]: 675 """ 676 Get value of line <line_index> or the list that starts in line <line_index>. 677 678 """ 679 nonlocal line_index 680 line = lines[line_index] 681 if line.endswith('{'): # start of a list 682 line_index += 1 683 value = get_mapping() 684 else: # it's a simple name=value line 685 value = line.split('=', 1)[1] # type: str 686 value = value.lstrip('"') # strings look like this: name="value 687 line_index += 1 688 return value 689 690 def skip_empty_lines(): 691 nonlocal line_index 692 while line_index < len(lines) and len(lines[line_index]) == 0: 693 line_index += 1 694 695 lines = text.split('\n') 696 line_index = 0 697 while line_index < len(lines): 698 name = get_name() 699 value = get_value() 700 yield (name, value) 701 skip_empty_lines() 702 703 def get(self, name: str, default: Any) -> Any: 704 return self.data.get(name, default) 705 706 707def int2color(color: int) -> Tuple[int, int, int, int]: 708 """ Convert color integer value from CTB-file to ``(r, g, b, color_type) tuple. """ 709 # Take color from layer, ignore other bytes. 710 color_type = (color & 0xff000000) >> 24 711 red = (color & 0xff0000) >> 16 712 green = (color & 0xff00) >> 8 713 blue = color & 0xff 714 return red, green, blue, color_type 715 716 717def mode_color2int(red: int, green: int, blue: int, color_type=COLOR_RGB) -> int: 718 """ Convert mode_color (r, g, b, color_type) tuple to integer. """ 719 return -color2int(red, green, blue, color_type) 720 721 722def color2int(red: int, green: int, blue: int, color_type: int) -> int: 723 """ Convert color (r, g, b, color_type) to integer. """ 724 return -((color_type << 24) + (red << 16) + (green << 8) + blue) & 0xffffffff 725