from __future__ import print_function # -*- coding: windows-1252 -*- from . import Formatting from .BIFFRecords import NumberFormatRecord, XFRecord, StyleRecord from .compat import basestring, xrange 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 = b'' 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 = b'' if self.style_compression: fonts = self._font_x2id.items() else: fonts = [(x, o) for o, x in self._font_id2x.items()] for font_idx, font in sorted(fonts): result += font.get_biff_record().get() return result def _all_num_formats(self): result = b'' 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 = b'' for i in range(0, 16): result += XFRecord(self._default_xf, 'style').get() if self.style_compression == 2: styles = self._xf_x2id.items() else: styles = [(x, o) for o, x in self._xf_id2x.items()] for xf_idx, xf in sorted(styles): 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): """ This function is used to create and configure :class:`XFStyle` objects for use with (for example) the :meth:`Worksheet.write` method. It takes a string to be parsed to obtain attribute values for :class:`Alignment`, :class:`Borders`, :class:`Font`, :class:`Pattern` and :class:`Protection` objects. Refer to the examples in the file `examples/xlwt_easyxf_simple_demo.py` and to the `xf_dict` dictionary in :mod:`xlwt.Style`. Various synonyms including color/colour, center/centre and gray/grey are allowed. Case is irrelevant (except maybe in font names). ``-`` may be used instead of ``_``. Example: ``font: bold on; align: wrap on, vert centre, horiz center`` :param num_format_str: To get the "number format string" of an existing cell whose format you want to reproduce, select the cell and click on Format/Cells/Number/Custom. Otherwise, refer to Excel help. Examples: ``"#,##0.00"``, ``"dd/mm/yyyy"`` :return: An :class:`XFstyle` object. """ 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