text: # -*- coding: windows-1252 -*-
  
  #  Copyright (c) 2010-2020, sikuli.org, sikulix.com - MIT license
  
  import Formatting
  from BIFFRecords import NumberFormatRecord, XFRecord, StyleRecord
  
  FIRST_USER_DEFINED_NUM_FORMAT_IDX = 164
  
  class XFStyle(object):
  
      def __init__(self):
          self.num_format_str  = 'General'
          self.font            = Formatting.Font()
          self.alignment       = Formatting.Alignment()
          self.borders         = Formatting.Borders()
          self.pattern         = Formatting.Pattern()
          self.protection      = Formatting.Protection()
  
  default_style = XFStyle()
  
  class StyleCollection(object):
      _std_num_fmt_list = [
              'general',
              '0',
              '0.00',
              '#,##0',
              '#,##0.00',
              '"$"#,##0_);("$"#,##0)',
              '"$"#,##0_);[Red]("$"#,##0)',
              '"$"#,##0.00_);("$"#,##0.00)',
              '"$"#,##0.00_);[Red]("$"#,##0.00)',
              '0%',
              '0.00%',
              '0.00E+00',
              '# ?/?',
              '# ??/??',
              'M/D/YY',
              'D-MMM-YY',
              'D-MMM',
              'MMM-YY',
              'h:mm AM/PM',
              'h:mm:ss AM/PM',
              'h:mm',
              'h:mm:ss',
              'M/D/YY h:mm',
              '_(#,##0_);(#,##0)',
              '_(#,##0_);[Red](#,##0)',
              '_(#,##0.00_);(#,##0.00)',
              '_(#,##0.00_);[Red](#,##0.00)',
              '_("$"* #,##0_);_("$"* (#,##0);_("$"* "-"_);_(@_)',
              '_(* #,##0_);_(* (#,##0);_(* "-"_);_(@_)',
              '_("$"* #,##0.00_);_("$"* (#,##0.00);_("$"* "-"??_);_(@_)',
              '_(* #,##0.00_);_(* (#,##0.00);_(* "-"??_);_(@_)',
              'mm:ss',
              '[h]:mm:ss',
              'mm:ss.0',
              '##0.0E+0',
              '@'
      ]
  
      def __init__(self, style_compression=0):
          self.style_compression = style_compression
          self.stats = [0, 0, 0, 0, 0, 0]
          self._font_id2x = {}
          self._font_x2id = {}
          self._font_val2x = {}
  
          for x in (0, 1, 2, 3, 5): # The font with index 4 is omitted in all BIFF versions
              font = Formatting.Font()
              search_key = font._search_key()
              self._font_id2x[font] = x
              self._font_x2id[x] = font
              self._font_val2x[search_key] = x
  
          self._xf_id2x = {}
          self._xf_x2id = {}
          self._xf_val2x = {}
  
          self._num_formats = {}
          for fmtidx, fmtstr in zip(range(0, 23), StyleCollection._std_num_fmt_list[0:23]):
              self._num_formats[fmtstr] = fmtidx
          for fmtidx, fmtstr in zip(range(37, 50), StyleCollection._std_num_fmt_list[23:]):
              self._num_formats[fmtstr] = fmtidx
  
          self.default_style = XFStyle()
          self._default_xf = self._add_style(self.default_style)[0]
  
      def add(self, style):
          if style == None:
              return 0x10
          return self._add_style(style)[1]
  
      def _add_style(self, style):
          num_format_str = style.num_format_str
          if num_format_str in self._num_formats:
              num_format_idx = self._num_formats[num_format_str]
          else:
              num_format_idx = (
                  FIRST_USER_DEFINED_NUM_FORMAT_IDX
                  + len(self._num_formats)
                  - len(StyleCollection._std_num_fmt_list)
                  )
              self._num_formats[num_format_str] = num_format_idx
  
          font = style.font
          if font in self._font_id2x:
              font_idx = self._font_id2x[font]
              self.stats[0] += 1
          elif self.style_compression:
              search_key = font._search_key()
              font_idx = self._font_val2x.get(search_key)
              if font_idx is not None:
                  self._font_id2x[font] = font_idx
                  self.stats[1] += 1
              else:
                  font_idx = len(self._font_x2id) + 1 # Why plus 1? Font 4 is missing
                  self._font_id2x[font] = font_idx
                  self._font_val2x[search_key] = font_idx
                  self._font_x2id[font_idx] = font
                  self.stats[2] += 1
          else:
              font_idx = len(self._font_id2x) + 1
              self._font_id2x[font] = font_idx
              self.stats[2] += 1
  
          gof = (style.alignment, style.borders, style.pattern, style.protection)
          xf = (font_idx, num_format_idx) + gof
          if xf in self._xf_id2x:
              xf_index = self._xf_id2x[xf]
              self.stats[3] += 1
          elif self.style_compression == 2:
              xf_key = (font_idx, num_format_idx) + tuple([obj._search_key() for obj in gof])
              xf_index = self._xf_val2x.get(xf_key)
              if xf_index is not None:
                  self._xf_id2x[xf] = xf_index
                  self.stats[4] += 1
              else:
                  xf_index = 0x10 + len(self._xf_x2id)
                  self._xf_id2x[xf] = xf_index
                  self._xf_val2x[xf_key] = xf_index
                  self._xf_x2id[xf_index] = xf
                  self.stats[5] += 1
          else:
              xf_index = 0x10 + len(self._xf_id2x)
              self._xf_id2x[xf] = xf_index
              self.stats[5] += 1
  
          if xf_index >= 0xFFF:
              # 12 bits allowed, 0xFFF is a sentinel value
              raise ValueError("More than 4094 XFs (styles)")
  
          return xf, xf_index
          
      def add_font(self, font):
          return self._add_font(font)
          
      def _add_font(self, font):
          if font in self._font_id2x:
              font_idx = self._font_id2x[font]
              self.stats[0] += 1
          elif self.style_compression:
              search_key = font._search_key()
              font_idx = self._font_val2x.get(search_key)
              if font_idx is not None:
                  self._font_id2x[font] = font_idx
                  self.stats[1] += 1
              else:
                  font_idx = len(self._font_x2id) + 1 # Why plus 1? Font 4 is missing
                  self._font_id2x[font] = font_idx
                  self._font_val2x[search_key] = font_idx
                  self._font_x2id[font_idx] = font
                  self.stats[2] += 1
          else:
              font_idx = len(self._font_id2x) + 1
              self._font_id2x[font] = font_idx
              self.stats[2] += 1
              
          return font_idx
  
  
      def get_biff_data(self):
          result = ''
          result += self._all_fonts()
          result += self._all_num_formats()
          result += self._all_cell_styles()
          result += self._all_styles()
          return result
  
      def _all_fonts(self):
          result = ''
          if self.style_compression:
              alist = self._font_x2id.items()
          else:
              alist = [(x, o) for o, x in self._font_id2x.items()]
          alist.sort()
          for font_idx, font in alist:
              result += font.get_biff_record().get()
          return result
  
      def _all_num_formats(self):
          result = ''
          alist = [
              (v, k)
              for k, v in self._num_formats.items()
              if v >= FIRST_USER_DEFINED_NUM_FORMAT_IDX
              ]
          alist.sort()
          for fmtidx, fmtstr in alist:
              result += NumberFormatRecord(fmtidx, fmtstr).get()
          return result
  
      def _all_cell_styles(self):
          result = ''
          for i in range(0, 16):
              result += XFRecord(self._default_xf, 'style').get()
          if self.style_compression == 2:
              alist = self._xf_x2id.items()
          else:
              alist = [(x, o) for o, x in self._xf_id2x.items()]
          alist.sort()
          for xf_idx, xf in alist:
              result += XFRecord(xf).get()
          return result
  
      def _all_styles(self):
          return StyleRecord().get()
  
  # easyxf and its supporting objects ###################################
  
  class EasyXFException(Exception):
      pass
  
  class EasyXFCallerError(EasyXFException):
      pass
  
  class EasyXFAuthorError(EasyXFException):
      pass
  
  class IntULim(object):
      # If astring represents a valid unsigned integer ('123', '0xabcd', etc)
      # and it is <= limit, return the int value; otherwise return None.
  
      def __init__(self, limit):
          self.limit = limit
  
      def __call__(self, astring):
          try:
              value = int(astring, 0)
          except ValueError:
              return None
          if not 0 <= value <= self.limit:
              return None
          return value
  
  bool_map = {
      # Text values for all Boolean attributes
      '1': 1, 'yes': 1, 'true':  1, 'on':  1,
      '0': 0, 'no':  0, 'false': 0, 'off': 0,
      }
  
  border_line_map = {
      # Text values for these borders attributes:
      # left, right, top, bottom and diag
      'no_line':  0x00,
      'thin':     0x01,
      'medium':   0x02,
      'dashed':   0x03,
      'dotted':   0x04,
      'thick':    0x05,
      'double':   0x06,
      'hair':     0x07,
      'medium_dashed':                0x08,
      'thin_dash_dotted':             0x09,
      'medium_dash_dotted':           0x0a,
      'thin_dash_dot_dotted':         0x0b,
      'medium_dash_dot_dotted':       0x0c,
      'slanted_medium_dash_dotted':   0x0d,
      }
  
  charset_map = {
      # Text values for font.charset
      'ansi_latin':           0x00,
      'sys_default':          0x01,
      'symbol':               0x02,
      'apple_roman':          0x4d,
      'ansi_jap_shift_jis':   0x80,
      'ansi_kor_hangul':      0x81,
      'ansi_kor_johab':       0x82,
      'ansi_chinese_gbk':     0x86,
      'ansi_chinese_big5':    0x88,
      'ansi_greek':           0xa1,
      'ansi_turkish':         0xa2,
      'ansi_vietnamese':      0xa3,
      'ansi_hebrew':          0xb1,
      'ansi_arabic':          0xb2,
      'ansi_baltic':          0xba,
      'ansi_cyrillic':        0xcc,
      'ansi_thai':            0xde,
      'ansi_latin_ii':        0xee,
      'oem_latin_i':          0xff,
      }
  
  
  # Text values for colour indices. "grey" is a synonym of "gray".
  # The names are those given by Microsoft Excel 2003 to the colours
  # in the default palette. There is no great correspondence with
  # any W3C name-to-RGB mapping.
  _colour_map_text = """\
  aqua 0x31
  black 0x08
  blue 0x0C
  blue_gray 0x36
  bright_green 0x0B
  brown 0x3C
  coral 0x1D
  cyan_ega 0x0F
  dark_blue 0x12
  dark_blue_ega 0x12
  dark_green 0x3A
  dark_green_ega 0x11
  dark_purple 0x1C
  dark_red 0x10
  dark_red_ega 0x10
  dark_teal 0x38
  dark_yellow 0x13
  gold 0x33
  gray_ega 0x17
  gray25 0x16
  gray40 0x37
  gray50 0x17
  gray80 0x3F
  green 0x11
  ice_blue 0x1F
  indigo 0x3E
  ivory 0x1A
  lavender 0x2E
  light_blue 0x30
  light_green 0x2A
  light_orange 0x34
  light_turquoise 0x29
  light_yellow 0x2B
  lime 0x32
  magenta_ega 0x0E
  ocean_blue 0x1E
  olive_ega 0x13
  olive_green 0x3B
  orange 0x35
  pale_blue 0x2C
  periwinkle 0x18
  pink 0x0E
  plum 0x3D
  purple_ega 0x14
  red 0x0A
  rose 0x2D
  sea_green 0x39
  silver_ega 0x16
  sky_blue 0x28
  tan 0x2F
  teal 0x15
  teal_ega 0x15
  turquoise 0x0F
  violet 0x14
  white 0x09
  yellow 0x0D"""
  
  colour_map = {}
  for _line in _colour_map_text.splitlines():
      _name, _num = _line.split()
      _num = int(_num, 0)
      colour_map[_name] = _num
      if 'gray' in _name:
          colour_map[_name.replace('gray', 'grey')] = _num
  del _colour_map_text, _line, _name, _num
  
  def add_palette_colour(colour_str, colour_index):
      if not (8 <= colour_index <= 63):
          raise Exception("add_palette_colour: colour_index (%d) not in range(8, 64)" % 
                  (colour_index))
      colour_map[colour_str] = colour_index
  
  # user-defined palette defines 56 RGB colors from entry 8 - 64
  #excel_default_palette_b8 = [ # (red, green, blue)
  #    (  0,  0,  0), (255,255,255), (255,  0,  0), (  0,255,  0),
  #    (  0,  0,255), (255,255,  0), (255,  0,255), (  0,255,255),
  #    (128,  0,  0), (  0,128,  0), (  0,  0,128), (128,128,  0),
  #    (128,  0,128), (  0,128,128), (192,192,192), (128,128,128),
  #    (153,153,255), (153, 51,102), (255,255,204), (204,255,255),
  #    (102,  0,102), (255,128,128), (  0,102,204), (204,204,255),
  #    (  0,  0,128), (255,  0,255), (255,255,  0), (  0,255,255),
  #    (128,  0,128), (128,  0,  0), (  0,128,128), (  0,  0,255),
  #    (  0,204,255), (204,255,255), (204,255,204), (255,255,153),
  #    (153,204,255), (255,153,204), (204,153,255), (255,204,153),
  #    ( 51,102,255), ( 51,204,204), (153,204,  0), (255,204,  0),
  #    (255,153,  0), (255,102,  0), (102,102,153), (150,150,150),
  #    (  0, 51,102), ( 51,153,102), (  0, 51,  0), ( 51, 51,  0),
  #    (153, 51,  0), (153, 51,102), ( 51, 51,153), ( 51, 51, 51),
  #    ]
  
  # Default colour table for BIFF8 copied from 
  # OpenOffice.org's Documentation of the Microsoft Excel File Format, Excel Version 2003
  # Note palette has LSB padded with 2 bytes 0x00
  excel_default_palette_b8 = ( 
  0x00000000, 
  0xFFFFFF00, 
  0xFF000000, 
  0x00FF0000, 
  0x0000FF00, 
  0xFFFF0000, 
  0xFF00FF00, 
  0x00FFFF00,
  0x80000000, 
  0x00800000, 
  0x00008000, 
  0x80800000, 
  0x80008000, 
  0x00808000, 
  0xC0C0C000, 
  0x80808000, 
  0x9999FF00, 
  0x99336600, 
  0xFFFFCC00, 
  0xCCFFFF00, 
  0x66006600, 
  0xFF808000, 
  0x0066CC00, 
  0xCCCCFF00, 
  0x00008000, 
  0xFF00FF00, 
  0xFFFF0000, 
  0x00FFFF00, 
  0x80008000, 
  0x80000000, 
  0x00808000, 
  0x0000FF00, 
  0x00CCFF00, 
  0xCCFFFF00, 
  0xCCFFCC00, 
  0xFFFF9900, 
  0x99CCFF00, 
  0xFF99CC00, 
  0xCC99FF00, 
  0xFFCC9900, 
  0x3366FF00, 
  0x33CCCC00, 
  0x99CC0000, 
  0xFFCC0000, 
  0xFF990000, 
  0xFF660000, 
  0x66669900, 
  0x96969600, 
  0x00336600, 
  0x33996600, 
  0x00330000, 
  0x33330000, 
  0x99330000, 
  0x99336600, 
  0x33339900, 
  0x33333300)
  
  assert len(excel_default_palette_b8) == 56
  
  pattern_map = {
      # Text values for pattern.pattern
      # xlwt/doc/pattern_examples.xls showcases all of these patterns.
      'no_fill':              0,
      'none':                 0,
      'solid':                1,
      'solid_fill':           1,
      'solid_pattern':        1,
      'fine_dots':            2,
      'alt_bars':             3,
      'sparse_dots':          4,
      'thick_horz_bands':     5,
      'thick_vert_bands':     6,
      'thick_backward_diag':  7,
      'thick_forward_diag':   8,
      'big_spots':            9,
      'bricks':               10,
      'thin_horz_bands':      11,
      'thin_vert_bands':      12,
      'thin_backward_diag':   13,
      'thin_forward_diag':    14,
      'squares':              15,
      'diamonds':             16,
      }
  
  def any_str_func(s):
      return s.strip()
  
  def colour_index_func(s, maxval=0x7F):
      try:
          value = int(s, 0)
      except ValueError:
          return None
      if not (0 <= value <= maxval):
          return None
      return value
  
  colour_index_func_7 = colour_index_func
  
  def colour_index_func_15(s):
      return colour_index_func(s, maxval=0x7FFF)
  
  def rotation_func(s):
      try:
          value = int(s, 0)
      except ValueError:
          return None
      if not (-90 <= value <= 90):
          raise EasyXFCallerError("rotation %d: should be -90 to +90 degrees" % value)
      if value < 0:
          value = 90 - value # encode as 91 to 180 (clockwise)
      return value
  
  xf_dict = {
      'align': 'alignment', # synonym
      'alignment': {
          'dire': {
              'general': 0,
              'lr': 1,
              'rl': 2,
              },
          'direction': 'dire',
          'horiz': 'horz',
          'horizontal': 'horz',
          'horz': {
              'general': 0,
              'left': 1,
              'center': 2,
              'centre': 2, # "align: horiz centre" means xf.alignment.horz is set to 2
              'right': 3,
              'filled': 4,
              'justified': 5,
              'center_across_selection': 6,
              'centre_across_selection': 6,
              'distributed': 7,
              },
          'inde': IntULim(15), # restriction: 0 <= value <= 15
          'indent': 'inde',
          'rota': [{'stacked': 255, 'none': 0, }, rotation_func],
          'rotation': 'rota',
          'shri': bool_map,
          'shrink': 'shri',
          'shrink_to_fit': 'shri',
          'vert': {
              'top': 0,
              'center': 1,
              'centre': 1,
              'bottom': 2,
              'justified': 3,
              'distributed': 4,
              },
           'vertical': 'vert',
           'wrap': bool_map,
           },
      'border': 'borders',
      'borders': {
          'left':     [border_line_map, IntULim(0x0d)],
          'right':    [border_line_map, IntULim(0x0d)],
          'top':      [border_line_map, IntULim(0x0d)],
          'bottom':   [border_line_map, IntULim(0x0d)],
          'diag':     [border_line_map, IntULim(0x0d)],
          'top_colour':       [colour_map, colour_index_func_7],
          'bottom_colour':    [colour_map, colour_index_func_7],
          'left_colour':      [colour_map, colour_index_func_7],
          'right_colour':     [colour_map, colour_index_func_7],
          'diag_colour':      [colour_map, colour_index_func_7],
          'top_color':        'top_colour',
          'bottom_color':     'bottom_colour',
          'left_color':       'left_colour',
          'right_color':      'right_colour',
          'diag_color':       'diag_colour',
          'need_diag1':  bool_map,
          'need_diag2':  bool_map,
          },
      'font': {
          'bold': bool_map,
          'charset': charset_map,
          'color':  'colour_index',
          'color_index':  'colour_index',
          'colour':  'colour_index',
          'colour_index': [colour_map, colour_index_func_15],
          'escapement': {'none': 0, 'superscript': 1, 'subscript': 2},
          'family': {'none': 0, 'roman': 1, 'swiss': 2, 'modern': 3, 'script': 4, 'decorative': 5, },
          'height': IntULim(0xFFFF), # practical limits are much narrower e.g. 160 to 1440 (8pt to 72pt)
          'italic': bool_map,
          'name': any_str_func,
          'outline': bool_map,
          'shadow': bool_map,
          'struck_out': bool_map,
          'underline': [bool_map, {'none': 0, 'single': 1, 'single_acc': 0x21, 'double': 2, 'double_acc': 0x22, }],
          },
      'pattern': {
          'back_color':   'pattern_back_colour',
          'back_colour':  'pattern_back_colour',
          'fore_color':   'pattern_fore_colour',
          'fore_colour':  'pattern_fore_colour',
          'pattern': [pattern_map, IntULim(16)],
          'pattern_back_color':   'pattern_back_colour',
          'pattern_back_colour':  [colour_map, colour_index_func_7],
          'pattern_fore_color':   'pattern_fore_colour',
          'pattern_fore_colour':  [colour_map, colour_index_func_7],
          },
      'protection': {
          'cell_locked' :   bool_map,
          'formula_hidden': bool_map,
          },
      }
  
  def _esplit(s, split_char, esc_char="\\"):
      escaped = False
      olist = ['']
      for c in s:
          if escaped:
              olist[-1] += c
              escaped = False
          elif c == esc_char:
              escaped = True
          elif c == split_char:
              olist.append('')
          else:
              olist[-1] += c
      return olist
  
  def _parse_strg_to_obj(strg, obj, parse_dict,
      field_sep=",", line_sep=";", intro_sep=":", esc_char="\\", debug=False):
      for line in _esplit(strg, line_sep, esc_char):
          line = line.strip()
          if not line:
              break
          split_line = _esplit(line, intro_sep, esc_char)
          if len(split_line) != 2:
              raise EasyXFCallerError('line %r should have exactly 1 "%c"' % (line, intro_sep))
          section, item_str = split_line
          section = section.strip().lower()
          for counter in range(2):
              result = parse_dict.get(section)
              if result is None:
                  raise EasyXFCallerError('section %r is unknown' % section)
              if isinstance(result, dict):
                  break
              if not isinstance(result, str):
                  raise EasyXFAuthorError(
                      'section %r should map to dict or str object; found %r' % (section, type(result)))
              # synonym
              old_section = section
              section = result
          else:
              raise EasyXFAuthorError('Attempt to define synonym of synonym (%r: %r)' % (old_section, result))
          section_dict = result
          section_obj = getattr(obj, section, None)
          if section_obj is None:
              raise EasyXFAuthorError('instance of %s class has no attribute named %s' % (obj.__class__.__name__, section))
          for kv_str in _esplit(item_str, field_sep, esc_char):
              guff = kv_str.split()
              if not guff:
                  continue
              k = guff[0].lower().replace('-', '_')
              v = ' '.join(guff[1:])
              if not v:
                  raise EasyXFCallerError("no value supplied for %s.%s" % (section, k))
              for counter in xrange(2):
                  result = section_dict.get(k)
                  if result is None:
                      raise EasyXFCallerError('%s.%s is not a known attribute' % (section, k))
                  if not isinstance(result, basestring):
                      break
                  # synonym
                  old_k = k
                  k = result
              else:
                  raise EasyXFAuthorError('Attempt to define synonym of synonym (%r: %r)' % (old_k, result))
              value_info = result
              if not isinstance(value_info, list):
                  value_info = [value_info]
              for value_rule in value_info:
                  if isinstance(value_rule, dict):
                      # dict maps strings to integer field values
                      vl = v.lower().replace('-', '_')
                      if vl in value_rule:
                          value = value_rule[vl]
                          break
                  elif callable(value_rule):
                      value = value_rule(v)
                      if value is not None:
                          break
                  else:
                      raise EasyXFAuthorError("unknown value rule for attribute %r: %r" % (k, value_rule))
              else:
                  raise EasyXFCallerError("unexpected value %r for %s.%s" % (v, section, k))
              try:
                  orig = getattr(section_obj, k)
              except AttributeError:
                  raise EasyXFAuthorError('%s.%s in dictionary but not in supplied object' % (section, k))
              if debug: print "+++ %s.%s = %r # %s; was %r" % (section, k, value, v, orig)
              setattr(section_obj, k, value)
  
  def easyxf(strg_to_parse="", num_format_str=None,
      field_sep=",", line_sep=";", intro_sep=":", esc_char="\\", debug=False):
      xfobj = XFStyle()
      if num_format_str is not None:
          xfobj.num_format_str = num_format_str
      if strg_to_parse:
          _parse_strg_to_obj(strg_to_parse, xfobj, xf_dict,
              field_sep=field_sep, line_sep=line_sep, intro_sep=intro_sep, esc_char=esc_char, debug=debug)
      return xfobj
  
  def easyfont(strg_to_parse="", field_sep=",", esc_char="\\", debug=False):
      xfobj = XFStyle()
      if strg_to_parse:
          _parse_strg_to_obj("font: " + strg_to_parse, xfobj, xf_dict,
              field_sep=field_sep, line_sep=";", intro_sep=":", esc_char=esc_char, debug=debug)
      return xfobj.font
  