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