diff options
Diffstat (limited to '.venv/lib/python3.12/site-packages/xlrd')
-rw-r--r-- | .venv/lib/python3.12/site-packages/xlrd/__init__.py | 213 | ||||
-rw-r--r-- | .venv/lib/python3.12/site-packages/xlrd/biffh.py | 643 | ||||
-rw-r--r-- | .venv/lib/python3.12/site-packages/xlrd/book.py | 1474 | ||||
-rw-r--r-- | .venv/lib/python3.12/site-packages/xlrd/compdoc.py | 485 | ||||
-rw-r--r-- | .venv/lib/python3.12/site-packages/xlrd/formatting.py | 1324 | ||||
-rw-r--r-- | .venv/lib/python3.12/site-packages/xlrd/formula.py | 2190 | ||||
-rw-r--r-- | .venv/lib/python3.12/site-packages/xlrd/info.py | 1 | ||||
-rw-r--r-- | .venv/lib/python3.12/site-packages/xlrd/sheet.py | 2490 | ||||
-rw-r--r-- | .venv/lib/python3.12/site-packages/xlrd/timemachine.py | 53 | ||||
-rw-r--r-- | .venv/lib/python3.12/site-packages/xlrd/xldate.py | 248 |
10 files changed, 9121 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/xlrd/__init__.py b/.venv/lib/python3.12/site-packages/xlrd/__init__.py new file mode 100644 index 00000000..84d5f269 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlrd/__init__.py @@ -0,0 +1,213 @@ +# Copyright (c) 2005-2012 Stephen John Machin, Lingfo Pty Ltd +# This module is part of the xlrd package, which is released under a +# BSD-style licence. +import os +import pprint +import sys +import zipfile + +from . import timemachine +from .biffh import ( + XL_CELL_BLANK, XL_CELL_BOOLEAN, XL_CELL_DATE, XL_CELL_EMPTY, XL_CELL_ERROR, + XL_CELL_NUMBER, XL_CELL_TEXT, XLRDError, biff_text_from_num, + error_text_from_code, +) +from .book import Book, colname, open_workbook_xls +from .compdoc import SIGNATURE as XLS_SIGNATURE +from .formula import * # is constrained by __all__ +from .info import __VERSION__, __version__ +from .sheet import empty_cell +from .xldate import XLDateError, xldate_as_datetime, xldate_as_tuple + + +#: descriptions of the file types :mod:`xlrd` can :func:`inspect <inspect_format>`. +FILE_FORMAT_DESCRIPTIONS = { + 'xls': 'Excel xls', + 'xlsb': 'Excel 2007 xlsb file', + 'xlsx': 'Excel xlsx file', + 'ods': 'Openoffice.org ODS file', + 'zip': 'Unknown ZIP file', + None: 'Unknown file type', +} + +ZIP_SIGNATURE = b"PK\x03\x04" + +PEEK_SIZE = max(len(XLS_SIGNATURE), len(ZIP_SIGNATURE)) + + +def inspect_format(path=None, content=None): + """ + Inspect the content at the supplied path or the :class:`bytes` content provided + and return the file's type as a :class:`str`, or ``None`` if it cannot + be determined. + + :param path: + A :class:`string <str>` path containing the content to inspect. + ``~`` will be expanded. + + :param content: + The :class:`bytes` content to inspect. + + :returns: + A :class:`str`, or ``None`` if the format cannot be determined. + The return value can always be looked up in :data:`FILE_FORMAT_DESCRIPTIONS` + to return a human-readable description of the format found. + """ + if content: + peek = content[:PEEK_SIZE] + else: + path = os.path.expanduser(path) + with open(path, "rb") as f: + peek = f.read(PEEK_SIZE) + + if peek.startswith(XLS_SIGNATURE): + return 'xls' + + if peek.startswith(ZIP_SIGNATURE): + zf = zipfile.ZipFile(timemachine.BYTES_IO(content) if content else path) + + # Workaround for some third party files that use forward slashes and + # lower case names. We map the expected name in lowercase to the + # actual filename in the zip container. + component_names = {name.replace('\\', '/').lower(): name + for name in zf.namelist()} + + if 'xl/workbook.xml' in component_names: + return 'xlsx' + if 'xl/workbook.bin' in component_names: + return 'xlsb' + if 'content.xml' in component_names: + return 'ods' + return 'zip' + + +def open_workbook(filename=None, + logfile=sys.stdout, + verbosity=0, + use_mmap=True, + file_contents=None, + encoding_override=None, + formatting_info=False, + on_demand=False, + ragged_rows=False, + ignore_workbook_corruption=False + ): + """ + Open a spreadsheet file for data extraction. + + :param filename: The path to the spreadsheet file to be opened. + + :param logfile: An open file to which messages and diagnostics are written. + + :param verbosity: Increases the volume of trace material written to the + logfile. + + :param use_mmap: + + Whether to use the mmap module is determined heuristically. + Use this arg to override the result. + + Current heuristic: mmap is used if it exists. + + :param file_contents: + + A string or an :class:`mmap.mmap` object or some other behave-alike + object. If ``file_contents`` is supplied, ``filename`` will not be used, + except (possibly) in messages. + + :param encoding_override: + + Used to overcome missing or bad codepage information + in older-version files. See :doc:`unicode`. + + :param formatting_info: + + The default is ``False``, which saves memory. + In this case, "Blank" cells, which are those with their own formatting + information but no data, are treated as empty by ignoring the file's + ``BLANK`` and ``MULBLANK`` records. + This cuts off any bottom or right "margin" of rows of empty or blank + cells. + Only :meth:`~xlrd.sheet.Sheet.cell_value` and + :meth:`~xlrd.sheet.Sheet.cell_type` are available. + + When ``True``, formatting information will be read from the spreadsheet + file. This provides all cells, including empty and blank cells. + Formatting information is available for each cell. + + Note that this will raise a NotImplementedError when used with an + xlsx file. + + :param on_demand: + + Governs whether sheets are all loaded initially or when demanded + by the caller. See :doc:`on_demand`. + + :param ragged_rows: + + The default of ``False`` means all rows are padded out with empty cells so + that all rows have the same size as found in + :attr:`~xlrd.sheet.Sheet.ncols`. + + ``True`` means that there are no empty cells at the ends of rows. + This can result in substantial memory savings if rows are of widely + varying sizes. See also the :meth:`~xlrd.sheet.Sheet.row_len` method. + + + :param ignore_workbook_corruption: + + This option allows to read corrupted workbooks. + When ``False`` you may face CompDocError: Workbook corruption. + When ``True`` that exception will be ignored. + + :returns: An instance of the :class:`~xlrd.book.Book` class. + """ + + file_format = inspect_format(filename, file_contents) + # We have to let unknown file formats pass through here, as some ancient + # files that xlrd can parse don't start with the expected signature. + if file_format and file_format != 'xls': + raise XLRDError(FILE_FORMAT_DESCRIPTIONS[file_format]+'; not supported') + + bk = open_workbook_xls( + filename=filename, + logfile=logfile, + verbosity=verbosity, + use_mmap=use_mmap, + file_contents=file_contents, + encoding_override=encoding_override, + formatting_info=formatting_info, + on_demand=on_demand, + ragged_rows=ragged_rows, + ignore_workbook_corruption=ignore_workbook_corruption, + ) + + return bk + + +def dump(filename, outfile=sys.stdout, unnumbered=False): + """ + For debugging: dump an XLS file's BIFF records in char & hex. + + :param filename: The path to the file to be dumped. + :param outfile: An open file, to which the dump is written. + :param unnumbered: If true, omit offsets (for meaningful diffs). + """ + from .biffh import biff_dump + bk = Book() + bk.biff2_8_load(filename=filename, logfile=outfile, ) + biff_dump(bk.mem, bk.base, bk.stream_len, 0, outfile, unnumbered) + + +def count_records(filename, outfile=sys.stdout): + """ + For debugging and analysis: summarise the file's BIFF records. + ie: produce a sorted file of ``(record_name, count)``. + + :param filename: The path to the file to be summarised. + :param outfile: An open file, to which the summary is written. + """ + from .biffh import biff_count_records + bk = Book() + bk.biff2_8_load(filename=filename, logfile=outfile, ) + biff_count_records(bk.mem, bk.base, bk.stream_len, outfile) diff --git a/.venv/lib/python3.12/site-packages/xlrd/biffh.py b/.venv/lib/python3.12/site-packages/xlrd/biffh.py new file mode 100644 index 00000000..07ac6291 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlrd/biffh.py @@ -0,0 +1,643 @@ +# -*- coding: utf-8 -*- +# Portions copyright © 2005-2010 Stephen John Machin, Lingfo Pty Ltd +# This module is part of the xlrd package, which is released under a +# BSD-style licence. +from __future__ import print_function + +import sys +from struct import unpack + +from .timemachine import * + +DEBUG = 0 + + + +class XLRDError(Exception): + """ + An exception indicating problems reading data from an Excel file. + """ + + +class BaseObject(object): + """ + Parent of almost all other classes in the package. Defines a common + :meth:`dump` method for debugging. + """ + + _repr_these = [] + + + def dump(self, f=None, header=None, footer=None, indent=0): + """ + :param f: open file object, to which the dump is written + :param header: text to write before the dump + :param footer: text to write after the dump + :param indent: number of leading spaces (for recursive calls) + """ + if f is None: + f = sys.stderr + if hasattr(self, "__slots__"): + alist = [] + for attr in self.__slots__: + alist.append((attr, getattr(self, attr))) + else: + alist = self.__dict__.items() + alist = sorted(alist) + pad = " " * indent + if header is not None: print(header, file=f) + list_type = type([]) + dict_type = type({}) + for attr, value in alist: + if getattr(value, 'dump', None) and attr != 'book': + value.dump(f, + header="%s%s (%s object):" % (pad, attr, value.__class__.__name__), + indent=indent+4) + elif (attr not in self._repr_these and + (isinstance(value, list_type) or isinstance(value, dict_type))): + print("%s%s: %s, len = %d" % (pad, attr, type(value), len(value)), file=f) + else: + fprintf(f, "%s%s: %r\n", pad, attr, value) + if footer is not None: print(footer, file=f) + +FUN, FDT, FNU, FGE, FTX = range(5) # unknown, date, number, general, text +DATEFORMAT = FDT +NUMBERFORMAT = FNU + +( + XL_CELL_EMPTY, + XL_CELL_TEXT, + XL_CELL_NUMBER, + XL_CELL_DATE, + XL_CELL_BOOLEAN, + XL_CELL_ERROR, + XL_CELL_BLANK, # for use in debugging, gathering stats, etc +) = range(7) + +biff_text_from_num = { + 0: "(not BIFF)", + 20: "2.0", + 21: "2.1", + 30: "3", + 40: "4S", + 45: "4W", + 50: "5", + 70: "7", + 80: "8", + 85: "8X", +} + +#: This dictionary can be used to produce a text version of the internal codes +#: that Excel uses for error cells. +error_text_from_code = { + 0x00: '#NULL!', # Intersection of two cell ranges is empty + 0x07: '#DIV/0!', # Division by zero + 0x0F: '#VALUE!', # Wrong type of operand + 0x17: '#REF!', # Illegal or deleted cell reference + 0x1D: '#NAME?', # Wrong function or range name + 0x24: '#NUM!', # Value range overflow + 0x2A: '#N/A', # Argument or function not available +} + +BIFF_FIRST_UNICODE = 80 + +XL_WORKBOOK_GLOBALS = WBKBLOBAL = 0x5 +XL_WORKBOOK_GLOBALS_4W = 0x100 +XL_WORKSHEET = WRKSHEET = 0x10 + +XL_BOUNDSHEET_WORKSHEET = 0x00 +XL_BOUNDSHEET_CHART = 0x02 +XL_BOUNDSHEET_VB_MODULE = 0x06 + +# XL_RK2 = 0x7e +XL_ARRAY = 0x0221 +XL_ARRAY2 = 0x0021 +XL_BLANK = 0x0201 +XL_BLANK_B2 = 0x01 +XL_BOF = 0x809 +XL_BOOLERR = 0x205 +XL_BOOLERR_B2 = 0x5 +XL_BOUNDSHEET = 0x85 +XL_BUILTINFMTCOUNT = 0x56 +XL_CF = 0x01B1 +XL_CODEPAGE = 0x42 +XL_COLINFO = 0x7D +XL_COLUMNDEFAULT = 0x20 # BIFF2 only +XL_COLWIDTH = 0x24 # BIFF2 only +XL_CONDFMT = 0x01B0 +XL_CONTINUE = 0x3c +XL_COUNTRY = 0x8C +XL_DATEMODE = 0x22 +XL_DEFAULTROWHEIGHT = 0x0225 +XL_DEFCOLWIDTH = 0x55 +XL_DIMENSION = 0x200 +XL_DIMENSION2 = 0x0 +XL_EFONT = 0x45 +XL_EOF = 0x0a +XL_EXTERNNAME = 0x23 +XL_EXTERNSHEET = 0x17 +XL_EXTSST = 0xff +XL_FEAT11 = 0x872 +XL_FILEPASS = 0x2f +XL_FONT = 0x31 +XL_FONT_B3B4 = 0x231 +XL_FORMAT = 0x41e +XL_FORMAT2 = 0x1E # BIFF2, BIFF3 +XL_FORMULA = 0x6 +XL_FORMULA3 = 0x206 +XL_FORMULA4 = 0x406 +XL_GCW = 0xab +XL_HLINK = 0x01B8 +XL_QUICKTIP = 0x0800 +XL_HORIZONTALPAGEBREAKS = 0x1b +XL_INDEX = 0x20b +XL_INTEGER = 0x2 # BIFF2 only +XL_IXFE = 0x44 # BIFF2 only +XL_LABEL = 0x204 +XL_LABEL_B2 = 0x04 +XL_LABELRANGES = 0x15f +XL_LABELSST = 0xfd +XL_LEFTMARGIN = 0x26 +XL_TOPMARGIN = 0x28 +XL_RIGHTMARGIN = 0x27 +XL_BOTTOMMARGIN = 0x29 +XL_HEADER = 0x14 +XL_FOOTER = 0x15 +XL_HCENTER = 0x83 +XL_VCENTER = 0x84 +XL_MERGEDCELLS = 0xE5 +XL_MSO_DRAWING = 0x00EC +XL_MSO_DRAWING_GROUP = 0x00EB +XL_MSO_DRAWING_SELECTION = 0x00ED +XL_MULRK = 0xbd +XL_MULBLANK = 0xbe +XL_NAME = 0x18 +XL_NOTE = 0x1c +XL_NUMBER = 0x203 +XL_NUMBER_B2 = 0x3 +XL_OBJ = 0x5D +XL_PAGESETUP = 0xA1 +XL_PALETTE = 0x92 +XL_PANE = 0x41 +XL_PRINTGRIDLINES = 0x2B +XL_PRINTHEADERS = 0x2A +XL_RK = 0x27e +XL_ROW = 0x208 +XL_ROW_B2 = 0x08 +XL_RSTRING = 0xd6 +XL_SCL = 0x00A0 +XL_SHEETHDR = 0x8F # BIFF4W only +XL_SHEETPR = 0x81 +XL_SHEETSOFFSET = 0x8E # BIFF4W only +XL_SHRFMLA = 0x04bc +XL_SST = 0xfc +XL_STANDARDWIDTH = 0x99 +XL_STRING = 0x207 +XL_STRING_B2 = 0x7 +XL_STYLE = 0x293 +XL_SUPBOOK = 0x1AE # aka EXTERNALBOOK in OOo docs +XL_TABLEOP = 0x236 +XL_TABLEOP2 = 0x37 +XL_TABLEOP_B2 = 0x36 +XL_TXO = 0x1b6 +XL_UNCALCED = 0x5e +XL_UNKNOWN = 0xffff +XL_VERTICALPAGEBREAKS = 0x1a +XL_WINDOW2 = 0x023E +XL_WINDOW2_B2 = 0x003E +XL_WRITEACCESS = 0x5C +XL_WSBOOL = XL_SHEETPR +XL_XF = 0xe0 +XL_XF2 = 0x0043 # BIFF2 version of XF record +XL_XF3 = 0x0243 # BIFF3 version of XF record +XL_XF4 = 0x0443 # BIFF4 version of XF record + +boflen = {0x0809: 8, 0x0409: 6, 0x0209: 6, 0x0009: 4} +bofcodes = (0x0809, 0x0409, 0x0209, 0x0009) + +XL_FORMULA_OPCODES = (0x0006, 0x0406, 0x0206) + +_cell_opcode_list = [ + XL_BOOLERR, + XL_FORMULA, + XL_FORMULA3, + XL_FORMULA4, + XL_LABEL, + XL_LABELSST, + XL_MULRK, + XL_NUMBER, + XL_RK, + XL_RSTRING, +] +_cell_opcode_dict = {} +for _cell_opcode in _cell_opcode_list: + _cell_opcode_dict[_cell_opcode] = 1 + +def is_cell_opcode(c): + return c in _cell_opcode_dict + +def upkbits(tgt_obj, src, manifest, local_setattr=setattr): + for n, mask, attr in manifest: + local_setattr(tgt_obj, attr, (src & mask) >> n) + +def upkbitsL(tgt_obj, src, manifest, local_setattr=setattr, local_int=int): + for n, mask, attr in manifest: + local_setattr(tgt_obj, attr, local_int((src & mask) >> n)) + +def unpack_string(data, pos, encoding, lenlen=1): + nchars = unpack('<' + 'BH'[lenlen-1], data[pos:pos+lenlen])[0] + pos += lenlen + return unicode(data[pos:pos+nchars], encoding) + +def unpack_string_update_pos(data, pos, encoding, lenlen=1, known_len=None): + if known_len is not None: + # On a NAME record, the length byte is detached from the front of the string. + nchars = known_len + else: + nchars = unpack('<' + 'BH'[lenlen-1], data[pos:pos+lenlen])[0] + pos += lenlen + newpos = pos + nchars + return (unicode(data[pos:newpos], encoding), newpos) + +def unpack_unicode(data, pos, lenlen=2): + "Return unicode_strg" + nchars = unpack('<' + 'BH'[lenlen-1], data[pos:pos+lenlen])[0] + if not nchars: + # Ambiguous whether 0-length string should have an "options" byte. + # Avoid crash if missing. + return UNICODE_LITERAL("") + pos += lenlen + options = BYTES_ORD(data[pos]) + pos += 1 + # phonetic = options & 0x04 + # richtext = options & 0x08 + if options & 0x08: + # rt = unpack('<H', data[pos:pos+2])[0] # unused + pos += 2 + if options & 0x04: + # sz = unpack('<i', data[pos:pos+4])[0] # unused + pos += 4 + if options & 0x01: + # Uncompressed UTF-16-LE + rawstrg = data[pos:pos+2*nchars] + # if DEBUG: print "nchars=%d pos=%d rawstrg=%r" % (nchars, pos, rawstrg) + strg = unicode(rawstrg, 'utf_16_le') + # pos += 2*nchars + else: + # Note: this is COMPRESSED (not ASCII!) encoding!!! + # Merely returning the raw bytes would work OK 99.99% of the time + # if the local codepage was cp1252 -- however this would rapidly go pear-shaped + # for other codepages so we grit our Anglocentric teeth and return Unicode :-) + + strg = unicode(data[pos:pos+nchars], "latin_1") + # pos += nchars + # if richtext: + # pos += 4 * rt + # if phonetic: + # pos += sz + # return (strg, pos) + return strg + +def unpack_unicode_update_pos(data, pos, lenlen=2, known_len=None): + "Return (unicode_strg, updated value of pos)" + if known_len is not None: + # On a NAME record, the length byte is detached from the front of the string. + nchars = known_len + else: + nchars = unpack('<' + 'BH'[lenlen-1], data[pos:pos+lenlen])[0] + pos += lenlen + if not nchars and not data[pos:]: + # Zero-length string with no options byte + return (UNICODE_LITERAL(""), pos) + options = BYTES_ORD(data[pos]) + pos += 1 + phonetic = options & 0x04 + richtext = options & 0x08 + if richtext: + rt = unpack('<H', data[pos:pos+2])[0] + pos += 2 + if phonetic: + sz = unpack('<i', data[pos:pos+4])[0] + pos += 4 + if options & 0x01: + # Uncompressed UTF-16-LE + strg = unicode(data[pos:pos+2*nchars], 'utf_16_le') + pos += 2*nchars + else: + # Note: this is COMPRESSED (not ASCII!) encoding!!! + strg = unicode(data[pos:pos+nchars], "latin_1") + pos += nchars + if richtext: + pos += 4 * rt + if phonetic: + pos += sz + return (strg, pos) + +def unpack_cell_range_address_list_update_pos(output_list, data, pos, biff_version, addr_size=6): + # output_list is updated in situ + assert addr_size in (6, 8) + # Used to assert size == 6 if not BIFF8, but pyWLWriter writes + # BIFF8-only MERGEDCELLS records in a BIFF5 file! + n, = unpack("<H", data[pos:pos+2]) + pos += 2 + if n: + if addr_size == 6: + fmt = "<HHBB" + else: + fmt = "<HHHH" + for _unused in xrange(n): + ra, rb, ca, cb = unpack(fmt, data[pos:pos+addr_size]) + output_list.append((ra, rb+1, ca, cb+1)) + pos += addr_size + return pos + +_brecstrg = """\ +0000 DIMENSIONS_B2 +0001 BLANK_B2 +0002 INTEGER_B2_ONLY +0003 NUMBER_B2 +0004 LABEL_B2 +0005 BOOLERR_B2 +0006 FORMULA +0007 STRING_B2 +0008 ROW_B2 +0009 BOF_B2 +000A EOF +000B INDEX_B2_ONLY +000C CALCCOUNT +000D CALCMODE +000E PRECISION +000F REFMODE +0010 DELTA +0011 ITERATION +0012 PROTECT +0013 PASSWORD +0014 HEADER +0015 FOOTER +0016 EXTERNCOUNT +0017 EXTERNSHEET +0018 NAME_B2,5+ +0019 WINDOWPROTECT +001A VERTICALPAGEBREAKS +001B HORIZONTALPAGEBREAKS +001C NOTE +001D SELECTION +001E FORMAT_B2-3 +001F BUILTINFMTCOUNT_B2 +0020 COLUMNDEFAULT_B2_ONLY +0021 ARRAY_B2_ONLY +0022 DATEMODE +0023 EXTERNNAME +0024 COLWIDTH_B2_ONLY +0025 DEFAULTROWHEIGHT_B2_ONLY +0026 LEFTMARGIN +0027 RIGHTMARGIN +0028 TOPMARGIN +0029 BOTTOMMARGIN +002A PRINTHEADERS +002B PRINTGRIDLINES +002F FILEPASS +0031 FONT +0032 FONT2_B2_ONLY +0036 TABLEOP_B2 +0037 TABLEOP2_B2 +003C CONTINUE +003D WINDOW1 +003E WINDOW2_B2 +0040 BACKUP +0041 PANE +0042 CODEPAGE +0043 XF_B2 +0044 IXFE_B2_ONLY +0045 EFONT_B2_ONLY +004D PLS +0051 DCONREF +0055 DEFCOLWIDTH +0056 BUILTINFMTCOUNT_B3-4 +0059 XCT +005A CRN +005B FILESHARING +005C WRITEACCESS +005D OBJECT +005E UNCALCED +005F SAVERECALC +0063 OBJECTPROTECT +007D COLINFO +007E RK2_mythical_? +0080 GUTS +0081 WSBOOL +0082 GRIDSET +0083 HCENTER +0084 VCENTER +0085 BOUNDSHEET +0086 WRITEPROT +008C COUNTRY +008D HIDEOBJ +008E SHEETSOFFSET +008F SHEETHDR +0090 SORT +0092 PALETTE +0099 STANDARDWIDTH +009B FILTERMODE +009C FNGROUPCOUNT +009D AUTOFILTERINFO +009E AUTOFILTER +00A0 SCL +00A1 SETUP +00AB GCW +00BD MULRK +00BE MULBLANK +00C1 MMS +00D6 RSTRING +00D7 DBCELL +00DA BOOKBOOL +00DD SCENPROTECT +00E0 XF +00E1 INTERFACEHDR +00E2 INTERFACEEND +00E5 MERGEDCELLS +00E9 BITMAP +00EB MSO_DRAWING_GROUP +00EC MSO_DRAWING +00ED MSO_DRAWING_SELECTION +00EF PHONETIC +00FC SST +00FD LABELSST +00FF EXTSST +013D TABID +015F LABELRANGES +0160 USESELFS +0161 DSF +01AE SUPBOOK +01AF PROTECTIONREV4 +01B0 CONDFMT +01B1 CF +01B2 DVAL +01B6 TXO +01B7 REFRESHALL +01B8 HLINK +01BC PASSWORDREV4 +01BE DV +01C0 XL9FILE +01C1 RECALCID +0200 DIMENSIONS +0201 BLANK +0203 NUMBER +0204 LABEL +0205 BOOLERR +0206 FORMULA_B3 +0207 STRING +0208 ROW +0209 BOF +020B INDEX_B3+ +0218 NAME +0221 ARRAY +0223 EXTERNNAME_B3-4 +0225 DEFAULTROWHEIGHT +0231 FONT_B3B4 +0236 TABLEOP +023E WINDOW2 +0243 XF_B3 +027E RK +0293 STYLE +0406 FORMULA_B4 +0409 BOF +041E FORMAT +0443 XF_B4 +04BC SHRFMLA +0800 QUICKTIP +0809 BOF +0862 SHEETLAYOUT +0867 SHEETPROTECTION +0868 RANGEPROTECTION +""" + +biff_rec_name_dict = {} +for _buff in _brecstrg.splitlines(): + _numh, _name = _buff.split() + biff_rec_name_dict[int(_numh, 16)] = _name +del _buff, _name, _brecstrg + +def hex_char_dump(strg, ofs, dlen, base=0, fout=sys.stdout, unnumbered=False): + endpos = min(ofs + dlen, len(strg)) + pos = ofs + numbered = not unnumbered + num_prefix = '' + while pos < endpos: + endsub = min(pos + 16, endpos) + substrg = strg[pos:endsub] + lensub = endsub - pos + if lensub <= 0 or lensub != len(substrg): + fprintf( + sys.stdout, + '??? hex_char_dump: ofs=%d dlen=%d base=%d -> endpos=%d pos=%d endsub=%d substrg=%r\n', + ofs, dlen, base, endpos, pos, endsub, substrg) + break + hexd = ''.join("%02x " % BYTES_ORD(c) for c in substrg) + + chard = '' + for c in substrg: + c = chr(BYTES_ORD(c)) + if c == '\0': + c = '~' + elif not (' ' <= c <= '~'): + c = '?' + chard += c + if numbered: + num_prefix = "%5d: " % (base+pos-ofs) + + fprintf(fout, "%s %-48s %s\n", num_prefix, hexd, chard) + pos = endsub + +def biff_dump(mem, stream_offset, stream_len, base=0, fout=sys.stdout, unnumbered=False): + pos = stream_offset + stream_end = stream_offset + stream_len + adj = base - stream_offset + dummies = 0 + numbered = not unnumbered + num_prefix = '' + while stream_end - pos >= 4: + rc, length = unpack('<HH', mem[pos:pos+4]) + if rc == 0 and length == 0: + if mem[pos:] == b'\0' * (stream_end - pos): + dummies = stream_end - pos + savpos = pos + pos = stream_end + break + if dummies: + dummies += 4 + else: + savpos = pos + dummies = 4 + pos += 4 + else: + if dummies: + if numbered: + num_prefix = "%5d: " % (adj + savpos) + fprintf(fout, "%s---- %d zero bytes skipped ----\n", num_prefix, dummies) + dummies = 0 + recname = biff_rec_name_dict.get(rc, '<UNKNOWN>') + if numbered: + num_prefix = "%5d: " % (adj + pos) + fprintf(fout, "%s%04x %s len = %04x (%d)\n", num_prefix, rc, recname, length, length) + pos += 4 + hex_char_dump(mem, pos, length, adj+pos, fout, unnumbered) + pos += length + if dummies: + if numbered: + num_prefix = "%5d: " % (adj + savpos) + fprintf(fout, "%s---- %d zero bytes skipped ----\n", num_prefix, dummies) + if pos < stream_end: + if numbered: + num_prefix = "%5d: " % (adj + pos) + fprintf(fout, "%s---- Misc bytes at end ----\n", num_prefix) + hex_char_dump(mem, pos, stream_end-pos, adj + pos, fout, unnumbered) + elif pos > stream_end: + fprintf(fout, "Last dumped record has length (%d) that is too large\n", length) + +def biff_count_records(mem, stream_offset, stream_len, fout=sys.stdout): + pos = stream_offset + stream_end = stream_offset + stream_len + tally = {} + while stream_end - pos >= 4: + rc, length = unpack('<HH', mem[pos:pos+4]) + if rc == 0 and length == 0: + if mem[pos:] == b'\0' * (stream_end - pos): + break + recname = "<Dummy (zero)>" + else: + recname = biff_rec_name_dict.get(rc, None) + if recname is None: + recname = "Unknown_0x%04X" % rc + if recname in tally: + tally[recname] += 1 + else: + tally[recname] = 1 + pos += length + 4 + slist = sorted(tally.items()) + for recname, count in slist: + print("%8d %s" % (count, recname), file=fout) + +encoding_from_codepage = { + 1200 : 'utf_16_le', + 10000: 'mac_roman', + 10006: 'mac_greek', # guess + 10007: 'mac_cyrillic', # guess + 10029: 'mac_latin2', # guess + 10079: 'mac_iceland', # guess + 10081: 'mac_turkish', # guess + 32768: 'mac_roman', + 32769: 'cp1252', +} +# some more guessing, for Indic scripts +# codepage 57000 range: +# 2 Devanagari [0] +# 3 Bengali [1] +# 4 Tamil [5] +# 5 Telegu [6] +# 6 Assamese [1] c.f. Bengali +# 7 Oriya [4] +# 8 Kannada [7] +# 9 Malayalam [8] +# 10 Gujarati [3] +# 11 Gurmukhi [2] diff --git a/.venv/lib/python3.12/site-packages/xlrd/book.py b/.venv/lib/python3.12/site-packages/xlrd/book.py new file mode 100644 index 00000000..6876a3e8 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlrd/book.py @@ -0,0 +1,1474 @@ +# Copyright (c) 2005-2012 Stephen John Machin, Lingfo Pty Ltd +# This module is part of the xlrd package, which is released under a +# BSD-style licence. + +from __future__ import print_function + +import struct + +from . import compdoc, formatting, sheet +from .biffh import * +from .formula import * +from .timemachine import * + +try: + from time import perf_counter +except ImportError: + # Python 2.7 + from time import clock as perf_counter + +from struct import unpack + +empty_cell = sheet.empty_cell # for exposure to the world ... + +DEBUG = 0 + +import mmap + +MY_EOF = 0xF00BAAA # not a 16-bit number + +SUPBOOK_UNK, SUPBOOK_INTERNAL, SUPBOOK_EXTERNAL, SUPBOOK_ADDIN, SUPBOOK_DDEOLE = range(5) + +SUPPORTED_VERSIONS = (80, 70, 50, 45, 40, 30, 21, 20) + +_code_from_builtin_name = { + "Consolidate_Area": "\x00", + "Auto_Open": "\x01", + "Auto_Close": "\x02", + "Extract": "\x03", + "Database": "\x04", + "Criteria": "\x05", + "Print_Area": "\x06", + "Print_Titles": "\x07", + "Recorder": "\x08", + "Data_Form": "\x09", + "Auto_Activate": "\x0A", + "Auto_Deactivate": "\x0B", + "Sheet_Title": "\x0C", + "_FilterDatabase": "\x0D", +} +builtin_name_from_code = {} +code_from_builtin_name = {} +for _bin, _bic in _code_from_builtin_name.items(): + _bin = UNICODE_LITERAL(_bin) + _bic = UNICODE_LITERAL(_bic) + code_from_builtin_name[_bin] = _bic + builtin_name_from_code[_bic] = _bin +del _bin, _bic, _code_from_builtin_name + +def open_workbook_xls(filename=None, + logfile=sys.stdout, verbosity=0, use_mmap=True, + file_contents=None, + encoding_override=None, + formatting_info=False, on_demand=False, ragged_rows=False, + ignore_workbook_corruption=False): + t0 = perf_counter() + bk = Book() + try: + bk.biff2_8_load( + filename=filename, file_contents=file_contents, + logfile=logfile, verbosity=verbosity, use_mmap=use_mmap, + encoding_override=encoding_override, + formatting_info=formatting_info, + on_demand=on_demand, + ragged_rows=ragged_rows, + ignore_workbook_corruption=ignore_workbook_corruption + ) + t1 = perf_counter() + bk.load_time_stage_1 = t1 - t0 + biff_version = bk.getbof(XL_WORKBOOK_GLOBALS) + if not biff_version: + raise XLRDError("Can't determine file's BIFF version") + if biff_version not in SUPPORTED_VERSIONS: + raise XLRDError( + "BIFF version %s is not supported" + % biff_text_from_num[biff_version] + ) + bk.biff_version = biff_version + if biff_version <= 40: + # no workbook globals, only 1 worksheet + if on_demand: + fprintf(bk.logfile, + "*** WARNING: on_demand is not supported for this Excel version.\n" + "*** Setting on_demand to False.\n") + bk.on_demand = on_demand = False + bk.fake_globals_get_sheet() + elif biff_version == 45: + # worksheet(s) embedded in global stream + bk.parse_globals() + if on_demand: + fprintf(bk.logfile, "*** WARNING: on_demand is not supported for this Excel version.\n" + "*** Setting on_demand to False.\n") + bk.on_demand = on_demand = False + else: + bk.parse_globals() + bk._sheet_list = [None for sh in bk._sheet_names] + if not on_demand: + bk.get_sheets() + bk.nsheets = len(bk._sheet_list) + if biff_version == 45 and bk.nsheets > 1: + fprintf( + bk.logfile, + "*** WARNING: Excel 4.0 workbook (.XLW) file contains %d worksheets.\n" + "*** Book-level data will be that of the last worksheet.\n", + bk.nsheets + ) + t2 = perf_counter() + bk.load_time_stage_2 = t2 - t1 + except: + bk.release_resources() + raise + # normal exit + if not on_demand: + bk.release_resources() + return bk + + +class Name(BaseObject): + """ + Information relating to a named reference, formula, macro, etc. + + .. note:: + + Name information is **not** extracted from files older than + Excel 5.0 (``Book.biff_version < 50``) + """ + _repr_these = ['stack'] + book = None # parent + + #: 0 = Visible; 1 = Hidden + hidden = 0 + + #: 0 = Command macro; 1 = Function macro. Relevant only if macro == 1 + func = 0 + + #: 0 = Sheet macro; 1 = VisualBasic macro. Relevant only if macro == 1 + vbasic = 0 + + #: 0 = Standard name; 1 = Macro name + macro = 0 + + #: 0 = Simple formula; 1 = Complex formula (array formula or user defined). + #: + #: .. note:: No examples have been sighted. + complex = 0 + + #: 0 = User-defined name; 1 = Built-in name + #: + #: Common examples: ``Print_Area``, ``Print_Titles``; see OOo docs for + #: full list + builtin = 0 + + #: Function group. Relevant only if macro == 1; see OOo docs for values. + funcgroup = 0 + + #: 0 = Formula definition; 1 = Binary data + #: + #: .. note:: No examples have been sighted. + binary = 0 + + #: The index of this object in book.name_obj_list + name_index = 0 + + # A Unicode string. If builtin, decoded as per OOo docs. + name = UNICODE_LITERAL("") + + #: An 8-bit string. + raw_formula = b'' + + #: ``-1``: + #: The name is global (visible in all calculation sheets). + #: ``-2``: + #: The name belongs to a macro sheet or VBA sheet. + #: ``-3``: + #: The name is invalid. + #: ``0 <= scope < book.nsheets``: + #: The name is local to the sheet whose index is scope. + scope = -1 + + #: The result of evaluating the formula, if any. + #: If no formula, or evaluation of the formula encountered problems, + #: the result is ``None``. Otherwise the result is a single instance of the + #: :class:`~xlrd.formula.Operand` class. + # + result = None + + def cell(self): + """ + This is a convenience method for the frequent use case where the name + refers to a single cell. + + :returns: An instance of the :class:`~xlrd.sheet.Cell` class. + + :raises xlrd.biffh.XLRDError: + The name is not a constant absolute reference + to a single cell. + """ + res = self.result + if res: + # result should be an instance of the Operand class + kind = res.kind + value = res.value + if kind == oREF and len(value) == 1: + ref3d = value[0] + if (0 <= ref3d.shtxlo == ref3d.shtxhi - 1 and + ref3d.rowxlo == ref3d.rowxhi - 1 and + ref3d.colxlo == ref3d.colxhi - 1): + sh = self.book.sheet_by_index(ref3d.shtxlo) + return sh.cell(ref3d.rowxlo, ref3d.colxlo) + self.dump( + self.book.logfile, + header="=== Dump of Name object ===", + footer="======= End of dump =======", + ) + raise XLRDError("Not a constant absolute reference to a single cell") + + def area2d(self, clipped=True): + """ + This is a convenience method for the use case where the name + refers to one rectangular area in one worksheet. + + :param clipped: + If ``True``, the default, the returned rectangle is clipped + to fit in ``(0, sheet.nrows, 0, sheet.ncols)``. + it is guaranteed that ``0 <= rowxlo <= rowxhi <= sheet.nrows`` and + that the number of usable rows in the area (which may be zero) is + ``rowxhi - rowxlo``; likewise for columns. + + :returns: a tuple ``(sheet_object, rowxlo, rowxhi, colxlo, colxhi)``. + + :raises xlrd.biffh.XLRDError: + The name is not a constant absolute reference + to a single area in a single sheet. + """ + res = self.result + if res: + # result should be an instance of the Operand class + kind = res.kind + value = res.value + if kind == oREF and len(value) == 1: # only 1 reference + ref3d = value[0] + if 0 <= ref3d.shtxlo == ref3d.shtxhi - 1: # only 1 usable sheet + sh = self.book.sheet_by_index(ref3d.shtxlo) + if not clipped: + return sh, ref3d.rowxlo, ref3d.rowxhi, ref3d.colxlo, ref3d.colxhi + rowxlo = min(ref3d.rowxlo, sh.nrows) + rowxhi = max(rowxlo, min(ref3d.rowxhi, sh.nrows)) + colxlo = min(ref3d.colxlo, sh.ncols) + colxhi = max(colxlo, min(ref3d.colxhi, sh.ncols)) + assert 0 <= rowxlo <= rowxhi <= sh.nrows + assert 0 <= colxlo <= colxhi <= sh.ncols + return sh, rowxlo, rowxhi, colxlo, colxhi + self.dump( + self.book.logfile, + header="=== Dump of Name object ===", + footer="======= End of dump =======", + ) + raise XLRDError("Not a constant absolute reference to a single area in a single sheet") + + +class Book(BaseObject): + """ + Contents of a "workbook". + + .. warning:: + + You should not instantiate this class yourself. You use the :class:`Book` + object that was returned when you called :func:`~xlrd.open_workbook`. + """ + + #: The number of worksheets present in the workbook file. + #: This information is available even when no sheets have yet been loaded. + nsheets = 0 + + #: Which date system was in force when this file was last saved. + #: + #: 0: + #: 1900 system (the Excel for Windows default). + #: + #: 1: + #: 1904 system (the Excel for Macintosh default). + #: + #: Defaults to 0 in case it's not specified in the file. + datemode = 0 + + #: Version of BIFF (Binary Interchange File Format) used to create the file. + #: Latest is 8.0 (represented here as 80), introduced with Excel 97. + #: Earliest supported by this module: 2.0 (represented as 20). + biff_version = 0 + + #: List containing a :class:`Name` object for each ``NAME`` record in the + #: workbook. + #: + #: .. versionadded:: 0.6.0 + name_obj_list = [] + + #: An integer denoting the character set used for strings in this file. + #: For BIFF 8 and later, this will be 1200, meaning Unicode; + #: more precisely, UTF_16_LE. + #: For earlier versions, this is used to derive the appropriate Python + #: encoding to be used to convert to Unicode. + #: Examples: ``1252 -> 'cp1252'``, ``10000 -> 'mac_roman'`` + codepage = None + + #: The encoding that was derived from the codepage. + encoding = None + + #: A tuple containing the telephone country code for: + #: + #: ``[0]``: + #: the user-interface setting when the file was created. + #: + #: ``[1]``: + #: the regional settings. + #: + #: Example: ``(1, 61)`` meaning ``(USA, Australia)``. + #: + #: This information may give a clue to the correct encoding for an + #: unknown codepage. For a long list of observed values, refer to the + #: OpenOffice.org documentation for the ``COUNTRY`` record. + countries = (0, 0) + + #: What (if anything) is recorded as the name of the last user to + #: save the file. + user_name = UNICODE_LITERAL('') + + #: A list of :class:`~xlrd.formatting.Font` class instances, + #: each corresponding to a FONT record. + #: + #: .. versionadded:: 0.6.1 + font_list = [] + + #: A list of :class:`~xlrd.formatting.XF` class instances, + #: each corresponding to an ``XF`` record. + #: + #: .. versionadded:: 0.6.1 + xf_list = [] + + #: A list of :class:`~xlrd.formatting.Format` objects, each corresponding to + #: a ``FORMAT`` record, in the order that they appear in the input file. + #: It does *not* contain builtin formats. + #: + #: If you are creating an output file using (for example) :mod:`xlwt`, + #: use this list. + #: + #: The collection to be used for all visual rendering purposes is + #: :attr:`format_map`. + #: + #: .. versionadded:: 0.6.1 + format_list = [] + + ## + #: The mapping from :attr:`~xlrd.formatting.XF.format_key` to + #: :class:`~xlrd.formatting.Format` object. + #: + #: .. versionadded:: 0.6.1 + format_map = {} + + #: This provides access via name to the extended format information for + #: both built-in styles and user-defined styles. + #: + #: It maps ``name`` to ``(built_in, xf_index)``, where + #: ``name`` is either the name of a user-defined style, + #: or the name of one of the built-in styles. Known built-in names are + #: Normal, RowLevel_1 to RowLevel_7, + #: ColLevel_1 to ColLevel_7, Comma, Currency, Percent, "Comma [0]", + #: "Currency [0]", Hyperlink, and "Followed Hyperlink". + #: + #: ``built_in`` has the following meanings + #: + #: 1: + #: built-in style + #: + #: 0: + #: user-defined + #: + #: ``xf_index`` is an index into :attr:`Book.xf_list`. + #: + #: References: OOo docs s6.99 (``STYLE`` record); Excel UI Format/Style + #: + #: .. versionadded:: 0.6.1 + #: + #: Extracted only if ``open_workbook(..., formatting_info=True)`` + #: + #: .. versionadded:: 0.7.4 + style_name_map = {} + + #: This provides definitions for colour indexes. Please refer to + #: :ref:`palette` for an explanation + #: of how colours are represented in Excel. + #: + #: Colour indexes into the palette map into ``(red, green, blue)`` tuples. + #: "Magic" indexes e.g. ``0x7FFF`` map to ``None``. + #: + #: :attr:`colour_map` is what you need if you want to render cells on screen + #: or in a PDF file. If you are writing an output XLS file, use + #: :attr:`palette_record`. + #: + #: .. note:: Extracted only if ``open_workbook(..., formatting_info=True)`` + #: + #: .. versionadded:: 0.6.1 + colour_map = {} + + #: If the user has changed any of the colours in the standard palette, the + #: XLS file will contain a ``PALETTE`` record with 56 (16 for Excel 4.0 and + #: earlier) RGB values in it, and this list will be e.g. + #: ``[(r0, b0, g0), ..., (r55, b55, g55)]``. + #: Otherwise this list will be empty. This is what you need if you are + #: writing an output XLS file. If you want to render cells on screen or in a + #: PDF file, use :attr:`colour_map`. + #: + #: .. note:: Extracted only if ``open_workbook(..., formatting_info=True)`` + #: + #: .. versionadded:: 0.6.1 + palette_record = [] + + #: Time in seconds to extract the XLS image as a contiguous string + #: (or mmap equivalent). + load_time_stage_1 = -1.0 + + #: Time in seconds to parse the data from the contiguous string + #: (or mmap equivalent). + load_time_stage_2 = -1.0 + + def sheets(self): + """ + :returns: A list of all sheets in the book. + + All sheets not already loaded will be loaded. + """ + for sheetx in xrange(self.nsheets): + if not self._sheet_list[sheetx]: + self.get_sheet(sheetx) + return self._sheet_list[:] + + def sheet_by_index(self, sheetx): + """ + :param sheetx: Sheet index in ``range(nsheets)`` + :returns: A :class:`~xlrd.sheet.Sheet`. + """ + return self._sheet_list[sheetx] or self.get_sheet(sheetx) + + def __iter__(self): + """ + Makes iteration through sheets of a book a little more straightforward. + Don't free resources after use since it can be called like `list(book)` + """ + for i in range(self.nsheets): + yield self.sheet_by_index(i) + + def sheet_by_name(self, sheet_name): + """ + :param sheet_name: Name of the sheet required. + :returns: A :class:`~xlrd.sheet.Sheet`. + """ + try: + sheetx = self._sheet_names.index(sheet_name) + except ValueError: + raise XLRDError('No sheet named <%r>' % sheet_name) + return self.sheet_by_index(sheetx) + + def __getitem__(self, item): + """ + Allow indexing with sheet name or index. + :param item: Name or index of sheet enquired upon + :return: :class:`~xlrd.sheet.Sheet`. + """ + if isinstance(item, int): + return self.sheet_by_index(item) + else: + return self.sheet_by_name(item) + + def sheet_names(self): + """ + :returns: + A list of the names of all the worksheets in the workbook file. + This information is available even when no sheets have yet been + loaded. + """ + return self._sheet_names[:] + + def sheet_loaded(self, sheet_name_or_index): + """ + :param sheet_name_or_index: Name or index of sheet enquired upon + :returns: ``True`` if sheet is loaded, ``False`` otherwise. + + .. versionadded:: 0.7.1 + """ + if isinstance(sheet_name_or_index, int): + sheetx = sheet_name_or_index + else: + try: + sheetx = self._sheet_names.index(sheet_name_or_index) + except ValueError: + raise XLRDError('No sheet named <%r>' % sheet_name_or_index) + return bool(self._sheet_list[sheetx]) + + def unload_sheet(self, sheet_name_or_index): + """ + :param sheet_name_or_index: Name or index of sheet to be unloaded. + + .. versionadded:: 0.7.1 + """ + if isinstance(sheet_name_or_index, int): + sheetx = sheet_name_or_index + else: + try: + sheetx = self._sheet_names.index(sheet_name_or_index) + except ValueError: + raise XLRDError('No sheet named <%r>' % sheet_name_or_index) + self._sheet_list[sheetx] = None + + def release_resources(self): + """ + This method has a dual purpose. You can call it to release + memory-consuming objects and (possibly) a memory-mapped file + (:class:`mmap.mmap` object) when you have finished loading sheets in + ``on_demand`` mode, but still require the :class:`Book` object to + examine the loaded sheets. It is also called automatically (a) when + :func:`~xlrd.open_workbook` + raises an exception and (b) if you are using a ``with`` statement, when + the ``with`` block is exited. Calling this method multiple times on the + same object has no ill effect. + """ + self._resources_released = 1 + if hasattr(self.mem, "close"): + # must be a mmap.mmap object + self.mem.close() + self.mem = None + if hasattr(self.filestr, "close"): + self.filestr.close() + self.filestr = None + self._sharedstrings = None + self._rich_text_runlist_map = None + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, exc_tb): + self.release_resources() + # return false + + #: A mapping from ``(lower_case_name, scope)`` to a single :class:`Name` + #: object. + #: + #: .. versionadded:: 0.6.0 + name_and_scope_map = {} + + #: A mapping from `lower_case_name` to a list of :class:`Name` objects. + #: The list is sorted in scope order. Typically there will be one item + #: (of global scope) in the list. + #: + #: .. versionadded:: 0.6.0 + name_map = {} + + def __init__(self): + self._sheet_list = [] + self._sheet_names = [] + self._sheet_visibility = [] # from BOUNDSHEET record + self.nsheets = 0 + self._sh_abs_posn = [] # sheet's absolute position in the stream + self._sharedstrings = [] + self._rich_text_runlist_map = {} + self.raw_user_name = False + self._sheethdr_count = 0 # BIFF 4W only + self.builtinfmtcount = -1 # unknown as yet. BIFF 3, 4S, 4W + self.initialise_format_info() + self._all_sheets_count = 0 # includes macro & VBA sheets + self._supbook_count = 0 + self._supbook_locals_inx = None + self._supbook_addins_inx = None + self._all_sheets_map = [] # maps an all_sheets index to a calc-sheets index (or -1) + self._externsheet_info = [] + self._externsheet_type_b57 = [] + self._extnsht_name_from_num = {} + self._sheet_num_from_name = {} + self._extnsht_count = 0 + self._supbook_types = [] + self._resources_released = 0 + self.addin_func_names = [] + self.name_obj_list = [] + self.colour_map = {} + self.palette_record = [] + self.xf_list = [] + self.style_name_map = {} + self.mem = b'' + self.filestr = b'' + + def biff2_8_load(self, filename=None, file_contents=None, + logfile=sys.stdout, verbosity=0, use_mmap=True, + encoding_override=None, + formatting_info=False, + on_demand=False, + ragged_rows=False, + ignore_workbook_corruption=False + ): + # DEBUG = 0 + self.logfile = logfile + self.verbosity = verbosity + self.use_mmap = use_mmap + self.encoding_override = encoding_override + self.formatting_info = formatting_info + self.on_demand = on_demand + self.ragged_rows = ragged_rows + + if not file_contents: + with open(filename, "rb") as f: + f.seek(0, 2) # EOF + size = f.tell() + f.seek(0, 0) # BOF + if size == 0: + raise XLRDError("File size is 0 bytes") + if self.use_mmap: + self.filestr = mmap.mmap(f.fileno(), size, access=mmap.ACCESS_READ) + self.stream_len = size + else: + self.filestr = f.read() + self.stream_len = len(self.filestr) + else: + self.filestr = file_contents + self.stream_len = len(file_contents) + + self.base = 0 + if self.filestr[:8] != compdoc.SIGNATURE: + # got this one at the antique store + self.mem = self.filestr + else: + cd = compdoc.CompDoc(self.filestr, logfile=self.logfile, + ignore_workbook_corruption=ignore_workbook_corruption) + for qname in ['Workbook', 'Book']: + self.mem, self.base, self.stream_len = \ + cd.locate_named_stream(UNICODE_LITERAL(qname)) + if self.mem: + break + else: + raise XLRDError("Can't find workbook in OLE2 compound document") + del cd + if self.mem is not self.filestr: + if hasattr(self.filestr, "close"): + self.filestr.close() + self.filestr = b'' + self._position = self.base + if DEBUG: + print("mem: %s, base: %d, len: %d" % (type(self.mem), self.base, self.stream_len), file=self.logfile) + + def initialise_format_info(self): + # needs to be done once per sheet for BIFF 4W :-( + self.format_map = {} + self.format_list = [] + self.xfcount = 0 + self.actualfmtcount = 0 # number of FORMAT records seen so far + self._xf_index_to_xl_type_map = {0: XL_CELL_NUMBER} + self._xf_epilogue_done = 0 + self.xf_list = [] + self.font_list = [] + + def get2bytes(self): + pos = self._position + buff_two = self.mem[pos:pos+2] + lenbuff = len(buff_two) + self._position += lenbuff + if lenbuff < 2: + return MY_EOF + lo, hi = buff_two + return (BYTES_ORD(hi) << 8) | BYTES_ORD(lo) + + def get_record_parts(self): + pos = self._position + mem = self.mem + code, length = unpack('<HH', mem[pos:pos+4]) + pos += 4 + data = mem[pos:pos+length] + self._position = pos + length + return (code, length, data) + + def get_record_parts_conditional(self, reqd_record): + pos = self._position + mem = self.mem + code, length = unpack('<HH', mem[pos:pos+4]) + if code != reqd_record: + return (None, 0, b'') + pos += 4 + data = mem[pos:pos+length] + self._position = pos + length + return (code, length, data) + + def get_sheet(self, sh_number, update_pos=True): + if self._resources_released: + raise XLRDError("Can't load sheets after releasing resources.") + if update_pos: + self._position = self._sh_abs_posn[sh_number] + self.getbof(XL_WORKSHEET) + # assert biff_version == self.biff_version ### FAILS + # Have an example where book is v7 but sheet reports v8!!! + # It appears to work OK if the sheet version is ignored. + # Confirmed by Daniel Rentz: happens when Excel does "save as" + # creating an old version file; ignore version details on sheet BOF. + sh = sheet.Sheet( + self, + self._position, + self._sheet_names[sh_number], + sh_number, + ) + sh.read(self) + self._sheet_list[sh_number] = sh + return sh + + def get_sheets(self): + # DEBUG = 0 + if DEBUG: print("GET_SHEETS:", self._sheet_names, self._sh_abs_posn, file=self.logfile) + for sheetno in xrange(len(self._sheet_names)): + if DEBUG: print("GET_SHEETS: sheetno =", sheetno, self._sheet_names, self._sh_abs_posn, file=self.logfile) + self.get_sheet(sheetno) + + def fake_globals_get_sheet(self): # for BIFF 4.0 and earlier + formatting.initialise_book(self) + fake_sheet_name = UNICODE_LITERAL('Sheet 1') + self._sheet_names = [fake_sheet_name] + self._sh_abs_posn = [0] + self._sheet_visibility = [0] # one sheet, visible + self._sheet_list.append(None) # get_sheet updates _sheet_list but needs a None beforehand + self.get_sheets() + + def handle_boundsheet(self, data): + # DEBUG = 1 + bv = self.biff_version + self.derive_encoding() + if DEBUG: + fprintf(self.logfile, "BOUNDSHEET: bv=%d data %r\n", bv, data) + if bv == 45: # BIFF4W + #### Not documented in OOo docs ... + # In fact, the *only* data is the name of the sheet. + sheet_name = unpack_string(data, 0, self.encoding, lenlen=1) + visibility = 0 + sheet_type = XL_BOUNDSHEET_WORKSHEET # guess, patch later + if len(self._sh_abs_posn) == 0: + abs_posn = self._sheetsoffset + self.base + # Note (a) this won't be used + # (b) it's the position of the SHEETHDR record + # (c) add 11 to get to the worksheet BOF record + else: + abs_posn = -1 # unknown + else: + offset, visibility, sheet_type = unpack('<iBB', data[0:6]) + abs_posn = offset + self.base # because global BOF is always at posn 0 in the stream + if bv < BIFF_FIRST_UNICODE: + sheet_name = unpack_string(data, 6, self.encoding, lenlen=1) + else: + sheet_name = unpack_unicode(data, 6, lenlen=1) + + if DEBUG or self.verbosity >= 2: + fprintf(self.logfile, + "BOUNDSHEET: inx=%d vis=%r sheet_name=%r abs_posn=%d sheet_type=0x%02x\n", + self._all_sheets_count, visibility, sheet_name, abs_posn, sheet_type) + self._all_sheets_count += 1 + if sheet_type != XL_BOUNDSHEET_WORKSHEET: + self._all_sheets_map.append(-1) + descr = { + 1: 'Macro sheet', + 2: 'Chart', + 6: 'Visual Basic module', + }.get(sheet_type, 'UNKNOWN') + + if DEBUG or self.verbosity >= 1: + fprintf(self.logfile, + "NOTE *** Ignoring non-worksheet data named %r (type 0x%02x = %s)\n", + sheet_name, sheet_type, descr) + else: + snum = len(self._sheet_names) + self._all_sheets_map.append(snum) + self._sheet_names.append(sheet_name) + self._sh_abs_posn.append(abs_posn) + self._sheet_visibility.append(visibility) + self._sheet_num_from_name[sheet_name] = snum + + def handle_builtinfmtcount(self, data): + ### N.B. This count appears to be utterly useless. + # DEBUG = 1 + builtinfmtcount = unpack('<H', data[0:2])[0] + if DEBUG: fprintf(self.logfile, "BUILTINFMTCOUNT: %r\n", builtinfmtcount) + self.builtinfmtcount = builtinfmtcount + + def derive_encoding(self): + if self.encoding_override: + self.encoding = self.encoding_override + elif self.codepage is None: + if self.biff_version < 80: + fprintf(self.logfile, + "*** No CODEPAGE record, no encoding_override: will use 'iso-8859-1'\n") + self.encoding = 'iso-8859-1' + else: + self.codepage = 1200 # utf16le + if self.verbosity >= 2: + fprintf(self.logfile, "*** No CODEPAGE record; assuming 1200 (utf_16_le)\n") + else: + codepage = self.codepage + if codepage in encoding_from_codepage: + encoding = encoding_from_codepage[codepage] + elif 300 <= codepage <= 1999: + encoding = 'cp' + str(codepage) + elif self.biff_version >= 80: + self.codepage = 1200 + encoding = 'utf_16_le' + else: + encoding = 'unknown_codepage_' + str(codepage) + if DEBUG or (self.verbosity and encoding != self.encoding) : + fprintf(self.logfile, "CODEPAGE: codepage %r -> encoding %r\n", codepage, encoding) + self.encoding = encoding + if self.codepage != 1200: # utf_16_le + # If we don't have a codec that can decode ASCII into Unicode, + # we're well & truly stuffed -- let the punter know ASAP. + try: + unicode(b'trial', self.encoding) + except BaseException as e: + fprintf(self.logfile, + "ERROR *** codepage %r -> encoding %r -> %s: %s\n", + self.codepage, self.encoding, type(e).__name__.split(".")[-1], e) + raise + if self.raw_user_name: + strg = unpack_string(self.user_name, 0, self.encoding, lenlen=1) + strg = strg.rstrip() + # if DEBUG: + # print "CODEPAGE: user name decoded from %r to %r" % (self.user_name, strg) + self.user_name = strg + self.raw_user_name = False + return self.encoding + + def handle_codepage(self, data): + # DEBUG = 0 + codepage = unpack('<H', data[0:2])[0] + self.codepage = codepage + self.derive_encoding() + + def handle_country(self, data): + countries = unpack('<HH', data[0:4]) + if self.verbosity: print("Countries:", countries, file=self.logfile) + # Note: in BIFF7 and earlier, country record was put (redundantly?) in each worksheet. + assert self.countries == (0, 0) or self.countries == countries + self.countries = countries + + def handle_datemode(self, data): + datemode = unpack('<H', data[0:2])[0] + if DEBUG or self.verbosity: + fprintf(self.logfile, "DATEMODE: datemode %r\n", datemode) + assert datemode in (0, 1) + self.datemode = datemode + + def handle_externname(self, data): + blah = DEBUG or self.verbosity >= 2 + if self.biff_version >= 80: + option_flags, other_info =unpack("<HI", data[:6]) + pos = 6 + name, pos = unpack_unicode_update_pos(data, pos, lenlen=1) + extra = data[pos:] + if self._supbook_types[-1] == SUPBOOK_ADDIN: + self.addin_func_names.append(name) + if blah: + fprintf(self.logfile, + "EXTERNNAME: sbktype=%d oflags=0x%04x oinfo=0x%08x name=%r extra=%r\n", + self._supbook_types[-1], option_flags, other_info, name, extra) + + def handle_externsheet(self, data): + self.derive_encoding() # in case CODEPAGE record missing/out of order/wrong + self._extnsht_count += 1 # for use as a 1-based index + blah1 = DEBUG or self.verbosity >= 1 + blah2 = DEBUG or self.verbosity >= 2 + if self.biff_version >= 80: + num_refs = unpack("<H", data[0:2])[0] + bytes_reqd = num_refs * 6 + 2 + while len(data) < bytes_reqd: + if blah1: + fprintf( + self.logfile, + "INFO: EXTERNSHEET needs %d bytes, have %d\n", + bytes_reqd, len(data), + ) + code2, length2, data2 = self.get_record_parts() + if code2 != XL_CONTINUE: + raise XLRDError("Missing CONTINUE after EXTERNSHEET record") + data += data2 + pos = 2 + for k in xrange(num_refs): + info = unpack("<HHH", data[pos:pos+6]) + ref_recordx, ref_first_sheetx, ref_last_sheetx = info + self._externsheet_info.append(info) + pos += 6 + if blah2: + fprintf( + self.logfile, + "EXTERNSHEET(b8): k = %2d, record = %2d, first_sheet = %5d, last sheet = %5d\n", + k, ref_recordx, ref_first_sheetx, ref_last_sheetx, + ) + else: + nc, ty = unpack("<BB", data[:2]) + if blah2: + print("EXTERNSHEET(b7-):", file=self.logfile) + hex_char_dump(data, 0, len(data), fout=self.logfile) + msg = { + 1: "Encoded URL", + 2: "Current sheet!!", + 3: "Specific sheet in own doc't", + 4: "Nonspecific sheet in own doc't!!", + }.get(ty, "Not encoded") + print(" %3d chars, type is %d (%s)" % (nc, ty, msg), file=self.logfile) + if ty == 3: + sheet_name = unicode(data[2:nc+2], self.encoding) + self._extnsht_name_from_num[self._extnsht_count] = sheet_name + if blah2: print(self._extnsht_name_from_num, file=self.logfile) + if not (1 <= ty <= 4): + ty = 0 + self._externsheet_type_b57.append(ty) + + def handle_filepass(self, data): + if self.verbosity >= 2: + logf = self.logfile + fprintf(logf, "FILEPASS:\n") + hex_char_dump(data, 0, len(data), base=0, fout=logf) + if self.biff_version >= 80: + kind1, = unpack('<H', data[:2]) + if kind1 == 0: # weak XOR encryption + key, hash_value = unpack('<HH', data[2:]) + fprintf(logf, + 'weak XOR: key=0x%04x hash=0x%04x\n', + key, hash_value) + elif kind1 == 1: + kind2, = unpack('<H', data[4:6]) + if kind2 == 1: # BIFF8 standard encryption + caption = "BIFF8 std" + elif kind2 == 2: + caption = "BIFF8 strong" + else: + caption = "** UNKNOWN ENCRYPTION METHOD **" + fprintf(logf, "%s\n", caption) + raise XLRDError("Workbook is encrypted") + + def handle_name(self, data): + blah = DEBUG or self.verbosity >= 2 + bv = self.biff_version + if bv < 50: + return + self.derive_encoding() + # print + # hex_char_dump(data, 0, len(data), fout=self.logfile) + ( + option_flags, kb_shortcut, name_len, fmla_len, extsht_index, sheet_index, + menu_text_len, description_text_len, help_topic_text_len, status_bar_text_len, + ) = unpack("<HBBHHH4B", data[0:14]) + nobj = Name() + nobj.book = self ### CIRCULAR ### + name_index = len(self.name_obj_list) + nobj.name_index = name_index + self.name_obj_list.append(nobj) + nobj.option_flags = option_flags + attrs = [ + ('hidden', 1, 0), + ('func', 2, 1), + ('vbasic', 4, 2), + ('macro', 8, 3), + ('complex', 0x10, 4), + ('builtin', 0x20, 5), + ('funcgroup', 0xFC0, 6), + ('binary', 0x1000, 12), + ] + for attr, mask, nshift in attrs: + setattr(nobj, attr, (option_flags & mask) >> nshift) + + macro_flag = " M"[nobj.macro] + if bv < 80: + internal_name, pos = unpack_string_update_pos(data, 14, self.encoding, known_len=name_len) + else: + internal_name, pos = unpack_unicode_update_pos(data, 14, known_len=name_len) + nobj.extn_sheet_num = extsht_index + nobj.excel_sheet_index = sheet_index + nobj.scope = None # patched up in the names_epilogue() method + if blah: + fprintf( + self.logfile, + "NAME[%d]:%s oflags=%d, name_len=%d, fmla_len=%d, extsht_index=%d, sheet_index=%d, name=%r\n", + name_index, macro_flag, option_flags, name_len, + fmla_len, extsht_index, sheet_index, internal_name) + name = internal_name + if nobj.builtin: + name = builtin_name_from_code.get(name, "??Unknown??") + if blah: print(" builtin: %s" % name, file=self.logfile) + nobj.name = name + nobj.raw_formula = data[pos:] + nobj.basic_formula_len = fmla_len + nobj.evaluated = 0 + if blah: + nobj.dump( + self.logfile, + header="--- handle_name: name[%d] ---" % name_index, + footer="-------------------", + ) + + def names_epilogue(self): + blah = self.verbosity >= 2 + f = self.logfile + if blah: + print("+++++ names_epilogue +++++", file=f) + print("_all_sheets_map", REPR(self._all_sheets_map), file=f) + print("_extnsht_name_from_num", REPR(self._extnsht_name_from_num), file=f) + print("_sheet_num_from_name", REPR(self._sheet_num_from_name), file=f) + num_names = len(self.name_obj_list) + for namex in range(num_names): + nobj = self.name_obj_list[namex] + # Convert from excel_sheet_index to scope. + # This is done here because in BIFF7 and earlier, the + # BOUNDSHEET records (from which _all_sheets_map is derived) + # come after the NAME records. + if self.biff_version >= 80: + sheet_index = nobj.excel_sheet_index + if sheet_index == 0: + intl_sheet_index = -1 # global + elif 1 <= sheet_index <= len(self._all_sheets_map): + intl_sheet_index = self._all_sheets_map[sheet_index-1] + if intl_sheet_index == -1: # maps to a macro or VBA sheet + intl_sheet_index = -2 # valid sheet reference but not useful + else: + # huh? + intl_sheet_index = -3 # invalid + elif 50 <= self.biff_version <= 70: + sheet_index = nobj.extn_sheet_num + if sheet_index == 0: + intl_sheet_index = -1 # global + else: + sheet_name = self._extnsht_name_from_num[sheet_index] + intl_sheet_index = self._sheet_num_from_name.get(sheet_name, -2) + nobj.scope = intl_sheet_index + + for namex in range(num_names): + nobj = self.name_obj_list[namex] + # Parse the formula ... + if nobj.macro or nobj.binary: continue + if nobj.evaluated: continue + evaluate_name_formula(self, nobj, namex, blah=blah) + + if self.verbosity >= 2: + print("---------- name object dump ----------", file=f) + for namex in range(num_names): + nobj = self.name_obj_list[namex] + nobj.dump(f, header="--- name[%d] ---" % namex) + print("--------------------------------------", file=f) + # + # Build some dicts for access to the name objects + # + name_and_scope_map = {} # (name.lower(), scope): Name_object + name_map = {} # name.lower() : list of Name_objects (sorted in scope order) + for namex in range(num_names): + nobj = self.name_obj_list[namex] + name_lcase = nobj.name.lower() + key = (name_lcase, nobj.scope) + if key in name_and_scope_map and self.verbosity: + fprintf(f, 'Duplicate entry %r in name_and_scope_map\n', key) + name_and_scope_map[key] = nobj + sort_data = (nobj.scope, namex, nobj) + # namex (a temp unique ID) ensures the Name objects will not + # be compared (fatal in py3) + if name_lcase in name_map: + name_map[name_lcase].append(sort_data) + else: + name_map[name_lcase] = [sort_data] + for key in name_map.keys(): + alist = name_map[key] + alist.sort() + name_map[key] = [x[2] for x in alist] + self.name_and_scope_map = name_and_scope_map + self.name_map = name_map + + def handle_obj(self, data): + # Not doing much handling at all. + # Worrying about embedded (BOF ... EOF) substreams is done elsewhere. + # DEBUG = 1 + obj_type, obj_id = unpack('<HI', data[4:10]) + # if DEBUG: print "---> handle_obj type=%d id=0x%08x" % (obj_type, obj_id) + + def handle_supbook(self, data): + # aka EXTERNALBOOK in OOo docs + self._supbook_types.append(None) + blah = DEBUG or self.verbosity >= 2 + if blah: + print("SUPBOOK:", file=self.logfile) + hex_char_dump(data, 0, len(data), fout=self.logfile) + num_sheets = unpack("<H", data[0:2])[0] + if blah: print("num_sheets = %d" % num_sheets, file=self.logfile) + sbn = self._supbook_count + self._supbook_count += 1 + if data[2:4] == b"\x01\x04": + self._supbook_types[-1] = SUPBOOK_INTERNAL + self._supbook_locals_inx = self._supbook_count - 1 + if blah: + print("SUPBOOK[%d]: internal 3D refs; %d sheets" % (sbn, num_sheets), file=self.logfile) + print(" _all_sheets_map", self._all_sheets_map, file=self.logfile) + return + if data[0:4] == b"\x01\x00\x01\x3A": + self._supbook_types[-1] = SUPBOOK_ADDIN + self._supbook_addins_inx = self._supbook_count - 1 + if blah: print("SUPBOOK[%d]: add-in functions" % sbn, file=self.logfile) + return + url, pos = unpack_unicode_update_pos(data, 2, lenlen=2) + if num_sheets == 0: + self._supbook_types[-1] = SUPBOOK_DDEOLE + if blah: fprintf(self.logfile, "SUPBOOK[%d]: DDE/OLE document = %r\n", sbn, url) + return + self._supbook_types[-1] = SUPBOOK_EXTERNAL + if blah: fprintf(self.logfile, "SUPBOOK[%d]: url = %r\n", sbn, url) + sheet_names = [] + for x in range(num_sheets): + try: + shname, pos = unpack_unicode_update_pos(data, pos, lenlen=2) + except struct.error: + # #### FIX ME #### + # Should implement handling of CONTINUE record(s) ... + if self.verbosity: + print( + "*** WARNING: unpack failure in sheet %d of %d in SUPBOOK record for file %r" + % (x, num_sheets, url), + file=self.logfile, + ) + break + sheet_names.append(shname) + if blah: fprintf(self.logfile, " sheetx=%d namelen=%d name=%r (next pos=%d)\n", x, len(shname), shname, pos) + + def handle_sheethdr(self, data): + # This a BIFF 4W special. + # The SHEETHDR record is followed by a (BOF ... EOF) substream containing + # a worksheet. + # DEBUG = 1 + self.derive_encoding() + sheet_len = unpack('<i', data[:4])[0] + sheet_name = unpack_string(data, 4, self.encoding, lenlen=1) + sheetno = self._sheethdr_count + assert sheet_name == self._sheet_names[sheetno] + self._sheethdr_count += 1 + BOF_posn = self._position + posn = BOF_posn - 4 - len(data) + if DEBUG: fprintf(self.logfile, 'SHEETHDR %d at posn %d: len=%d name=%r\n', sheetno, posn, sheet_len, sheet_name) + self.initialise_format_info() + if DEBUG: print('SHEETHDR: xf epilogue flag is %d' % self._xf_epilogue_done, file=self.logfile) + self._sheet_list.append(None) # get_sheet updates _sheet_list but needs a None beforehand + self.get_sheet(sheetno, update_pos=False) + if DEBUG: print('SHEETHDR: posn after get_sheet() =', self._position, file=self.logfile) + self._position = BOF_posn + sheet_len + + def handle_sheetsoffset(self, data): + # DEBUG = 0 + posn = unpack('<i', data)[0] + if DEBUG: print('SHEETSOFFSET:', posn, file=self.logfile) + self._sheetsoffset = posn + + def handle_sst(self, data): + # DEBUG = 1 + if DEBUG: + print("SST Processing", file=self.logfile) + t0 = perf_counter() + nbt = len(data) + strlist = [data] + uniquestrings = unpack('<i', data[4:8])[0] + if DEBUG or self.verbosity >= 2: + fprintf(self.logfile, "SST: unique strings: %d\n", uniquestrings) + while 1: + code, nb, data = self.get_record_parts_conditional(XL_CONTINUE) + if code is None: + break + nbt += nb + if DEBUG >= 2: + fprintf(self.logfile, "CONTINUE: adding %d bytes to SST -> %d\n", nb, nbt) + strlist.append(data) + self._sharedstrings, rt_runlist = unpack_SST_table(strlist, uniquestrings) + if self.formatting_info: + self._rich_text_runlist_map = rt_runlist + if DEBUG: + t1 = perf_counter() + print("SST processing took %.2f seconds" % (t1 - t0, ), file=self.logfile) + + def handle_writeaccess(self, data): + DEBUG = 0 + if self.biff_version < 80: + if not self.encoding: + self.raw_user_name = True + self.user_name = data + return + strg = unpack_string(data, 0, self.encoding, lenlen=1) + else: + try: + strg = unpack_unicode(data, 0, lenlen=2) + except UnicodeDecodeError: + # may have invalid trailing characters + strg = unpack_unicode(data.strip(), 0, lenlen=2) + if DEBUG: fprintf(self.logfile, "WRITEACCESS: %d bytes; raw=%s %r\n", len(data), self.raw_user_name, strg) + strg = strg.rstrip() + self.user_name = strg + + def parse_globals(self): + # DEBUG = 0 + # no need to position, just start reading (after the BOF) + formatting.initialise_book(self) + while 1: + rc, length, data = self.get_record_parts() + if DEBUG: print("parse_globals: record code is 0x%04x" % rc, file=self.logfile) + if rc == XL_SST: + self.handle_sst(data) + elif rc == XL_FONT or rc == XL_FONT_B3B4: + self.handle_font(data) + elif rc == XL_FORMAT: # XL_FORMAT2 is BIFF <= 3.0, can't appear in globals + self.handle_format(data) + elif rc == XL_XF: + self.handle_xf(data) + elif rc == XL_BOUNDSHEET: + self.handle_boundsheet(data) + elif rc == XL_DATEMODE: + self.handle_datemode(data) + elif rc == XL_CODEPAGE: + self.handle_codepage(data) + elif rc == XL_COUNTRY: + self.handle_country(data) + elif rc == XL_EXTERNNAME: + self.handle_externname(data) + elif rc == XL_EXTERNSHEET: + self.handle_externsheet(data) + elif rc == XL_FILEPASS: + self.handle_filepass(data) + elif rc == XL_WRITEACCESS: + self.handle_writeaccess(data) + elif rc == XL_SHEETSOFFSET: + self.handle_sheetsoffset(data) + elif rc == XL_SHEETHDR: + self.handle_sheethdr(data) + elif rc == XL_SUPBOOK: + self.handle_supbook(data) + elif rc == XL_NAME: + self.handle_name(data) + elif rc == XL_PALETTE: + self.handle_palette(data) + elif rc == XL_STYLE: + self.handle_style(data) + elif rc & 0xff == 9 and self.verbosity: + fprintf(self.logfile, "*** Unexpected BOF at posn %d: 0x%04x len=%d data=%r\n", + self._position - length - 4, rc, length, data) + elif rc == XL_EOF: + self.xf_epilogue() + self.names_epilogue() + self.palette_epilogue() + if not self.encoding: + self.derive_encoding() + if self.biff_version == 45: + # DEBUG = 0 + if DEBUG: print("global EOF: position", self._position, file=self.logfile) + # if DEBUG: + # pos = self._position - 4 + # print repr(self.mem[pos:pos+40]) + return + else: + # if DEBUG: + # print >> self.logfile, "parse_globals: ignoring record code 0x%04x" % rc + pass + + def read(self, pos, length): + data = self.mem[pos:pos+length] + self._position = pos + len(data) + return data + + def getbof(self, rqd_stream): + # DEBUG = 1 + # if DEBUG: print >> self.logfile, "getbof(): position", self._position + if DEBUG: print("reqd: 0x%04x" % rqd_stream, file=self.logfile) + + def bof_error(msg): + raise XLRDError('Unsupported format, or corrupt file: ' + msg) + savpos = self._position + opcode = self.get2bytes() + if opcode == MY_EOF: + bof_error('Expected BOF record; met end of file') + if opcode not in bofcodes: + bof_error('Expected BOF record; found %r' % self.mem[savpos:savpos+8]) + length = self.get2bytes() + if length == MY_EOF: + bof_error('Incomplete BOF record[1]; met end of file') + if not (4 <= length <= 20): + bof_error( + 'Invalid length (%d) for BOF record type 0x%04x' + % (length, opcode)) + padding = b'\0' * max(0, boflen[opcode] - length) + data = self.read(self._position, length) + if DEBUG: fprintf(self.logfile, "\ngetbof(): data=%r\n", data) + if len(data) < length: + bof_error('Incomplete BOF record[2]; met end of file') + data += padding + version1 = opcode >> 8 + version2, streamtype = unpack('<HH', data[0:4]) + if DEBUG: + print("getbof(): op=0x%04x version2=0x%04x streamtype=0x%04x" + % (opcode, version2, streamtype), file=self.logfile) + bof_offset = self._position - 4 - length + if DEBUG: + print("getbof(): BOF found at offset %d; savpos=%d" + % (bof_offset, savpos), file=self.logfile) + version = build = year = 0 + if version1 == 0x08: + build, year = unpack('<HH', data[4:8]) + if version2 == 0x0600: + version = 80 + elif version2 == 0x0500: + if year < 1994 or build in (2412, 3218, 3321): + version = 50 + else: + version = 70 + else: + # dodgy one, created by a 3rd-party tool + version = { + 0x0000: 21, + 0x0007: 21, + 0x0200: 21, + 0x0300: 30, + 0x0400: 40, + }.get(version2, 0) + elif version1 in (0x04, 0x02, 0x00): + version = {0x04: 40, 0x02: 30, 0x00: 21}[version1] + + if version == 40 and streamtype == XL_WORKBOOK_GLOBALS_4W: + version = 45 # i.e. 4W + + if DEBUG or self.verbosity >= 2: + print("BOF: op=0x%04x vers=0x%04x stream=0x%04x buildid=%d buildyr=%d -> BIFF%d" + % (opcode, version2, streamtype, build, year, version), file=self.logfile) + got_globals = streamtype == XL_WORKBOOK_GLOBALS or ( + version == 45 and streamtype == XL_WORKBOOK_GLOBALS_4W) + if (rqd_stream == XL_WORKBOOK_GLOBALS and got_globals) or streamtype == rqd_stream: + return version + if version < 50 and streamtype == XL_WORKSHEET: + return version + if version >= 50 and streamtype == 0x0100: + bof_error("Workspace file -- no spreadsheet data") + bof_error( + 'BOF not workbook/worksheet: op=0x%04x vers=0x%04x strm=0x%04x build=%d year=%d -> BIFF%d' + % (opcode, version2, streamtype, build, year, version) + ) + +# === helper functions + +def expand_cell_address(inrow, incol): + # Ref : OOo docs, "4.3.4 Cell Addresses in BIFF8" + outrow = inrow + if incol & 0x8000: + if outrow >= 32768: + outrow -= 65536 + relrow = 1 + else: + relrow = 0 + outcol = incol & 0xFF + if incol & 0x4000: + if outcol >= 128: + outcol -= 256 + relcol = 1 + else: + relcol = 0 + return outrow, outcol, relrow, relcol + +def colname(colx, _A2Z="ABCDEFGHIJKLMNOPQRSTUVWXYZ"): + assert colx >= 0 + name = UNICODE_LITERAL('') + while 1: + quot, rem = divmod(colx, 26) + name = _A2Z[rem] + name + if not quot: + return name + colx = quot - 1 + +def display_cell_address(rowx, colx, relrow, relcol): + if relrow: + rowpart = "(*%s%d)" % ("+-"[rowx < 0], abs(rowx)) + else: + rowpart = "$%d" % (rowx+1,) + if relcol: + colpart = "(*%s%d)" % ("+-"[colx < 0], abs(colx)) + else: + colpart = "$" + colname(colx) + return colpart + rowpart + +def unpack_SST_table(datatab, nstrings): + "Return list of strings" + datainx = 0 + ndatas = len(datatab) + data = datatab[0] + datalen = len(data) + pos = 8 + strings = [] + strappend = strings.append + richtext_runs = {} + local_unpack = unpack + local_min = min + local_BYTES_ORD = BYTES_ORD + latin_1 = "latin_1" + for _unused_i in xrange(nstrings): + nchars = local_unpack('<H', data[pos:pos+2])[0] + pos += 2 + options = local_BYTES_ORD(data[pos]) + pos += 1 + rtcount = 0 + phosz = 0 + if options & 0x08: # richtext + rtcount = local_unpack('<H', data[pos:pos+2])[0] + pos += 2 + if options & 0x04: # phonetic + phosz = local_unpack('<i', data[pos:pos+4])[0] + pos += 4 + accstrg = UNICODE_LITERAL('') + charsgot = 0 + while 1: + charsneed = nchars - charsgot + if options & 0x01: + # Uncompressed UTF-16 + charsavail = local_min((datalen - pos) >> 1, charsneed) + rawstrg = data[pos:pos+2*charsavail] + # if DEBUG: print "SST U16: nchars=%d pos=%d rawstrg=%r" % (nchars, pos, rawstrg) + try: + accstrg += unicode(rawstrg, "utf_16_le") + except: + # print "SST U16: nchars=%d pos=%d rawstrg=%r" % (nchars, pos, rawstrg) + # Probable cause: dodgy data e.g. unfinished surrogate pair. + # E.g. file unicode2.xls in pyExcelerator's examples has cells containing + # unichr(i) for i in range(0x100000) + # so this will include 0xD800 etc + raise + pos += 2*charsavail + else: + # Note: this is COMPRESSED (not ASCII!) encoding!!! + charsavail = local_min(datalen - pos, charsneed) + rawstrg = data[pos:pos+charsavail] + # if DEBUG: print "SST CMPRSD: nchars=%d pos=%d rawstrg=%r" % (nchars, pos, rawstrg) + accstrg += unicode(rawstrg, latin_1) + pos += charsavail + charsgot += charsavail + if charsgot == nchars: + break + datainx += 1 + data = datatab[datainx] + datalen = len(data) + options = local_BYTES_ORD(data[0]) + pos = 1 + + if rtcount: + runs = [] + for runindex in xrange(rtcount): + if pos == datalen: + pos = 0 + datainx += 1 + data = datatab[datainx] + datalen = len(data) + runs.append(local_unpack("<HH", data[pos:pos+4])) + pos += 4 + richtext_runs[len(strings)] = runs + + pos += phosz # size of the phonetic stuff to skip + if pos >= datalen: + # adjust to correct position in next record + pos = pos - datalen + datainx += 1 + if datainx < ndatas: + data = datatab[datainx] + datalen = len(data) + else: + assert _unused_i == nstrings - 1 + strappend(accstrg) + return strings, richtext_runs diff --git a/.venv/lib/python3.12/site-packages/xlrd/compdoc.py b/.venv/lib/python3.12/site-packages/xlrd/compdoc.py new file mode 100644 index 00000000..412a89e7 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlrd/compdoc.py @@ -0,0 +1,485 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2005-2012 Stephen John Machin, Lingfo Pty Ltd +# This module is part of the xlrd package, which is released under a +# BSD-style licence. +# No part of the content of this file was derived from the works of +# David Giffin. +""" +Implements the minimal functionality required +to extract a "Workbook" or "Book" stream (as one big string) +from an OLE2 Compound Document file. +""" +from __future__ import print_function + +import array +import sys +from struct import unpack + +from .timemachine import * + +#: Magic cookie that should appear in the first 8 bytes of the file. +SIGNATURE = b"\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1" + +EOCSID = -2 +FREESID = -1 +SATSID = -3 +MSATSID = -4 +EVILSID = -5 + +class CompDocError(Exception): + pass + +class DirNode(object): + + def __init__(self, DID, dent, DEBUG=0, logfile=sys.stdout): + # dent is the 128-byte directory entry + self.DID = DID + self.logfile = logfile + (cbufsize, self.etype, self.colour, self.left_DID, self.right_DID, + self.root_DID) = \ + unpack('<HBBiii', dent[64:80]) + (self.first_SID, self.tot_size) = \ + unpack('<ii', dent[116:124]) + if cbufsize == 0: + self.name = UNICODE_LITERAL('') + else: + self.name = unicode(dent[0:cbufsize-2], 'utf_16_le') # omit the trailing U+0000 + self.children = [] # filled in later + self.parent = -1 # indicates orphan; fixed up later + self.tsinfo = unpack('<IIII', dent[100:116]) + if DEBUG: + self.dump(DEBUG) + + def dump(self, DEBUG=1): + fprintf( + self.logfile, + "DID=%d name=%r etype=%d DIDs(left=%d right=%d root=%d parent=%d kids=%r) first_SID=%d tot_size=%d\n", + self.DID, self.name, self.etype, self.left_DID, + self.right_DID, self.root_DID, self.parent, self.children, self.first_SID, self.tot_size + ) + if DEBUG == 2: + # cre_lo, cre_hi, mod_lo, mod_hi = tsinfo + print("timestamp info", self.tsinfo, file=self.logfile) + +def _build_family_tree(dirlist, parent_DID, child_DID): + if child_DID < 0: return + _build_family_tree(dirlist, parent_DID, dirlist[child_DID].left_DID) + dirlist[parent_DID].children.append(child_DID) + dirlist[child_DID].parent = parent_DID + _build_family_tree(dirlist, parent_DID, dirlist[child_DID].right_DID) + if dirlist[child_DID].etype == 1: # storage + _build_family_tree(dirlist, child_DID, dirlist[child_DID].root_DID) + + +class CompDoc(object): + """ + Compound document handler. + + :param mem: + The raw contents of the file, as a string, or as an :class:`mmap.mmap` + object. The only operation it needs to support is slicing. + """ + + + def __init__(self, mem, logfile=sys.stdout, DEBUG=0, ignore_workbook_corruption=False): + self.logfile = logfile + self.ignore_workbook_corruption = ignore_workbook_corruption + self.DEBUG = DEBUG + if mem[0:8] != SIGNATURE: + raise CompDocError('Not an OLE2 compound document') + if mem[28:30] != b'\xFE\xFF': + raise CompDocError('Expected "little-endian" marker, found %r' % mem[28:30]) + revision, version = unpack('<HH', mem[24:28]) + if DEBUG: + print("\nCompDoc format: version=0x%04x revision=0x%04x" % (version, revision), file=logfile) + self.mem = mem + ssz, sssz = unpack('<HH', mem[30:34]) + if ssz > 20: # allows for 2**20 bytes i.e. 1MB + print("WARNING: sector size (2**%d) is preposterous; assuming 512 and continuing ..." + % ssz, file=logfile) + ssz = 9 + if sssz > ssz: + print("WARNING: short stream sector size (2**%d) is preposterous; assuming 64 and continuing ..." + % sssz, file=logfile) + sssz = 6 + self.sec_size = sec_size = 1 << ssz + self.short_sec_size = 1 << sssz + if self.sec_size != 512 or self.short_sec_size != 64: + print("@@@@ sec_size=%d short_sec_size=%d" % (self.sec_size, self.short_sec_size), file=logfile) + ( + SAT_tot_secs, self.dir_first_sec_sid, _unused, self.min_size_std_stream, + SSAT_first_sec_sid, SSAT_tot_secs, + MSATX_first_sec_sid, MSATX_tot_secs, + ) = unpack('<iiiiiiii', mem[44:76]) + mem_data_len = len(mem) - 512 + mem_data_secs, left_over = divmod(mem_data_len, sec_size) + if left_over: + #### raise CompDocError("Not a whole number of sectors") + mem_data_secs += 1 + print("WARNING *** file size (%d) not 512 + multiple of sector size (%d)" + % (len(mem), sec_size), file=logfile) + self.mem_data_secs = mem_data_secs # use for checking later + self.mem_data_len = mem_data_len + seen = self.seen = array.array('B', [0]) * mem_data_secs + + if DEBUG: + print('sec sizes', ssz, sssz, sec_size, self.short_sec_size, file=logfile) + print("mem data: %d bytes == %d sectors" % (mem_data_len, mem_data_secs), file=logfile) + print("SAT_tot_secs=%d, dir_first_sec_sid=%d, min_size_std_stream=%d" + % (SAT_tot_secs, self.dir_first_sec_sid, self.min_size_std_stream,), file=logfile) + print("SSAT_first_sec_sid=%d, SSAT_tot_secs=%d" % (SSAT_first_sec_sid, SSAT_tot_secs,), file=logfile) + print("MSATX_first_sec_sid=%d, MSATX_tot_secs=%d" % (MSATX_first_sec_sid, MSATX_tot_secs,), file=logfile) + nent = sec_size // 4 # number of SID entries in a sector + fmt = "<%di" % nent + trunc_warned = 0 + # + # === build the MSAT === + # + MSAT = list(unpack('<109i', mem[76:512])) + SAT_sectors_reqd = (mem_data_secs + nent - 1) // nent + expected_MSATX_sectors = max(0, (SAT_sectors_reqd - 109 + nent - 2) // (nent - 1)) + actual_MSATX_sectors = 0 + if MSATX_tot_secs == 0 and MSATX_first_sec_sid in (EOCSID, FREESID, 0): + # Strictly, if there is no MSAT extension, then MSATX_first_sec_sid + # should be set to EOCSID ... FREESID and 0 have been met in the wild. + pass # Presuming no extension + else: + sid = MSATX_first_sec_sid + while sid not in (EOCSID, FREESID, MSATSID): + # Above should be only EOCSID according to MS & OOo docs + # but Excel doesn't complain about FREESID. Zero is a valid + # sector number, not a sentinel. + if DEBUG > 1: + print('MSATX: sid=%d (0x%08X)' % (sid, sid), file=logfile) + if sid >= mem_data_secs: + msg = "MSAT extension: accessing sector %d but only %d in file" % (sid, mem_data_secs) + if DEBUG > 1: + print(msg, file=logfile) + break + raise CompDocError(msg) + elif sid < 0: + raise CompDocError("MSAT extension: invalid sector id: %d" % sid) + if seen[sid]: + raise CompDocError("MSAT corruption: seen[%d] == %d" % (sid, seen[sid])) + seen[sid] = 1 + actual_MSATX_sectors += 1 + if DEBUG and actual_MSATX_sectors > expected_MSATX_sectors: + print("[1]===>>>", mem_data_secs, nent, SAT_sectors_reqd, expected_MSATX_sectors, actual_MSATX_sectors, file=logfile) + offset = 512 + sec_size * sid + MSAT.extend(unpack(fmt, mem[offset:offset+sec_size])) + sid = MSAT.pop() # last sector id is sid of next sector in the chain + + if DEBUG and actual_MSATX_sectors != expected_MSATX_sectors: + print("[2]===>>>", mem_data_secs, nent, SAT_sectors_reqd, expected_MSATX_sectors, actual_MSATX_sectors, file=logfile) + if DEBUG: + print("MSAT: len =", len(MSAT), file=logfile) + dump_list(MSAT, 10, logfile) + # + # === build the SAT === + # + self.SAT = [] + actual_SAT_sectors = 0 + dump_again = 0 + for msidx in xrange(len(MSAT)): + msid = MSAT[msidx] + if msid in (FREESID, EOCSID): + # Specification: the MSAT array may be padded with trailing FREESID entries. + # Toleration: a FREESID or EOCSID entry anywhere in the MSAT array will be ignored. + continue + if msid >= mem_data_secs: + if not trunc_warned: + print("WARNING *** File is truncated, or OLE2 MSAT is corrupt!!", file=logfile) + print("INFO: Trying to access sector %d but only %d available" + % (msid, mem_data_secs), file=logfile) + trunc_warned = 1 + MSAT[msidx] = EVILSID + dump_again = 1 + continue + elif msid < -2: + raise CompDocError("MSAT: invalid sector id: %d" % msid) + if seen[msid]: + raise CompDocError("MSAT extension corruption: seen[%d] == %d" % (msid, seen[msid])) + seen[msid] = 2 + actual_SAT_sectors += 1 + if DEBUG and actual_SAT_sectors > SAT_sectors_reqd: + print("[3]===>>>", mem_data_secs, nent, SAT_sectors_reqd, expected_MSATX_sectors, actual_MSATX_sectors, actual_SAT_sectors, msid, file=logfile) + offset = 512 + sec_size * msid + self.SAT.extend(unpack(fmt, mem[offset:offset+sec_size])) + + if DEBUG: + print("SAT: len =", len(self.SAT), file=logfile) + dump_list(self.SAT, 10, logfile) + # print >> logfile, "SAT ", + # for i, s in enumerate(self.SAT): + # print >> logfile, "entry: %4d offset: %6d, next entry: %4d" % (i, 512 + sec_size * i, s) + # print >> logfile, "%d:%d " % (i, s), + print(file=logfile) + if DEBUG and dump_again: + print("MSAT: len =", len(MSAT), file=logfile) + dump_list(MSAT, 10, logfile) + for satx in xrange(mem_data_secs, len(self.SAT)): + self.SAT[satx] = EVILSID + print("SAT: len =", len(self.SAT), file=logfile) + dump_list(self.SAT, 10, logfile) + # + # === build the directory === + # + dbytes = self._get_stream( + self.mem, 512, self.SAT, self.sec_size, self.dir_first_sec_sid, + name="directory", seen_id=3) + dirlist = [] + did = -1 + for pos in xrange(0, len(dbytes), 128): + did += 1 + dirlist.append(DirNode(did, dbytes[pos:pos+128], 0, logfile)) + self.dirlist = dirlist + _build_family_tree(dirlist, 0, dirlist[0].root_DID) # and stand well back ... + if DEBUG: + for d in dirlist: + d.dump(DEBUG) + # + # === get the SSCS === + # + sscs_dir = self.dirlist[0] + assert sscs_dir.etype == 5 # root entry + if sscs_dir.first_SID < 0 or sscs_dir.tot_size == 0: + # Problem reported by Frank Hoffsuemmer: some software was + # writing -1 instead of -2 (EOCSID) for the first_SID + # when the SCCS was empty. Not having EOCSID caused assertion + # failure in _get_stream. + # Solution: avoid calling _get_stream in any case when the + # SCSS appears to be empty. + self.SSCS = "" + else: + self.SSCS = self._get_stream( + self.mem, 512, self.SAT, sec_size, sscs_dir.first_SID, + sscs_dir.tot_size, name="SSCS", seen_id=4) + # if DEBUG: print >> logfile, "SSCS", repr(self.SSCS) + # + # === build the SSAT === + # + self.SSAT = [] + if SSAT_tot_secs > 0 and sscs_dir.tot_size == 0: + print("WARNING *** OLE2 inconsistency: SSCS size is 0 but SSAT size is non-zero", file=logfile) + if sscs_dir.tot_size > 0: + sid = SSAT_first_sec_sid + nsecs = SSAT_tot_secs + while sid >= 0 and nsecs > 0: + if seen[sid]: + raise CompDocError("SSAT corruption: seen[%d] == %d" % (sid, seen[sid])) + seen[sid] = 5 + nsecs -= 1 + start_pos = 512 + sid * sec_size + news = list(unpack(fmt, mem[start_pos:start_pos+sec_size])) + self.SSAT.extend(news) + sid = self.SAT[sid] + if DEBUG: print("SSAT last sid %d; remaining sectors %d" % (sid, nsecs), file=logfile) + assert nsecs == 0 and sid == EOCSID + if DEBUG: + print("SSAT", file=logfile) + dump_list(self.SSAT, 10, logfile) + if DEBUG: + print("seen", file=logfile) + dump_list(seen, 20, logfile) + + def _get_stream(self, mem, base, sat, sec_size, start_sid, size=None, name='', seen_id=None): + # print >> self.logfile, "_get_stream", base, sec_size, start_sid, size + sectors = [] + s = start_sid + if size is None: + # nothing to check against + while s >= 0: + if seen_id is not None: + if self.seen[s]: + raise CompDocError("%s corruption: seen[%d] == %d" % (name, s, self.seen[s])) + self.seen[s] = seen_id + start_pos = base + s * sec_size + sectors.append(mem[start_pos:start_pos+sec_size]) + try: + s = sat[s] + except IndexError: + raise CompDocError( + "OLE2 stream %r: sector allocation table invalid entry (%d)" % + (name, s) + ) + assert s == EOCSID + else: + todo = size + while s >= 0: + if seen_id is not None: + if self.seen[s]: + raise CompDocError("%s corruption: seen[%d] == %d" % (name, s, self.seen[s])) + self.seen[s] = seen_id + start_pos = base + s * sec_size + grab = sec_size + if grab > todo: + grab = todo + todo -= grab + sectors.append(mem[start_pos:start_pos+grab]) + try: + s = sat[s] + except IndexError: + raise CompDocError( + "OLE2 stream %r: sector allocation table invalid entry (%d)" % + (name, s) + ) + assert s == EOCSID + if todo != 0: + fprintf(self.logfile, + "WARNING *** OLE2 stream %r: expected size %d, actual size %d\n", + name, size, size - todo) + + return b''.join(sectors) + + def _dir_search(self, path, storage_DID=0): + # Return matching DirNode instance, or None + head = path[0] + tail = path[1:] + dl = self.dirlist + for child in dl[storage_DID].children: + if dl[child].name.lower() == head.lower(): + et = dl[child].etype + if et == 2: + return dl[child] + if et == 1: + if not tail: + raise CompDocError("Requested component is a 'storage'") + return self._dir_search(tail, child) + dl[child].dump(1) + raise CompDocError("Requested stream is not a 'user stream'") + return None + + + def get_named_stream(self, qname): + """ + Interrogate the compound document's directory; return the stream as a + string if found, otherwise return ``None``. + + :param qname: + Name of the desired stream e.g. ``'Workbook'``. + Should be in Unicode or convertible thereto. + """ + d = self._dir_search(qname.split("/")) + if d is None: + return None + if d.tot_size >= self.min_size_std_stream: + return self._get_stream( + self.mem, 512, self.SAT, self.sec_size, d.first_SID, + d.tot_size, name=qname, seen_id=d.DID+6) + else: + return self._get_stream( + self.SSCS, 0, self.SSAT, self.short_sec_size, d.first_SID, + d.tot_size, name=qname + " (from SSCS)", seen_id=None) + + def locate_named_stream(self, qname): + """ + Interrogate the compound document's directory. + + If the named stream is not found, ``(None, 0, 0)`` will be returned. + + If the named stream is found and is contiguous within the original + byte sequence (``mem``) used when the document was opened, + then ``(mem, offset_to_start_of_stream, length_of_stream)`` is returned. + + Otherwise a new string is built from the fragments and + ``(new_string, 0, length_of_stream)`` is returned. + + :param qname: + Name of the desired stream e.g. ``'Workbook'``. + Should be in Unicode or convertible thereto. + """ + d = self._dir_search(qname.split("/")) + if d is None: + return (None, 0, 0) + if d.tot_size > self.mem_data_len: + raise CompDocError("%r stream length (%d bytes) > file data size (%d bytes)" + % (qname, d.tot_size, self.mem_data_len)) + if d.tot_size >= self.min_size_std_stream: + result = self._locate_stream( + self.mem, 512, self.SAT, self.sec_size, d.first_SID, + d.tot_size, qname, d.DID+6) + if self.DEBUG: + print("\nseen", file=self.logfile) + dump_list(self.seen, 20, self.logfile) + return result + else: + return ( + self._get_stream( + self.SSCS, 0, self.SSAT, self.short_sec_size, d.first_SID, + d.tot_size, qname + " (from SSCS)", None), + 0, + d.tot_size, + ) + + def _locate_stream(self, mem, base, sat, sec_size, start_sid, expected_stream_size, qname, seen_id): + # print >> self.logfile, "_locate_stream", base, sec_size, start_sid, expected_stream_size + s = start_sid + if s < 0: + raise CompDocError("_locate_stream: start_sid (%d) is -ve" % start_sid) + p = -99 # dummy previous SID + start_pos = -9999 + end_pos = -8888 + slices = [] + tot_found = 0 + found_limit = (expected_stream_size + sec_size - 1) // sec_size + while s >= 0: + if self.seen[s]: + if not self.ignore_workbook_corruption: + print("_locate_stream(%s): seen" % qname, file=self.logfile); dump_list(self.seen, 20, self.logfile) + raise CompDocError("%s corruption: seen[%d] == %d" % (qname, s, self.seen[s])) + self.seen[s] = seen_id + tot_found += 1 + if tot_found > found_limit: + # Note: expected size rounded up to higher sector + raise CompDocError( + "%s: size exceeds expected %d bytes; corrupt?" + % (qname, found_limit * sec_size) + ) + if s == p+1: + # contiguous sectors + end_pos += sec_size + else: + # start new slice + if p >= 0: + # not first time + slices.append((start_pos, end_pos)) + start_pos = base + s * sec_size + end_pos = start_pos + sec_size + p = s + s = sat[s] + assert s == EOCSID + assert tot_found == found_limit + # print >> self.logfile, "_locate_stream(%s): seen" % qname; dump_list(self.seen, 20, self.logfile) + if not slices: + # The stream is contiguous ... just what we like! + return (mem, start_pos, expected_stream_size) + slices.append((start_pos, end_pos)) + # print >> self.logfile, "+++>>> %d fragments" % len(slices) + return (b''.join(mem[start_pos:end_pos] for start_pos, end_pos in slices), 0, expected_stream_size) + +# ========================================================================================== +def x_dump_line(alist, stride, f, dpos, equal=0): + print("%5d%s" % (dpos, " ="[equal]), end=' ', file=f) + for value in alist[dpos:dpos + stride]: + print(str(value), end=' ', file=f) + print(file=f) + +def dump_list(alist, stride, f=sys.stdout): + def _dump_line(dpos, equal=0): + print("%5d%s" % (dpos, " ="[equal]), end=' ', file=f) + for value in alist[dpos:dpos + stride]: + print(str(value), end=' ', file=f) + print(file=f) + pos = None + oldpos = None + for pos in xrange(0, len(alist), stride): + if oldpos is None: + _dump_line(pos) + oldpos = pos + elif alist[pos:pos+stride] != alist[oldpos:oldpos+stride]: + if pos - oldpos > stride: + _dump_line(pos - stride, equal=1) + _dump_line(pos) + oldpos = pos + if oldpos is not None and pos is not None and pos != oldpos: + _dump_line(pos, equal=1) diff --git a/.venv/lib/python3.12/site-packages/xlrd/formatting.py b/.venv/lib/python3.12/site-packages/xlrd/formatting.py new file mode 100644 index 00000000..ca637b81 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlrd/formatting.py @@ -0,0 +1,1324 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2005-2012 Stephen John Machin, Lingfo Pty Ltd +# This module is part of the xlrd package, which is released under a +# BSD-style licence. +# No part of the content of this file was derived from the works of +# David Giffin. +""" +Module for formatting information. +""" + +from __future__ import print_function + +import re +from struct import unpack + +from .biffh import ( + FDT, FGE, FNU, FTX, FUN, XL_CELL_DATE, XL_CELL_NUMBER, XL_CELL_TEXT, + XL_FORMAT, XL_FORMAT2, BaseObject, XLRDError, fprintf, unpack_string, + unpack_unicode, upkbits, upkbitsL, +) +from .timemachine import * + +DEBUG = 0 + +_cellty_from_fmtty = { + FNU: XL_CELL_NUMBER, + FUN: XL_CELL_NUMBER, + FGE: XL_CELL_NUMBER, + FDT: XL_CELL_DATE, + FTX: XL_CELL_NUMBER, # Yes, a number can be formatted as text. +} + +excel_default_palette_b5 = ( + ( 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), (227, 227, 227), + ( 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), +) + +excel_default_palette_b2 = excel_default_palette_b5[:16] + +# Following table borrowed from Gnumeric 1.4 source. +# Checked against OOo docs and MS docs. +excel_default_palette_b8 = ( # (red, green, blue) + ( 0, 0, 0), (255,255,255), (255, 0, 0), ( 0,255, 0), # 0 + ( 0, 0,255), (255,255, 0), (255, 0,255), ( 0,255,255), # 4 + (128, 0, 0), ( 0,128, 0), ( 0, 0,128), (128,128, 0), # 8 + (128, 0,128), ( 0,128,128), (192,192,192), (128,128,128), # 12 + (153,153,255), (153, 51,102), (255,255,204), (204,255,255), # 16 + (102, 0,102), (255,128,128), ( 0,102,204), (204,204,255), # 20 + ( 0, 0,128), (255, 0,255), (255,255, 0), ( 0,255,255), # 24 + (128, 0,128), (128, 0, 0), ( 0,128,128), ( 0, 0,255), # 28 + ( 0,204,255), (204,255,255), (204,255,204), (255,255,153), # 32 + (153,204,255), (255,153,204), (204,153,255), (255,204,153), # 36 + ( 51,102,255), ( 51,204,204), (153,204, 0), (255,204, 0), # 40 + (255,153, 0), (255,102, 0), (102,102,153), (150,150,150), # 44 + ( 0, 51,102), ( 51,153,102), ( 0, 51, 0), ( 51, 51, 0), # 48 + (153, 51, 0), (153, 51,102), ( 51, 51,153), ( 51, 51, 51), # 52 +) + +default_palette = { + 80: excel_default_palette_b8, + 70: excel_default_palette_b5, + 50: excel_default_palette_b5, + 45: excel_default_palette_b2, + 40: excel_default_palette_b2, + 30: excel_default_palette_b2, + 21: excel_default_palette_b2, + 20: excel_default_palette_b2, +} + +# 00H = Normal +# 01H = RowLevel_lv (see next field) +# 02H = ColLevel_lv (see next field) +# 03H = Comma +# 04H = Currency +# 05H = Percent +# 06H = Comma [0] (BIFF4-BIFF8) +# 07H = Currency [0] (BIFF4-BIFF8) +# 08H = Hyperlink (BIFF8) +# 09H = Followed Hyperlink (BIFF8) +built_in_style_names = [ + "Normal", + "RowLevel_", + "ColLevel_", + "Comma", + "Currency", + "Percent", + "Comma [0]", + "Currency [0]", + "Hyperlink", + "Followed Hyperlink", +] + +def initialise_colour_map(book): + book.colour_map = {} + book.colour_indexes_used = {} + if not book.formatting_info: + return + # Add the 8 invariant colours + for i in xrange(8): + book.colour_map[i] = excel_default_palette_b8[i] + # Add the default palette depending on the version + dpal = default_palette[book.biff_version] + ndpal = len(dpal) + for i in xrange(ndpal): + book.colour_map[i+8] = dpal[i] + # Add the specials -- None means the RGB value is not known + # System window text colour for border lines + book.colour_map[ndpal+8] = None + # System window background colour for pattern background + book.colour_map[ndpal+8+1] = None + # System ToolTip text colour (used in note objects) + book.colour_map[0x51] = None + # 32767, system window text colour for fonts + book.colour_map[0x7FFF] = None + + +def nearest_colour_index(colour_map, rgb, debug=0): + """ + General purpose function. Uses Euclidean distance. + So far used only for pre-BIFF8 ``WINDOW2`` record. + Doesn't have to be fast. + Doesn't have to be fancy. + """ + best_metric = 3 * 256 * 256 + best_colourx = 0 + for colourx, cand_rgb in colour_map.items(): + if cand_rgb is None: + continue + metric = 0 + for v1, v2 in zip(rgb, cand_rgb): + metric += (v1 - v2) * (v1 - v2) + if metric < best_metric: + best_metric = metric + best_colourx = colourx + if metric == 0: + break + if 0 and debug: + print("nearest_colour_index for %r is %r -> %r; best_metric is %d" + % (rgb, best_colourx, colour_map[best_colourx], best_metric)) + return best_colourx + +class EqNeAttrs(object): + """ + This mixin class exists solely so that :class:`Format`, :class:`Font`, and + :class:`XF` objects can be compared by value of their attributes. + """ + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + def __ne__(self, other): + return self.__dict__ != other.__dict__ + +class Font(BaseObject, EqNeAttrs): + """ + An Excel "font" contains the details of not only what is normally + considered a font, but also several other display attributes. + Items correspond to those in the Excel UI's Format -> Cells -> Font tab. + + .. versionadded:: 0.6.1 + """ + + #: 1 = Characters are bold. Redundant; see "weight" attribute. + bold = 0 + + #: Values: + #: :: + #: + #: 0 = ANSI Latin + #: 1 = System default + #: 2 = Symbol, + #: 77 = Apple Roman, + #: 128 = ANSI Japanese Shift-JIS, + #: 129 = ANSI Korean (Hangul), + #: 130 = ANSI Korean (Johab), + #: 134 = ANSI Chinese Simplified GBK, + #: 136 = ANSI Chinese Traditional BIG5, + #: 161 = ANSI Greek, + #: 162 = ANSI Turkish, + #: 163 = ANSI Vietnamese, + #: 177 = ANSI Hebrew, + #: 178 = ANSI Arabic, + #: 186 = ANSI Baltic, + #: 204 = ANSI Cyrillic, + #: 222 = ANSI Thai, + #: 238 = ANSI Latin II (Central European), + #: 255 = OEM Latin I + character_set = 0 + + #: An explanation of "colour index" is given in :ref:`palette`. + colour_index = 0 + + #: 1 = Superscript, 2 = Subscript. + escapement = 0 + + #: Values: + #: :: + #: + #: 0 = None (unknown or don't care) + #: 1 = Roman (variable width, serifed) + #: 2 = Swiss (variable width, sans-serifed) + #: 3 = Modern (fixed width, serifed or sans-serifed) + #: 4 = Script (cursive) + #: 5 = Decorative (specialised, for example Old English, Fraktur) + family = 0 + + #: The 0-based index used to refer to this Font() instance. + #: Note that index 4 is never used; xlrd supplies a dummy place-holder. + font_index = 0 + + #: Height of the font (in twips). A twip = 1/20 of a point. + height = 0 + + #: 1 = Characters are italic. + italic = 0 + + #: The name of the font. Example: ``"Arial"``. + name = UNICODE_LITERAL("") + + #: 1 = Characters are struck out. + struck_out = 0 + + #: Values: + #: :: + #: + #: 0 = None + #: 1 = Single; 0x21 (33) = Single accounting + #: 2 = Double; 0x22 (34) = Double accounting + underline_type = 0 + + #: 1 = Characters are underlined. Redundant; see + #: :attr:`underline_type` attribute. + underlined = 0 + + #: Font weight (100-1000). Standard values are 400 for normal text + #: and 700 for bold text. + weight = 400 + + #: 1 = Font is outline style (Macintosh only) + outline = 0 + + #: 1 = Font is shadow style (Macintosh only) + shadow = 0 + +def handle_efont(book, data): # BIFF2 only + if not book.formatting_info: + return + book.font_list[-1].colour_index = unpack('<H', data)[0] + +def handle_font(book, data): + if not book.formatting_info: + return + if not book.encoding: + book.derive_encoding() + blah = DEBUG or book.verbosity >= 2 + bv = book.biff_version + k = len(book.font_list) + if k == 4: + f = Font() + f.name = UNICODE_LITERAL('Dummy Font') + f.font_index = k + book.font_list.append(f) + k += 1 + f = Font() + f.font_index = k + book.font_list.append(f) + if bv >= 50: + ( + f.height, option_flags, f.colour_index, f.weight, + f.escapement, f.underline_type, f.family, + f.character_set, + ) = unpack('<HHHHHBBB', data[0:13]) + f.bold = option_flags & 1 + f.italic = (option_flags & 2) >> 1 + f.underlined = (option_flags & 4) >> 2 + f.struck_out = (option_flags & 8) >> 3 + f.outline = (option_flags & 16) >> 4 + f.shadow = (option_flags & 32) >> 5 + if bv >= 80: + f.name = unpack_unicode(data, 14, lenlen=1) + else: + f.name = unpack_string(data, 14, book.encoding, lenlen=1) + elif bv >= 30: + f.height, option_flags, f.colour_index = unpack('<HHH', data[0:6]) + f.bold = option_flags & 1 + f.italic = (option_flags & 2) >> 1 + f.underlined = (option_flags & 4) >> 2 + f.struck_out = (option_flags & 8) >> 3 + f.outline = (option_flags & 16) >> 4 + f.shadow = (option_flags & 32) >> 5 + f.name = unpack_string(data, 6, book.encoding, lenlen=1) + # Now cook up the remaining attributes ... + f.weight = [400, 700][f.bold] + f.escapement = 0 # None + f.underline_type = f.underlined # None or Single + f.family = 0 # Unknown / don't care + f.character_set = 1 # System default (0 means "ANSI Latin") + else: # BIFF2 + f.height, option_flags = unpack('<HH', data[0:4]) + f.colour_index = 0x7FFF # "system window text colour" + f.bold = option_flags & 1 + f.italic = (option_flags & 2) >> 1 + f.underlined = (option_flags & 4) >> 2 + f.struck_out = (option_flags & 8) >> 3 + f.outline = 0 + f.shadow = 0 + f.name = unpack_string(data, 4, book.encoding, lenlen=1) + # Now cook up the remaining attributes ... + f.weight = [400, 700][f.bold] + f.escapement = 0 # None + f.underline_type = f.underlined # None or Single + f.family = 0 # Unknown / don't care + f.character_set = 1 # System default (0 means "ANSI Latin") + if blah: + f.dump( + book.logfile, + header="--- handle_font: font[%d] ---" % f.font_index, + footer="-------------------", + ) + +# === "Number formats" === + +class Format(BaseObject, EqNeAttrs): + """ + "Number format" information from a ``FORMAT`` record. + + .. versionadded:: 0.6.1 + """ + + #: The key into :attr:`~xlrd.book.Book.format_map` + format_key = 0 + + #: A classification that has been inferred from the format string. + #: Currently, this is used only to distinguish between numbers and dates. + #: Values:: + #: + #: FUN = 0 # unknown + #: FDT = 1 # date + #: FNU = 2 # number + #: FGE = 3 # general + #: FTX = 4 # text + type = FUN + + #: The format string + format_str = UNICODE_LITERAL('') + + def __init__(self, format_key, ty, format_str): + self.format_key = format_key + self.type = ty + self.format_str = format_str + +std_format_strings = { + # "std" == "standard for US English locale" + # #### TODO ... a lot of work to tailor these to the user's locale. + # See e.g. gnumeric-1.x.y/src/formats.c + 0x00: "General", + 0x01: "0", + 0x02: "0.00", + 0x03: "#,##0", + 0x04: "#,##0.00", + 0x05: "$#,##0_);($#,##0)", + 0x06: "$#,##0_);[Red]($#,##0)", + 0x07: "$#,##0.00_);($#,##0.00)", + 0x08: "$#,##0.00_);[Red]($#,##0.00)", + 0x09: "0%", + 0x0a: "0.00%", + 0x0b: "0.00E+00", + 0x0c: "# ?/?", + 0x0d: "# ??/??", + 0x0e: "m/d/yy", + 0x0f: "d-mmm-yy", + 0x10: "d-mmm", + 0x11: "mmm-yy", + 0x12: "h:mm AM/PM", + 0x13: "h:mm:ss AM/PM", + 0x14: "h:mm", + 0x15: "h:mm:ss", + 0x16: "m/d/yy h:mm", + 0x25: "#,##0_);(#,##0)", + 0x26: "#,##0_);[Red](#,##0)", + 0x27: "#,##0.00_);(#,##0.00)", + 0x28: "#,##0.00_);[Red](#,##0.00)", + 0x29: "_(* #,##0_);_(* (#,##0);_(* \"-\"_);_(@_)", + 0x2a: "_($* #,##0_);_($* (#,##0);_($* \"-\"_);_(@_)", + 0x2b: "_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)", + 0x2c: "_($* #,##0.00_);_($* (#,##0.00);_($* \"-\"??_);_(@_)", + 0x2d: "mm:ss", + 0x2e: "[h]:mm:ss", + 0x2f: "mm:ss.0", + 0x30: "##0.0E+0", + 0x31: "@", +} + +fmt_code_ranges = [ # both-inclusive ranges of "standard" format codes + # Source: the openoffice.org doc't + # and the OOXML spec Part 4, section 3.8.30 + ( 0, 0, FGE), + ( 1, 13, FNU), + (14, 22, FDT), + (27, 36, FDT), # CJK date formats + (37, 44, FNU), + (45, 47, FDT), + (48, 48, FNU), + (49, 49, FTX), + # Gnumeric assumes (or assumed) that built-in formats finish at 49, not at 163 + (50, 58, FDT), # CJK date formats + (59, 62, FNU), # Thai number (currency?) formats + (67, 70, FNU), # Thai number (currency?) formats + (71, 81, FDT), # Thai date formats +] + +std_format_code_types = {} +for lo, hi, ty in fmt_code_ranges: + for x in xrange(lo, hi+1): + std_format_code_types[x] = ty +del lo, hi, ty, x + +date_chars = UNICODE_LITERAL('ymdhs') # year, month/minute, day, hour, second +date_char_dict = {} +for _c in date_chars + date_chars.upper(): + date_char_dict[_c] = 5 +del _c, date_chars + +skip_char_dict = {} +for _c in UNICODE_LITERAL('$-+/(): '): + skip_char_dict[_c] = 1 + +num_char_dict = { + UNICODE_LITERAL('0'): 5, + UNICODE_LITERAL('#'): 5, + UNICODE_LITERAL('?'): 5, +} + +non_date_formats = { + UNICODE_LITERAL('0.00E+00'):1, + UNICODE_LITERAL('##0.0E+0'):1, + UNICODE_LITERAL('General') :1, + UNICODE_LITERAL('GENERAL') :1, # OOo Calc 1.1.4 does this. + UNICODE_LITERAL('general') :1, # pyExcelerator 0.6.3 does this. + UNICODE_LITERAL('@') :1, +} + +fmt_bracketed_sub = re.compile(r'\[[^]]*\]').sub + +# Boolean format strings (actual cases) +# '"Yes";"Yes";"No"' +# '"True";"True";"False"' +# '"On";"On";"Off"' + +def is_date_format_string(book, fmt): + # Heuristics: + # Ignore "text" and [stuff in square brackets (aarrgghh -- see below)]. + # Handle backslashed-escaped chars properly. + # E.g. hh\hmm\mss\s should produce a display like 23h59m59s + # Date formats have one or more of ymdhs (caseless) in them. + # Numeric formats have # and 0. + # N.B. 'General"."' hence get rid of "text" first. + # TODO: Find where formats are interpreted in Gnumeric + # TODO: '[h]\\ \\h\\o\\u\\r\\s' ([h] means don't care about hours > 23) + state = 0 + s = '' + + for c in fmt: + if state == 0: + if c == UNICODE_LITERAL('"'): + state = 1 + elif c in UNICODE_LITERAL(r"\_*"): + state = 2 + elif c in skip_char_dict: + pass + else: + s += c + elif state == 1: + if c == UNICODE_LITERAL('"'): + state = 0 + elif state == 2: + # Ignore char after backslash, underscore or asterisk + state = 0 + assert 0 <= state <= 2 + if book.verbosity >= 4: + print("is_date_format_string: reduced format is %s" % REPR(s), file=book.logfile) + s = fmt_bracketed_sub('', s) + if s in non_date_formats: + return False + state = 0 + separator = ";" + got_sep = 0 + date_count = num_count = 0 + for c in s: + if c in date_char_dict: + date_count += date_char_dict[c] + elif c in num_char_dict: + num_count += num_char_dict[c] + elif c == separator: + got_sep = 1 + # print num_count, date_count, repr(fmt) + if date_count and not num_count: + return True + if num_count and not date_count: + return False + if date_count: + if book.verbosity: + fprintf(book.logfile, + 'WARNING *** is_date_format: ambiguous d=%d n=%d fmt=%r\n', + date_count, num_count, fmt) + elif not got_sep: + if book.verbosity: + fprintf(book.logfile, + "WARNING *** format %r produces constant result\n", + fmt) + return date_count > num_count + +def handle_format(self, data, rectype=XL_FORMAT): + DEBUG = 0 + bv = self.biff_version + if rectype == XL_FORMAT2: + bv = min(bv, 30) + if not self.encoding: + self.derive_encoding() + strpos = 2 + if bv >= 50: + fmtkey = unpack('<H', data[0:2])[0] + else: + fmtkey = self.actualfmtcount + if bv <= 30: + strpos = 0 + self.actualfmtcount += 1 + if bv >= 80: + unistrg = unpack_unicode(data, 2) + else: + unistrg = unpack_string(data, strpos, self.encoding, lenlen=1) + blah = DEBUG or self.verbosity >= 3 + if blah: + fprintf(self.logfile, + "FORMAT: count=%d fmtkey=0x%04x (%d) s=%r\n", + self.actualfmtcount, fmtkey, fmtkey, unistrg) + is_date_s = self.is_date_format_string(unistrg) + ty = [FGE, FDT][is_date_s] + if not(fmtkey > 163 or bv < 50): + # user_defined if fmtkey > 163 + # N.B. Gnumeric incorrectly starts these at 50 instead of 164 :-( + # if earlier than BIFF 5, standard info is useless + std_ty = std_format_code_types.get(fmtkey, FUN) + # print "std ty", std_ty + is_date_c = std_ty == FDT + if self.verbosity and 0 < fmtkey < 50 and (is_date_c ^ is_date_s): + DEBUG = 2 + fprintf(self.logfile, + "WARNING *** Conflict between " + "std format key %d and its format string %r\n", + fmtkey, unistrg) + if DEBUG == 2: + fprintf(self.logfile, + "ty: %d; is_date_c: %r; is_date_s: %r; fmt_strg: %r", + ty, is_date_c, is_date_s, unistrg) + fmtobj = Format(fmtkey, ty, unistrg) + if blah: + fmtobj.dump(self.logfile, + header="--- handle_format [%d] ---" % (self.actualfmtcount-1, )) + self.format_map[fmtkey] = fmtobj + self.format_list.append(fmtobj) + +# ============================================================================= + +def handle_palette(book, data): + if not book.formatting_info: + return + blah = DEBUG or book.verbosity >= 2 + n_colours, = unpack('<H', data[:2]) + expected_n_colours = (16, 56)[book.biff_version >= 50] + if (DEBUG or book.verbosity >= 1) and n_colours != expected_n_colours: + fprintf(book.logfile, + "NOTE *** Expected %d colours in PALETTE record, found %d\n", + expected_n_colours, n_colours) + elif blah: + fprintf(book.logfile, + "PALETTE record with %d colours\n", n_colours) + fmt = '<xx%di' % n_colours # use i to avoid long integers + expected_size = 4 * n_colours + 2 + actual_size = len(data) + tolerance = 4 + if not expected_size <= actual_size <= expected_size + tolerance: + raise XLRDError('PALETTE record: expected size %d, actual size %d' % (expected_size, actual_size)) + colours = unpack(fmt, data[:expected_size]) + assert book.palette_record == [] # There should be only 1 PALETTE record + # a colour will be 0xbbggrr + # IOW, red is at the little end + for i in xrange(n_colours): + c = colours[i] + red = c & 0xff + green = (c >> 8) & 0xff + blue = (c >> 16) & 0xff + old_rgb = book.colour_map[8+i] + new_rgb = (red, green, blue) + book.palette_record.append(new_rgb) + book.colour_map[8+i] = new_rgb + if blah: + if new_rgb != old_rgb: + print("%2d: %r -> %r" % (i, old_rgb, new_rgb), file=book.logfile) + +def palette_epilogue(book): + # Check colour indexes in fonts etc. + # This must be done here as FONT records + # come *before* the PALETTE record :-( + for font in book.font_list: + if font.font_index == 4: # the missing font record + continue + cx = font.colour_index + if cx == 0x7fff: # system window text colour + continue + if cx in book.colour_map: + book.colour_indexes_used[cx] = 1 + elif book.verbosity: + print("Size of colour table:", len(book.colour_map), file=book.logfile) + fprintf(book.logfile, "*** Font #%d (%r): colour index 0x%04x is unknown\n", + font.font_index, font.name, cx) + if book.verbosity >= 1: + used = sorted(book.colour_indexes_used.keys()) + print("\nColour indexes used:\n%r\n" % used, file=book.logfile) + +def handle_style(book, data): + if not book.formatting_info: + return + blah = DEBUG or book.verbosity >= 2 + bv = book.biff_version + flag_and_xfx, built_in_id, level = unpack('<HBB', data[:4]) + xf_index = flag_and_xfx & 0x0fff + if data == b"\0\0\0\0" and "Normal" not in book.style_name_map: + # Erroneous record (doesn't have built-in bit set). + # Example file supplied by Jeff Bell. + built_in = 1 + built_in_id = 0 + xf_index = 0 + name = "Normal" + level = 255 + elif flag_and_xfx & 0x8000: + # built-in style + built_in = 1 + name = built_in_style_names[built_in_id] + if 1 <= built_in_id <= 2: + name += str(level + 1) + else: + # user-defined style + built_in = 0 + built_in_id = 0 + level = 0 + if bv >= 80: + try: + name = unpack_unicode(data, 2, lenlen=2) + except UnicodeDecodeError: + print("STYLE: built_in=%d xf_index=%d built_in_id=%d level=%d" + % (built_in, xf_index, built_in_id, level), file=book.logfile) + print("raw bytes:", repr(data[2:]), file=book.logfile) + raise + else: + name = unpack_string(data, 2, book.encoding, lenlen=1) + if blah and not name: + print("WARNING *** A user-defined style has a zero-length name", file=book.logfile) + book.style_name_map[name] = (built_in, xf_index) + if blah: + fprintf(book.logfile, "STYLE: built_in=%d xf_index=%d built_in_id=%d level=%d name=%r\n", + built_in, xf_index, built_in_id, level, name) + +def check_colour_indexes_in_obj(book, obj, orig_index): + alist = sorted(obj.__dict__.items()) + for attr, nobj in alist: + if hasattr(nobj, 'dump'): + check_colour_indexes_in_obj(book, nobj, orig_index) + elif attr.find('colour_index') >= 0: + if nobj in book.colour_map: + book.colour_indexes_used[nobj] = 1 + continue + oname = obj.__class__.__name__ + print("*** xf #%d : %s.%s = 0x%04x (unknown)" + % (orig_index, oname, attr, nobj), file=book.logfile) + +def fill_in_standard_formats(book): + for x in std_format_code_types.keys(): + if x not in book.format_map: + ty = std_format_code_types[x] + # Note: many standard format codes (mostly CJK date formats) have + # format strings that vary by locale; xlrd does not (yet) + # handle those; the type (date or numeric) is recorded but the fmt_str will be None. + fmt_str = std_format_strings.get(x) + fmtobj = Format(x, ty, fmt_str) + book.format_map[x] = fmtobj + +def handle_xf(self, data): + # self is a Book instance + # DEBUG = 0 + blah = DEBUG or self.verbosity >= 3 + bv = self.biff_version + xf = XF() + xf.alignment = XFAlignment() + xf.alignment.indent_level = 0 + xf.alignment.shrink_to_fit = 0 + xf.alignment.text_direction = 0 + xf.border = XFBorder() + xf.border.diag_up = 0 + xf.border.diag_down = 0 + xf.border.diag_colour_index = 0 + xf.border.diag_line_style = 0 # no line + xf.background = XFBackground() + xf.protection = XFProtection() + # fill in the known standard formats + if bv >= 50 and not self.xfcount: + # i.e. do this once before we process the first XF record + fill_in_standard_formats(self) + if bv >= 80: + unpack_fmt = '<HHHBBBBIiH' + ( + xf.font_index, xf.format_key, pkd_type_par, + pkd_align1, xf.alignment.rotation, pkd_align2, + pkd_used, pkd_brdbkg1, pkd_brdbkg2, pkd_brdbkg3, + ) = unpack(unpack_fmt, data[0:20]) + upkbits(xf.protection, pkd_type_par, ( + (0, 0x01, 'cell_locked'), + (1, 0x02, 'formula_hidden'), + )) + upkbits(xf, pkd_type_par, ( + (2, 0x0004, 'is_style'), + # Following is not in OOo docs, but is mentioned + # in Gnumeric source and also in (deep breath) + # org.apache.poi.hssf.record.ExtendedFormatRecord.java + (3, 0x0008, 'lotus_123_prefix'), # Meaning is not known. + (4, 0xFFF0, 'parent_style_index'), + )) + upkbits(xf.alignment, pkd_align1, ( + (0, 0x07, 'hor_align'), + (3, 0x08, 'text_wrapped'), + (4, 0x70, 'vert_align'), + )) + upkbits(xf.alignment, pkd_align2, ( + (0, 0x0f, 'indent_level'), + (4, 0x10, 'shrink_to_fit'), + (6, 0xC0, 'text_direction'), + )) + reg = pkd_used >> 2 + attr_stems = [ + 'format', + 'font', + 'alignment', + 'border', + 'background', + 'protection', + ] + for attr_stem in attr_stems: + attr = "_" + attr_stem + "_flag" + setattr(xf, attr, reg & 1) + reg >>= 1 + upkbitsL(xf.border, pkd_brdbkg1, ( + (0, 0x0000000f, 'left_line_style'), + (4, 0x000000f0, 'right_line_style'), + (8, 0x00000f00, 'top_line_style'), + (12, 0x0000f000, 'bottom_line_style'), + (16, 0x007f0000, 'left_colour_index'), + (23, 0x3f800000, 'right_colour_index'), + (30, 0x40000000, 'diag_down'), + (31, 0x80000000, 'diag_up'), + )) + upkbits(xf.border, pkd_brdbkg2, ( + (0, 0x0000007F, 'top_colour_index'), + (7, 0x00003F80, 'bottom_colour_index'), + (14, 0x001FC000, 'diag_colour_index'), + (21, 0x01E00000, 'diag_line_style'), + )) + upkbitsL(xf.background, pkd_brdbkg2, ( + (26, 0xFC000000, 'fill_pattern'), + )) + upkbits(xf.background, pkd_brdbkg3, ( + (0, 0x007F, 'pattern_colour_index'), + (7, 0x3F80, 'background_colour_index'), + )) + elif bv >= 50: + unpack_fmt = '<HHHBBIi' + ( + xf.font_index, xf.format_key, pkd_type_par, + pkd_align1, pkd_orient_used, + pkd_brdbkg1, pkd_brdbkg2, + ) = unpack(unpack_fmt, data[0:16]) + upkbits(xf.protection, pkd_type_par, ( + (0, 0x01, 'cell_locked'), + (1, 0x02, 'formula_hidden'), + )) + upkbits(xf, pkd_type_par, ( + (2, 0x0004, 'is_style'), + (3, 0x0008, 'lotus_123_prefix'), # Meaning is not known. + (4, 0xFFF0, 'parent_style_index'), + )) + upkbits(xf.alignment, pkd_align1, ( + (0, 0x07, 'hor_align'), + (3, 0x08, 'text_wrapped'), + (4, 0x70, 'vert_align'), + )) + orientation = pkd_orient_used & 0x03 + xf.alignment.rotation = [0, 255, 90, 180][orientation] + reg = pkd_orient_used >> 2 + attr_stems = [ + 'format', + 'font', + 'alignment', + 'border', + 'background', + 'protection', + ] + for attr_stem in attr_stems: + attr = "_" + attr_stem + "_flag" + setattr(xf, attr, reg & 1) + reg >>= 1 + upkbitsL(xf.background, pkd_brdbkg1, ( + ( 0, 0x0000007F, 'pattern_colour_index'), + ( 7, 0x00003F80, 'background_colour_index'), + (16, 0x003F0000, 'fill_pattern'), + )) + upkbitsL(xf.border, pkd_brdbkg1, ( + (22, 0x01C00000, 'bottom_line_style'), + (25, 0xFE000000, 'bottom_colour_index'), + )) + upkbits(xf.border, pkd_brdbkg2, ( + ( 0, 0x00000007, 'top_line_style'), + ( 3, 0x00000038, 'left_line_style'), + ( 6, 0x000001C0, 'right_line_style'), + ( 9, 0x0000FE00, 'top_colour_index'), + (16, 0x007F0000, 'left_colour_index'), + (23, 0x3F800000, 'right_colour_index'), + )) + elif bv >= 40: + unpack_fmt = '<BBHBBHI' + ( + xf.font_index, xf.format_key, pkd_type_par, + pkd_align_orient, pkd_used, + pkd_bkg_34, pkd_brd_34, + ) = unpack(unpack_fmt, data[0:12]) + upkbits(xf.protection, pkd_type_par, ( + (0, 0x01, 'cell_locked'), + (1, 0x02, 'formula_hidden'), + )) + upkbits(xf, pkd_type_par, ( + (2, 0x0004, 'is_style'), + (3, 0x0008, 'lotus_123_prefix'), # Meaning is not known. + (4, 0xFFF0, 'parent_style_index'), + )) + upkbits(xf.alignment, pkd_align_orient, ( + (0, 0x07, 'hor_align'), + (3, 0x08, 'text_wrapped'), + (4, 0x30, 'vert_align'), + )) + orientation = (pkd_align_orient & 0xC0) >> 6 + xf.alignment.rotation = [0, 255, 90, 180][orientation] + reg = pkd_used >> 2 + attr_stems = [ + 'format', + 'font', + 'alignment', + 'border', + 'background', + 'protection', + ] + for attr_stem in attr_stems: + attr = "_" + attr_stem + "_flag" + setattr(xf, attr, reg & 1) + reg >>= 1 + upkbits(xf.background, pkd_bkg_34, ( + ( 0, 0x003F, 'fill_pattern'), + ( 6, 0x07C0, 'pattern_colour_index'), + (11, 0xF800, 'background_colour_index'), + )) + upkbitsL(xf.border, pkd_brd_34, ( + ( 0, 0x00000007, 'top_line_style'), + ( 3, 0x000000F8, 'top_colour_index'), + ( 8, 0x00000700, 'left_line_style'), + (11, 0x0000F800, 'left_colour_index'), + (16, 0x00070000, 'bottom_line_style'), + (19, 0x00F80000, 'bottom_colour_index'), + (24, 0x07000000, 'right_line_style'), + (27, 0xF8000000, 'right_colour_index'), + )) + elif bv == 30: + unpack_fmt = '<BBBBHHI' + ( + xf.font_index, xf.format_key, pkd_type_prot, + pkd_used, pkd_align_par, + pkd_bkg_34, pkd_brd_34, + ) = unpack(unpack_fmt, data[0:12]) + upkbits(xf.protection, pkd_type_prot, ( + (0, 0x01, 'cell_locked'), + (1, 0x02, 'formula_hidden'), + )) + upkbits(xf, pkd_type_prot, ( + (2, 0x0004, 'is_style'), + (3, 0x0008, 'lotus_123_prefix'), # Meaning is not known. + )) + upkbits(xf.alignment, pkd_align_par, ( + (0, 0x07, 'hor_align'), + (3, 0x08, 'text_wrapped'), + )) + upkbits(xf, pkd_align_par, ( + (4, 0xFFF0, 'parent_style_index'), + )) + reg = pkd_used >> 2 + attr_stems = [ + 'format', + 'font', + 'alignment', + 'border', + 'background', + 'protection', + ] + for attr_stem in attr_stems: + attr = "_" + attr_stem + "_flag" + setattr(xf, attr, reg & 1) + reg >>= 1 + upkbits(xf.background, pkd_bkg_34, ( + ( 0, 0x003F, 'fill_pattern'), + ( 6, 0x07C0, 'pattern_colour_index'), + (11, 0xF800, 'background_colour_index'), + )) + upkbitsL(xf.border, pkd_brd_34, ( + ( 0, 0x00000007, 'top_line_style'), + ( 3, 0x000000F8, 'top_colour_index'), + ( 8, 0x00000700, 'left_line_style'), + (11, 0x0000F800, 'left_colour_index'), + (16, 0x00070000, 'bottom_line_style'), + (19, 0x00F80000, 'bottom_colour_index'), + (24, 0x07000000, 'right_line_style'), + (27, 0xF8000000, 'right_colour_index'), + )) + xf.alignment.vert_align = 2 # bottom + xf.alignment.rotation = 0 + elif bv == 21: + ## Warning: incomplete treatment; formatting_info not fully supported. + ## Probably need to offset incoming BIFF2 XF[n] to BIFF8-like XF[n+16], + ## and create XF[0:16] like the standard ones in BIFF8 *AND* add 16 to + ## all XF references in cell records :-( + (xf.font_index, format_etc, halign_etc) = unpack('<BxBB', data) + xf.format_key = format_etc & 0x3F + upkbits(xf.protection, format_etc, ( + (6, 0x40, 'cell_locked'), + (7, 0x80, 'formula_hidden'), + )) + upkbits(xf.alignment, halign_etc, ( + (0, 0x07, 'hor_align'), + )) + for mask, side in ((0x08, 'left'), (0x10, 'right'), (0x20, 'top'), (0x40, 'bottom')): + if halign_etc & mask: + colour_index, line_style = 8, 1 # black, thin + else: + colour_index, line_style = 0, 0 # none, none + setattr(xf.border, side + '_colour_index', colour_index) + setattr(xf.border, side + '_line_style', line_style) + bg = xf.background + if halign_etc & 0x80: + bg.fill_pattern = 17 + else: + bg.fill_pattern = 0 + bg.background_colour_index = 9 # white + bg.pattern_colour_index = 8 # black + xf.parent_style_index = 0 # ??????????? + xf.alignment.vert_align = 2 # bottom + xf.alignment.rotation = 0 + attr_stems = [ + 'format', + 'font', + 'alignment', + 'border', + 'background', + 'protection', + ] + for attr_stem in attr_stems: + attr = "_" + attr_stem + "_flag" + setattr(xf, attr, 1) + else: + raise XLRDError('programmer stuff-up: bv=%d' % bv) + + xf.xf_index = len(self.xf_list) + self.xf_list.append(xf) + self.xfcount += 1 + if blah: + xf.dump( + self.logfile, + header="--- handle_xf: xf[%d] ---" % xf.xf_index, + footer=" ", + ) + try: + fmt = self.format_map[xf.format_key] + cellty = _cellty_from_fmtty[fmt.type] + except KeyError: + cellty = XL_CELL_NUMBER + self._xf_index_to_xl_type_map[xf.xf_index] = cellty + + # Now for some assertions ... + if self.formatting_info: + if self.verbosity and xf.is_style and xf.parent_style_index != 0x0FFF: + msg = "WARNING *** XF[%d] is a style XF but parent_style_index is 0x%04x, not 0x0fff\n" + fprintf(self.logfile, msg, xf.xf_index, xf.parent_style_index) + check_colour_indexes_in_obj(self, xf, xf.xf_index) + if xf.format_key not in self.format_map: + msg = "WARNING *** XF[%d] unknown (raw) format key (%d, 0x%04x)\n" + if self.verbosity: + fprintf(self.logfile, msg, + xf.xf_index, xf.format_key, xf.format_key) + xf.format_key = 0 + +def xf_epilogue(self): + # self is a Book instance. + self._xf_epilogue_done = 1 + num_xfs = len(self.xf_list) + blah = DEBUG or self.verbosity >= 3 + blah1 = DEBUG or self.verbosity >= 1 + if blah: + fprintf(self.logfile, "xf_epilogue called ...\n") + + def check_same(book_arg, xf_arg, parent_arg, attr): + # the _arg caper is to avoid a Warning msg from Python 2.1 :-( + if getattr(xf_arg, attr) != getattr(parent_arg, attr): + fprintf(book_arg.logfile, + "NOTE !!! XF[%d] parent[%d] %s different\n", + xf_arg.xf_index, parent_arg.xf_index, attr) + + for xfx in xrange(num_xfs): + xf = self.xf_list[xfx] + + try: + fmt = self.format_map[xf.format_key] + cellty = _cellty_from_fmtty[fmt.type] + except KeyError: + cellty = XL_CELL_TEXT + self._xf_index_to_xl_type_map[xf.xf_index] = cellty + # Now for some assertions etc + if not self.formatting_info: + continue + if xf.is_style: + continue + if not(0 <= xf.parent_style_index < num_xfs): + if blah1: + fprintf(self.logfile, + "WARNING *** XF[%d]: is_style=%d but parent_style_index=%d\n", + xf.xf_index, xf.is_style, xf.parent_style_index) + # make it conform + xf.parent_style_index = 0 + if self.biff_version >= 30: + if blah1: + if xf.parent_style_index == xf.xf_index: + fprintf(self.logfile, + "NOTE !!! XF[%d]: parent_style_index is also %d\n", + xf.xf_index, xf.parent_style_index) + elif not self.xf_list[xf.parent_style_index].is_style: + fprintf(self.logfile, + "NOTE !!! XF[%d]: parent_style_index is %d; style flag not set\n", + xf.xf_index, xf.parent_style_index) + if blah1 and xf.parent_style_index > xf.xf_index: + fprintf(self.logfile, + "NOTE !!! XF[%d]: parent_style_index is %d; out of order?\n", + xf.xf_index, xf.parent_style_index) + parent = self.xf_list[xf.parent_style_index] + if not xf._alignment_flag and not parent._alignment_flag: + if blah1: check_same(self, xf, parent, 'alignment') + if not xf._background_flag and not parent._background_flag: + if blah1: check_same(self, xf, parent, 'background') + if not xf._border_flag and not parent._border_flag: + if blah1: check_same(self, xf, parent, 'border') + if not xf._protection_flag and not parent._protection_flag: + if blah1: check_same(self, xf, parent, 'protection') + if not xf._format_flag and not parent._format_flag: + if blah1 and xf.format_key != parent.format_key: + fprintf(self.logfile, + "NOTE !!! XF[%d] fmtk=%d, parent[%d] fmtk=%r\n%r / %r\n", + xf.xf_index, xf.format_key, parent.xf_index, parent.format_key, + self.format_map[xf.format_key].format_str, + self.format_map[parent.format_key].format_str) + if not xf._font_flag and not parent._font_flag: + if blah1 and xf.font_index != parent.font_index: + fprintf(self.logfile, + "NOTE !!! XF[%d] fontx=%d, parent[%d] fontx=%r\n", + xf.xf_index, xf.font_index, parent.xf_index, parent.font_index) + +def initialise_book(book): + initialise_colour_map(book) + book._xf_epilogue_done = 0 + methods = ( + handle_font, + handle_efont, + handle_format, + is_date_format_string, + handle_palette, + palette_epilogue, + handle_style, + handle_xf, + xf_epilogue, + ) + for method in methods: + setattr(book.__class__, method.__name__, method) + +class XFBorder(BaseObject, EqNeAttrs): + """ + A collection of the border-related attributes of an ``XF`` record. + Items correspond to those in the Excel UI's Format -> Cells -> Border tab. + + An explanations of "colour index" is given in :ref:`palette`. + + There are five line style attributes; possible values and the + associated meanings are:: + + 0 = No line, + 1 = Thin, + 2 = Medium, + 3 = Dashed, + 4 = Dotted, + 5 = Thick, + 6 = Double, + 7 = Hair, + 8 = Medium dashed, + 9 = Thin dash-dotted, + 10 = Medium dash-dotted, + 11 = Thin dash-dot-dotted, + 12 = Medium dash-dot-dotted, + 13 = Slanted medium dash-dotted. + + The line styles 8 to 13 appear in BIFF8 files (Excel 97 and later) only. + For pictures of the line styles, refer to OOo docs s3.10 (p22) + "Line Styles for Cell Borders (BIFF3-BIFF8)".</p> + + .. versionadded:: 0.6.1 + """ + + #: The colour index for the cell's top line + top_colour_index = 0 + #: The colour index for the cell's bottom line + bottom_colour_index = 0 + + #: The colour index for the cell's left line + left_colour_index = 0 + + #: The colour index for the cell's right line + right_colour_index = 0 + + #: The colour index for the cell's diagonal lines, if any + diag_colour_index = 0 + + #: The line style for the cell's top line + top_line_style = 0 + + #: The line style for the cell's bottom line + bottom_line_style = 0 + + #: The line style for the cell's left line + left_line_style = 0 + + #: The line style for the cell's right line + right_line_style = 0 + + #: The line style for the cell's diagonal lines, if any + diag_line_style = 0 + + #: 1 = draw a diagonal from top left to bottom right + diag_down = 0 + + #: 1 = draw a diagonal from bottom left to top right + diag_up = 0 + +class XFBackground(BaseObject, EqNeAttrs): + """ + A collection of the background-related attributes of an ``XF`` record. + Items correspond to those in the Excel UI's Format -> Cells -> Patterns tab. + + An explanations of "colour index" is given in :ref:`palette`. + + .. versionadded:: 0.6.1 + """ + + #: See section 3.11 of the OOo docs. + fill_pattern = 0 + + #: See section 3.11 of the OOo docs. + background_colour_index = 0 + + #: See section 3.11 of the OOo docs. + pattern_colour_index = 0 + + +class XFAlignment(BaseObject, EqNeAttrs): + """ + A collection of the alignment and similar attributes of an ``XF`` record. + Items correspond to those in the Excel UI's Format -> Cells -> Alignment tab. + + .. versionadded:: 0.6.1 + """ + + #: Values: section 6.115 (p 214) of OOo docs + hor_align = 0 + + #: Values: section 6.115 (p 215) of OOo docs + vert_align = 0 + + #: Values: section 6.115 (p 215) of OOo docs. + #: + #: .. note:: + #: file versions BIFF7 and earlier use the documented + #: :attr:`orientation` attribute; this will be mapped (without loss) + #: into :attr:`rotation`. + rotation = 0 + + #: 1 = text is wrapped at right margin + text_wrapped = 0 + + #: A number in ``range(15)``. + indent_level = 0 + + #: 1 = shrink font size to fit text into cell. + shrink_to_fit = 0 + + #: 0 = according to context; 1 = left-to-right; 2 = right-to-left + text_direction = 0 + +class XFProtection(BaseObject, EqNeAttrs): + """ + A collection of the protection-related attributes of an ``XF`` record. + Items correspond to those in the Excel UI's Format -> Cells -> Protection tab. + Note the OOo docs include the "cell or style" bit in this bundle of + attributes. This is incorrect; the bit is used in determining which bundles + to use. + + .. versionadded:: 0.6.1 + """ + + #: 1 = Cell is prevented from being changed, moved, resized, or deleted + #: (only if the sheet is protected). + cell_locked = 0 + + #: 1 = Hide formula so that it doesn't appear in the formula bar when + #: the cell is selected (only if the sheet is protected). + formula_hidden = 0 + +class XF(BaseObject): + """ + eXtended Formatting information for cells, rows, columns and styles. + + Each of the 6 flags below describes the validity of + a specific group of attributes. + + In cell XFs: + + - ``flag==0`` means the attributes of the parent style ``XF`` are + used, (but only if the attributes are valid there); + + - ``flag==1`` means the attributes of this ``XF`` are used. + + In style XFs: + + - ``flag==0`` means the attribute setting is valid; + - ``flag==1`` means the attribute should be ignored. + + .. note:: + the API provides both "raw" XFs and "computed" XFs. In the latter case, + cell XFs have had the above inheritance mechanism applied. + + .. versionadded:: 0.6.1 + """ + + #: 0 = cell XF, 1 = style XF + is_style = 0 + + #: cell XF: Index into Book.xf_list of this XF's style XF + #: + #: style XF: 0xFFF + parent_style_index = 0 + + # + _format_flag = 0 + + # + _font_flag = 0 + + # + _alignment_flag = 0 + + # + _border_flag = 0 + + # + _background_flag = 0 + + _protection_flag = 0 + + #: Index into :attr:`~xlrd.book.Book.xf_list` + xf_index = 0 + + #: Index into :attr:`~xlrd.book.Book.font_list` + font_index = 0 + + #: Key into :attr:`~xlrd.book.Book.format_map` + #: + #: .. warning:: + #: OOo docs on the XF record call this "Index to FORMAT record". + #: It is not an index in the Python sense. It is a key to a map. + #: It is true *only* for Excel 4.0 and earlier files + #: that the key into format_map from an XF instance + #: is the same as the index into format_list, and *only* + #: if the index is less than 164. + format_key = 0 + + #: An instance of an :class:`XFProtection` object. + protection = None + + #: An instance of an :class:`XFBackground` object. + background = None + + #: An instance of an :class:`XFAlignment` object. + alignment = None + + #: An instance of an :class:`XFBorder` object. + border = None diff --git a/.venv/lib/python3.12/site-packages/xlrd/formula.py b/.venv/lib/python3.12/site-packages/xlrd/formula.py new file mode 100644 index 00000000..e26639b9 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlrd/formula.py @@ -0,0 +1,2190 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2005-2012 Stephen John Machin, Lingfo Pty Ltd +# This module is part of the xlrd package, which is released under a +# BSD-style licence. +# No part of the content of this file was derived from the works of +# David Giffin. +""" +Module for parsing/evaluating Microsoft Excel formulas. +""" + +from __future__ import print_function + +import copy +import operator as opr +from struct import unpack + +from .biffh import ( + BaseObject, XLRDError, error_text_from_code, hex_char_dump, + unpack_string_update_pos, unpack_unicode_update_pos, +) +from .timemachine import * + +__all__ = [ + 'oBOOL', 'oERR', 'oNUM', 'oREF', 'oREL', 'oSTRG', 'oUNK', + 'decompile_formula', + 'dump_formula', + 'evaluate_name_formula', + 'okind_dict', + 'rangename3d', 'rangename3drel', 'cellname', 'cellnameabs', 'colname', + 'FMLA_TYPE_CELL', + 'FMLA_TYPE_SHARED', + 'FMLA_TYPE_ARRAY', + 'FMLA_TYPE_COND_FMT', + 'FMLA_TYPE_DATA_VAL', + 'FMLA_TYPE_NAME', + 'Operand', 'Ref3D', +] + +FMLA_TYPE_CELL = 1 +FMLA_TYPE_SHARED = 2 +FMLA_TYPE_ARRAY = 4 +FMLA_TYPE_COND_FMT = 8 +FMLA_TYPE_DATA_VAL = 16 +FMLA_TYPE_NAME = 32 +ALL_FMLA_TYPES = 63 + + +FMLA_TYPEDESCR_MAP = { + 1 : 'CELL', + 2 : 'SHARED', + 4 : 'ARRAY', + 8 : 'COND-FMT', + 16: 'DATA-VAL', + 32: 'NAME', +} + +_TOKEN_NOT_ALLOWED = { + 0x01: ALL_FMLA_TYPES - FMLA_TYPE_CELL, # tExp + 0x02: ALL_FMLA_TYPES - FMLA_TYPE_CELL, # tTbl + 0x0F: FMLA_TYPE_SHARED + FMLA_TYPE_COND_FMT + FMLA_TYPE_DATA_VAL, # tIsect + 0x10: FMLA_TYPE_SHARED + FMLA_TYPE_COND_FMT + FMLA_TYPE_DATA_VAL, # tUnion/List + 0x11: FMLA_TYPE_SHARED + FMLA_TYPE_COND_FMT + FMLA_TYPE_DATA_VAL, # tRange + 0x20: FMLA_TYPE_SHARED + FMLA_TYPE_COND_FMT + FMLA_TYPE_DATA_VAL, # tArray + 0x23: FMLA_TYPE_SHARED, # tName + 0x39: FMLA_TYPE_SHARED + FMLA_TYPE_COND_FMT + FMLA_TYPE_DATA_VAL, # tNameX + 0x3A: FMLA_TYPE_SHARED + FMLA_TYPE_COND_FMT + FMLA_TYPE_DATA_VAL, # tRef3d + 0x3B: FMLA_TYPE_SHARED + FMLA_TYPE_COND_FMT + FMLA_TYPE_DATA_VAL, # tArea3d + 0x2C: FMLA_TYPE_CELL + FMLA_TYPE_ARRAY, # tRefN + 0x2D: FMLA_TYPE_CELL + FMLA_TYPE_ARRAY, # tAreaN + # plus weird stuff like tMem* +}.get + +oBOOL = 3 +oERR = 4 +oMSNG = 5 # tMissArg +oNUM = 2 +oREF = -1 +oREL = -2 +oSTRG = 1 +oUNK = 0 + +okind_dict = { + -2: "oREL", + -1: "oREF", + 0 : "oUNK", + 1 : "oSTRG", + 2 : "oNUM", + 3 : "oBOOL", + 4 : "oERR", + 5 : "oMSNG", +} + +listsep = ',' #### probably should depend on locale + + +# sztabN[opcode] -> the number of bytes to consume. +# -1 means variable +# -2 means this opcode not implemented in this version. +# Which N to use? Depends on biff_version; see szdict. +sztab0 = [-2, 4, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -1, -2, -1, 8, 4, 2, 2, 3, 9, 8, 2, 3, 8, 4, 7, 5, 5, 5, 2, 4, 7, 4, 7, 2, 2, -2, -2, -2, -2, -2, -2, -2, -2, 3, -2, -2, -2, -2, -2, -2, -2] +sztab1 = [-2, 5, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -1, -2, -1, 11, 5, 2, 2, 3, 9, 9, 2, 3, 11, 4, 7, 7, 7, 7, 3, 4, 7, 4, 7, 3, 3, -2, -2, -2, -2, -2, -2, -2, -2, 3, -2, -2, -2, -2, -2, -2, -2] +sztab2 = [-2, 5, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -1, -2, -1, 11, 5, 2, 2, 3, 9, 9, 3, 4, 11, 4, 7, 7, 7, 7, 3, 4, 7, 4, 7, 3, 3, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2] +sztab3 = [-2, 5, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -1, -2, -1, -2, -2, 2, 2, 3, 9, 9, 3, 4, 15, 4, 7, 7, 7, 7, 3, 4, 7, 4, 7, 3, 3, -2, -2, -2, -2, -2, -2, -2, -2, -2, 25, 18, 21, 18, 21, -2, -2] +sztab4 = [-2, 5, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -2, -2, 2, 2, 3, 9, 9, 3, 4, 5, 5, 9, 7, 7, 7, 3, 5, 9, 5, 9, 3, 3, -2, -2, -2, -2, -2, -2, -2, -2, -2, 7, 7, 11, 7, 11, -2, -2] + +szdict = { + 20 : sztab0, + 21 : sztab0, + 30 : sztab1, + 40 : sztab2, + 45 : sztab2, + 50 : sztab3, + 70 : sztab3, + 80 : sztab4, +} + +# For debugging purposes ... the name for each opcode +# (without the prefix "t" used on OOo docs) +onames = ['Unk00', 'Exp', 'Tbl', 'Add', 'Sub', 'Mul', 'Div', 'Power', 'Concat', 'LT', 'LE', 'EQ', 'GE', 'GT', 'NE', 'Isect', 'List', 'Range', 'Uplus', 'Uminus', 'Percent', 'Paren', 'MissArg', 'Str', 'Extended', 'Attr', 'Sheet', 'EndSheet', 'Err', 'Bool', 'Int', 'Num', 'Array', 'Func', 'FuncVar', 'Name', 'Ref', 'Area', 'MemArea', 'MemErr', 'MemNoMem', 'MemFunc', 'RefErr', 'AreaErr', 'RefN', 'AreaN', 'MemAreaN', 'MemNoMemN', '', '', '', '', '', '', '', '', 'FuncCE', 'NameX', 'Ref3d', 'Area3d', 'RefErr3d', 'AreaErr3d', '', ''] + +func_defs = { + # index: (name, min#args, max#args, flags, #known_args, return_type, kargs) + 0 : ('COUNT', 0, 30, 0x04, 1, 'V', 'R'), + 1 : ('IF', 2, 3, 0x04, 3, 'V', 'VRR'), + 2 : ('ISNA', 1, 1, 0x02, 1, 'V', 'V'), + 3 : ('ISERROR', 1, 1, 0x02, 1, 'V', 'V'), + 4 : ('SUM', 0, 30, 0x04, 1, 'V', 'R'), + 5 : ('AVERAGE', 1, 30, 0x04, 1, 'V', 'R'), + 6 : ('MIN', 1, 30, 0x04, 1, 'V', 'R'), + 7 : ('MAX', 1, 30, 0x04, 1, 'V', 'R'), + 8 : ('ROW', 0, 1, 0x04, 1, 'V', 'R'), + 9 : ('COLUMN', 0, 1, 0x04, 1, 'V', 'R'), + 10 : ('NA', 0, 0, 0x02, 0, 'V', ''), + 11 : ('NPV', 2, 30, 0x04, 2, 'V', 'VR'), + 12 : ('STDEV', 1, 30, 0x04, 1, 'V', 'R'), + 13 : ('DOLLAR', 1, 2, 0x04, 1, 'V', 'V'), + 14 : ('FIXED', 2, 3, 0x04, 3, 'V', 'VVV'), + 15 : ('SIN', 1, 1, 0x02, 1, 'V', 'V'), + 16 : ('COS', 1, 1, 0x02, 1, 'V', 'V'), + 17 : ('TAN', 1, 1, 0x02, 1, 'V', 'V'), + 18 : ('ATAN', 1, 1, 0x02, 1, 'V', 'V'), + 19 : ('PI', 0, 0, 0x02, 0, 'V', ''), + 20 : ('SQRT', 1, 1, 0x02, 1, 'V', 'V'), + 21 : ('EXP', 1, 1, 0x02, 1, 'V', 'V'), + 22 : ('LN', 1, 1, 0x02, 1, 'V', 'V'), + 23 : ('LOG10', 1, 1, 0x02, 1, 'V', 'V'), + 24 : ('ABS', 1, 1, 0x02, 1, 'V', 'V'), + 25 : ('INT', 1, 1, 0x02, 1, 'V', 'V'), + 26 : ('SIGN', 1, 1, 0x02, 1, 'V', 'V'), + 27 : ('ROUND', 2, 2, 0x02, 2, 'V', 'VV'), + 28 : ('LOOKUP', 2, 3, 0x04, 2, 'V', 'VR'), + 29 : ('INDEX', 2, 4, 0x0c, 4, 'R', 'RVVV'), + 30 : ('REPT', 2, 2, 0x02, 2, 'V', 'VV'), + 31 : ('MID', 3, 3, 0x02, 3, 'V', 'VVV'), + 32 : ('LEN', 1, 1, 0x02, 1, 'V', 'V'), + 33 : ('VALUE', 1, 1, 0x02, 1, 'V', 'V'), + 34 : ('TRUE', 0, 0, 0x02, 0, 'V', ''), + 35 : ('FALSE', 0, 0, 0x02, 0, 'V', ''), + 36 : ('AND', 1, 30, 0x04, 1, 'V', 'R'), + 37 : ('OR', 1, 30, 0x04, 1, 'V', 'R'), + 38 : ('NOT', 1, 1, 0x02, 1, 'V', 'V'), + 39 : ('MOD', 2, 2, 0x02, 2, 'V', 'VV'), + 40 : ('DCOUNT', 3, 3, 0x02, 3, 'V', 'RRR'), + 41 : ('DSUM', 3, 3, 0x02, 3, 'V', 'RRR'), + 42 : ('DAVERAGE', 3, 3, 0x02, 3, 'V', 'RRR'), + 43 : ('DMIN', 3, 3, 0x02, 3, 'V', 'RRR'), + 44 : ('DMAX', 3, 3, 0x02, 3, 'V', 'RRR'), + 45 : ('DSTDEV', 3, 3, 0x02, 3, 'V', 'RRR'), + 46 : ('VAR', 1, 30, 0x04, 1, 'V', 'R'), + 47 : ('DVAR', 3, 3, 0x02, 3, 'V', 'RRR'), + 48 : ('TEXT', 2, 2, 0x02, 2, 'V', 'VV'), + 49 : ('LINEST', 1, 4, 0x04, 4, 'A', 'RRVV'), + 50 : ('TREND', 1, 4, 0x04, 4, 'A', 'RRRV'), + 51 : ('LOGEST', 1, 4, 0x04, 4, 'A', 'RRVV'), + 52 : ('GROWTH', 1, 4, 0x04, 4, 'A', 'RRRV'), + 56 : ('PV', 3, 5, 0x04, 5, 'V', 'VVVVV'), + 57 : ('FV', 3, 5, 0x04, 5, 'V', 'VVVVV'), + 58 : ('NPER', 3, 5, 0x04, 5, 'V', 'VVVVV'), + 59 : ('PMT', 3, 5, 0x04, 5, 'V', 'VVVVV'), + 60 : ('RATE', 3, 6, 0x04, 6, 'V', 'VVVVVV'), + 61 : ('MIRR', 3, 3, 0x02, 3, 'V', 'RVV'), + 62 : ('IRR', 1, 2, 0x04, 2, 'V', 'RV'), + 63 : ('RAND', 0, 0, 0x0a, 0, 'V', ''), + 64 : ('MATCH', 2, 3, 0x04, 3, 'V', 'VRR'), + 65 : ('DATE', 3, 3, 0x02, 3, 'V', 'VVV'), + 66 : ('TIME', 3, 3, 0x02, 3, 'V', 'VVV'), + 67 : ('DAY', 1, 1, 0x02, 1, 'V', 'V'), + 68 : ('MONTH', 1, 1, 0x02, 1, 'V', 'V'), + 69 : ('YEAR', 1, 1, 0x02, 1, 'V', 'V'), + 70 : ('WEEKDAY', 1, 2, 0x04, 2, 'V', 'VV'), + 71 : ('HOUR', 1, 1, 0x02, 1, 'V', 'V'), + 72 : ('MINUTE', 1, 1, 0x02, 1, 'V', 'V'), + 73 : ('SECOND', 1, 1, 0x02, 1, 'V', 'V'), + 74 : ('NOW', 0, 0, 0x0a, 0, 'V', ''), + 75 : ('AREAS', 1, 1, 0x02, 1, 'V', 'R'), + 76 : ('ROWS', 1, 1, 0x02, 1, 'V', 'R'), + 77 : ('COLUMNS', 1, 1, 0x02, 1, 'V', 'R'), + 78 : ('OFFSET', 3, 5, 0x04, 5, 'R', 'RVVVV'), + 82 : ('SEARCH', 2, 3, 0x04, 3, 'V', 'VVV'), + 83 : ('TRANSPOSE', 1, 1, 0x02, 1, 'A', 'A'), + 86 : ('TYPE', 1, 1, 0x02, 1, 'V', 'V'), + 92 : ('SERIESSUM', 4, 4, 0x02, 4, 'V', 'VVVA'), + 97 : ('ATAN2', 2, 2, 0x02, 2, 'V', 'VV'), + 98 : ('ASIN', 1, 1, 0x02, 1, 'V', 'V'), + 99 : ('ACOS', 1, 1, 0x02, 1, 'V', 'V'), + 100: ('CHOOSE', 2, 30, 0x04, 2, 'V', 'VR'), + 101: ('HLOOKUP', 3, 4, 0x04, 4, 'V', 'VRRV'), + 102: ('VLOOKUP', 3, 4, 0x04, 4, 'V', 'VRRV'), + 105: ('ISREF', 1, 1, 0x02, 1, 'V', 'R'), + 109: ('LOG', 1, 2, 0x04, 2, 'V', 'VV'), + 111: ('CHAR', 1, 1, 0x02, 1, 'V', 'V'), + 112: ('LOWER', 1, 1, 0x02, 1, 'V', 'V'), + 113: ('UPPER', 1, 1, 0x02, 1, 'V', 'V'), + 114: ('PROPER', 1, 1, 0x02, 1, 'V', 'V'), + 115: ('LEFT', 1, 2, 0x04, 2, 'V', 'VV'), + 116: ('RIGHT', 1, 2, 0x04, 2, 'V', 'VV'), + 117: ('EXACT', 2, 2, 0x02, 2, 'V', 'VV'), + 118: ('TRIM', 1, 1, 0x02, 1, 'V', 'V'), + 119: ('REPLACE', 4, 4, 0x02, 4, 'V', 'VVVV'), + 120: ('SUBSTITUTE', 3, 4, 0x04, 4, 'V', 'VVVV'), + 121: ('CODE', 1, 1, 0x02, 1, 'V', 'V'), + 124: ('FIND', 2, 3, 0x04, 3, 'V', 'VVV'), + 125: ('CELL', 1, 2, 0x0c, 2, 'V', 'VR'), + 126: ('ISERR', 1, 1, 0x02, 1, 'V', 'V'), + 127: ('ISTEXT', 1, 1, 0x02, 1, 'V', 'V'), + 128: ('ISNUMBER', 1, 1, 0x02, 1, 'V', 'V'), + 129: ('ISBLANK', 1, 1, 0x02, 1, 'V', 'V'), + 130: ('T', 1, 1, 0x02, 1, 'V', 'R'), + 131: ('N', 1, 1, 0x02, 1, 'V', 'R'), + 140: ('DATEVALUE', 1, 1, 0x02, 1, 'V', 'V'), + 141: ('TIMEVALUE', 1, 1, 0x02, 1, 'V', 'V'), + 142: ('SLN', 3, 3, 0x02, 3, 'V', 'VVV'), + 143: ('SYD', 4, 4, 0x02, 4, 'V', 'VVVV'), + 144: ('DDB', 4, 5, 0x04, 5, 'V', 'VVVVV'), + 148: ('INDIRECT', 1, 2, 0x0c, 2, 'R', 'VV'), + 162: ('CLEAN', 1, 1, 0x02, 1, 'V', 'V'), + 163: ('MDETERM', 1, 1, 0x02, 1, 'V', 'A'), + 164: ('MINVERSE', 1, 1, 0x02, 1, 'A', 'A'), + 165: ('MMULT', 2, 2, 0x02, 2, 'A', 'AA'), + 167: ('IPMT', 4, 6, 0x04, 6, 'V', 'VVVVVV'), + 168: ('PPMT', 4, 6, 0x04, 6, 'V', 'VVVVVV'), + 169: ('COUNTA', 0, 30, 0x04, 1, 'V', 'R'), + 183: ('PRODUCT', 0, 30, 0x04, 1, 'V', 'R'), + 184: ('FACT', 1, 1, 0x02, 1, 'V', 'V'), + 189: ('DPRODUCT', 3, 3, 0x02, 3, 'V', 'RRR'), + 190: ('ISNONTEXT', 1, 1, 0x02, 1, 'V', 'V'), + 193: ('STDEVP', 1, 30, 0x04, 1, 'V', 'R'), + 194: ('VARP', 1, 30, 0x04, 1, 'V', 'R'), + 195: ('DSTDEVP', 3, 3, 0x02, 3, 'V', 'RRR'), + 196: ('DVARP', 3, 3, 0x02, 3, 'V', 'RRR'), + 197: ('TRUNC', 1, 2, 0x04, 2, 'V', 'VV'), + 198: ('ISLOGICAL', 1, 1, 0x02, 1, 'V', 'V'), + 199: ('DCOUNTA', 3, 3, 0x02, 3, 'V', 'RRR'), + 204: ('USDOLLAR', 1, 2, 0x04, 2, 'V', 'VV'), + 205: ('FINDB', 2, 3, 0x04, 3, 'V', 'VVV'), + 206: ('SEARCHB', 2, 3, 0x04, 3, 'V', 'VVV'), + 207: ('REPLACEB', 4, 4, 0x02, 4, 'V', 'VVVV'), + 208: ('LEFTB', 1, 2, 0x04, 2, 'V', 'VV'), + 209: ('RIGHTB', 1, 2, 0x04, 2, 'V', 'VV'), + 210: ('MIDB', 3, 3, 0x02, 3, 'V', 'VVV'), + 211: ('LENB', 1, 1, 0x02, 1, 'V', 'V'), + 212: ('ROUNDUP', 2, 2, 0x02, 2, 'V', 'VV'), + 213: ('ROUNDDOWN', 2, 2, 0x02, 2, 'V', 'VV'), + 214: ('ASC', 1, 1, 0x02, 1, 'V', 'V'), + 215: ('DBCS', 1, 1, 0x02, 1, 'V', 'V'), + 216: ('RANK', 2, 3, 0x04, 3, 'V', 'VRV'), + 219: ('ADDRESS', 2, 5, 0x04, 5, 'V', 'VVVVV'), + 220: ('DAYS360', 2, 3, 0x04, 3, 'V', 'VVV'), + 221: ('TODAY', 0, 0, 0x0a, 0, 'V', ''), + 222: ('VDB', 5, 7, 0x04, 7, 'V', 'VVVVVVV'), + 227: ('MEDIAN', 1, 30, 0x04, 1, 'V', 'R'), + 228: ('SUMPRODUCT', 1, 30, 0x04, 1, 'V', 'A'), + 229: ('SINH', 1, 1, 0x02, 1, 'V', 'V'), + 230: ('COSH', 1, 1, 0x02, 1, 'V', 'V'), + 231: ('TANH', 1, 1, 0x02, 1, 'V', 'V'), + 232: ('ASINH', 1, 1, 0x02, 1, 'V', 'V'), + 233: ('ACOSH', 1, 1, 0x02, 1, 'V', 'V'), + 234: ('ATANH', 1, 1, 0x02, 1, 'V', 'V'), + 235: ('DGET', 3, 3, 0x02, 3, 'V', 'RRR'), + 244: ('INFO', 1, 1, 0x02, 1, 'V', 'V'), + 247: ('DB', 4, 5, 0x04, 5, 'V', 'VVVVV'), + 252: ('FREQUENCY', 2, 2, 0x02, 2, 'A', 'RR'), + 261: ('ERROR.TYPE', 1, 1, 0x02, 1, 'V', 'V'), + 269: ('AVEDEV', 1, 30, 0x04, 1, 'V', 'R'), + 270: ('BETADIST', 3, 5, 0x04, 1, 'V', 'V'), + 271: ('GAMMALN', 1, 1, 0x02, 1, 'V', 'V'), + 272: ('BETAINV', 3, 5, 0x04, 1, 'V', 'V'), + 273: ('BINOMDIST', 4, 4, 0x02, 4, 'V', 'VVVV'), + 274: ('CHIDIST', 2, 2, 0x02, 2, 'V', 'VV'), + 275: ('CHIINV', 2, 2, 0x02, 2, 'V', 'VV'), + 276: ('COMBIN', 2, 2, 0x02, 2, 'V', 'VV'), + 277: ('CONFIDENCE', 3, 3, 0x02, 3, 'V', 'VVV'), + 278: ('CRITBINOM', 3, 3, 0x02, 3, 'V', 'VVV'), + 279: ('EVEN', 1, 1, 0x02, 1, 'V', 'V'), + 280: ('EXPONDIST', 3, 3, 0x02, 3, 'V', 'VVV'), + 281: ('FDIST', 3, 3, 0x02, 3, 'V', 'VVV'), + 282: ('FINV', 3, 3, 0x02, 3, 'V', 'VVV'), + 283: ('FISHER', 1, 1, 0x02, 1, 'V', 'V'), + 284: ('FISHERINV', 1, 1, 0x02, 1, 'V', 'V'), + 285: ('FLOOR', 2, 2, 0x02, 2, 'V', 'VV'), + 286: ('GAMMADIST', 4, 4, 0x02, 4, 'V', 'VVVV'), + 287: ('GAMMAINV', 3, 3, 0x02, 3, 'V', 'VVV'), + 288: ('CEILING', 2, 2, 0x02, 2, 'V', 'VV'), + 289: ('HYPGEOMDIST', 4, 4, 0x02, 4, 'V', 'VVVV'), + 290: ('LOGNORMDIST', 3, 3, 0x02, 3, 'V', 'VVV'), + 291: ('LOGINV', 3, 3, 0x02, 3, 'V', 'VVV'), + 292: ('NEGBINOMDIST', 3, 3, 0x02, 3, 'V', 'VVV'), + 293: ('NORMDIST', 4, 4, 0x02, 4, 'V', 'VVVV'), + 294: ('NORMSDIST', 1, 1, 0x02, 1, 'V', 'V'), + 295: ('NORMINV', 3, 3, 0x02, 3, 'V', 'VVV'), + 296: ('NORMSINV', 1, 1, 0x02, 1, 'V', 'V'), + 297: ('STANDARDIZE', 3, 3, 0x02, 3, 'V', 'VVV'), + 298: ('ODD', 1, 1, 0x02, 1, 'V', 'V'), + 299: ('PERMUT', 2, 2, 0x02, 2, 'V', 'VV'), + 300: ('POISSON', 3, 3, 0x02, 3, 'V', 'VVV'), + 301: ('TDIST', 3, 3, 0x02, 3, 'V', 'VVV'), + 302: ('WEIBULL', 4, 4, 0x02, 4, 'V', 'VVVV'), + 303: ('SUMXMY2', 2, 2, 0x02, 2, 'V', 'AA'), + 304: ('SUMX2MY2', 2, 2, 0x02, 2, 'V', 'AA'), + 305: ('SUMX2PY2', 2, 2, 0x02, 2, 'V', 'AA'), + 306: ('CHITEST', 2, 2, 0x02, 2, 'V', 'AA'), + 307: ('CORREL', 2, 2, 0x02, 2, 'V', 'AA'), + 308: ('COVAR', 2, 2, 0x02, 2, 'V', 'AA'), + 309: ('FORECAST', 3, 3, 0x02, 3, 'V', 'VAA'), + 310: ('FTEST', 2, 2, 0x02, 2, 'V', 'AA'), + 311: ('INTERCEPT', 2, 2, 0x02, 2, 'V', 'AA'), + 312: ('PEARSON', 2, 2, 0x02, 2, 'V', 'AA'), + 313: ('RSQ', 2, 2, 0x02, 2, 'V', 'AA'), + 314: ('STEYX', 2, 2, 0x02, 2, 'V', 'AA'), + 315: ('SLOPE', 2, 2, 0x02, 2, 'V', 'AA'), + 316: ('TTEST', 4, 4, 0x02, 4, 'V', 'AAVV'), + 317: ('PROB', 3, 4, 0x04, 3, 'V', 'AAV'), + 318: ('DEVSQ', 1, 30, 0x04, 1, 'V', 'R'), + 319: ('GEOMEAN', 1, 30, 0x04, 1, 'V', 'R'), + 320: ('HARMEAN', 1, 30, 0x04, 1, 'V', 'R'), + 321: ('SUMSQ', 0, 30, 0x04, 1, 'V', 'R'), + 322: ('KURT', 1, 30, 0x04, 1, 'V', 'R'), + 323: ('SKEW', 1, 30, 0x04, 1, 'V', 'R'), + 324: ('ZTEST', 2, 3, 0x04, 2, 'V', 'RV'), + 325: ('LARGE', 2, 2, 0x02, 2, 'V', 'RV'), + 326: ('SMALL', 2, 2, 0x02, 2, 'V', 'RV'), + 327: ('QUARTILE', 2, 2, 0x02, 2, 'V', 'RV'), + 328: ('PERCENTILE', 2, 2, 0x02, 2, 'V', 'RV'), + 329: ('PERCENTRANK', 2, 3, 0x04, 2, 'V', 'RV'), + 330: ('MODE', 1, 30, 0x04, 1, 'V', 'A'), + 331: ('TRIMMEAN', 2, 2, 0x02, 2, 'V', 'RV'), + 332: ('TINV', 2, 2, 0x02, 2, 'V', 'VV'), + 336: ('CONCATENATE', 0, 30, 0x04, 1, 'V', 'V'), + 337: ('POWER', 2, 2, 0x02, 2, 'V', 'VV'), + 342: ('RADIANS', 1, 1, 0x02, 1, 'V', 'V'), + 343: ('DEGREES', 1, 1, 0x02, 1, 'V', 'V'), + 344: ('SUBTOTAL', 2, 30, 0x04, 2, 'V', 'VR'), + 345: ('SUMIF', 2, 3, 0x04, 3, 'V', 'RVR'), + 346: ('COUNTIF', 2, 2, 0x02, 2, 'V', 'RV'), + 347: ('COUNTBLANK', 1, 1, 0x02, 1, 'V', 'R'), + 350: ('ISPMT', 4, 4, 0x02, 4, 'V', 'VVVV'), + 351: ('DATEDIF', 3, 3, 0x02, 3, 'V', 'VVV'), + 352: ('DATESTRING', 1, 1, 0x02, 1, 'V', 'V'), + 353: ('NUMBERSTRING', 2, 2, 0x02, 2, 'V', 'VV'), + 354: ('ROMAN', 1, 2, 0x04, 2, 'V', 'VV'), + 358: ('GETPIVOTDATA', 2, 2, 0x02, 2, 'V', 'RV'), + 359: ('HYPERLINK', 1, 2, 0x04, 2, 'V', 'VV'), + 360: ('PHONETIC', 1, 1, 0x02, 1, 'V', 'V'), + 361: ('AVERAGEA', 1, 30, 0x04, 1, 'V', 'R'), + 362: ('MAXA', 1, 30, 0x04, 1, 'V', 'R'), + 363: ('MINA', 1, 30, 0x04, 1, 'V', 'R'), + 364: ('STDEVPA', 1, 30, 0x04, 1, 'V', 'R'), + 365: ('VARPA', 1, 30, 0x04, 1, 'V', 'R'), + 366: ('STDEVA', 1, 30, 0x04, 1, 'V', 'R'), + 367: ('VARA', 1, 30, 0x04, 1, 'V', 'R'), + 368: ('BAHTTEXT', 1, 1, 0x02, 1, 'V', 'V'), + 369: ('THAIDAYOFWEEK', 1, 1, 0x02, 1, 'V', 'V'), + 370: ('THAIDIGIT', 1, 1, 0x02, 1, 'V', 'V'), + 371: ('THAIMONTHOFYEAR', 1, 1, 0x02, 1, 'V', 'V'), + 372: ('THAINUMSOUND', 1, 1, 0x02, 1, 'V', 'V'), + 373: ('THAINUMSTRING', 1, 1, 0x02, 1, 'V', 'V'), + 374: ('THAISTRINGLENGTH', 1, 1, 0x02, 1, 'V', 'V'), + 375: ('ISTHAIDIGIT', 1, 1, 0x02, 1, 'V', 'V'), + 376: ('ROUNDBAHTDOWN', 1, 1, 0x02, 1, 'V', 'V'), + 377: ('ROUNDBAHTUP', 1, 1, 0x02, 1, 'V', 'V'), + 378: ('THAIYEAR', 1, 1, 0x02, 1, 'V', 'V'), + 379: ('RTD', 2, 5, 0x04, 1, 'V', 'V'), +} + +tAttrNames = { + 0x00: "Skip??", # seen in SAMPLES.XLS which shipped with Excel 5.0 + 0x01: "Volatile", + 0x02: "If", + 0x04: "Choose", + 0x08: "Skip", + 0x10: "Sum", + 0x20: "Assign", + 0x40: "Space", + 0x41: "SpaceVolatile", +} + +error_opcodes = set([0x07, 0x08, 0x0A, 0x0B, 0x1C, 0x1D, 0x2F]) + +tRangeFuncs = (min, max, min, max, min, max) +tIsectFuncs = (max, min, max, min, max, min) + +def do_box_funcs(box_funcs, boxa, boxb): + return tuple( + func(numa, numb) + for func, numa, numb in zip(box_funcs, boxa.coords, boxb.coords) + ) + +def adjust_cell_addr_biff8(rowval, colval, reldelta, browx=None, bcolx=None): + row_rel = (colval >> 15) & 1 + col_rel = (colval >> 14) & 1 + rowx = rowval + colx = colval & 0xff + if reldelta: + if row_rel and rowx >= 32768: + rowx -= 65536 + if col_rel and colx >= 128: + colx -= 256 + else: + if row_rel: + rowx -= browx + if col_rel: + colx -= bcolx + return rowx, colx, row_rel, col_rel + +def adjust_cell_addr_biff_le7( + rowval, colval, reldelta, browx=None, bcolx=None): + row_rel = (rowval >> 15) & 1 + col_rel = (rowval >> 14) & 1 + rowx = rowval & 0x3fff + colx = colval + if reldelta: + if row_rel and rowx >= 8192: + rowx -= 16384 + if col_rel and colx >= 128: + colx -= 256 + else: + if row_rel: + rowx -= browx + if col_rel: + colx -= bcolx + return rowx, colx, row_rel, col_rel + +def get_cell_addr(data, pos, bv, reldelta, browx=None, bcolx=None): + if bv >= 80: + rowval, colval = unpack("<HH", data[pos:pos+4]) + # print " rv=%04xh cv=%04xh" % (rowval, colval) + return adjust_cell_addr_biff8(rowval, colval, reldelta, browx, bcolx) + else: + rowval, colval = unpack("<HB", data[pos:pos+3]) + # print " rv=%04xh cv=%04xh" % (rowval, colval) + return adjust_cell_addr_biff_le7( + rowval, colval, reldelta, browx, bcolx) + +def get_cell_range_addr(data, pos, bv, reldelta, browx=None, bcolx=None): + if bv >= 80: + row1val, row2val, col1val, col2val = unpack("<HHHH", data[pos:pos+8]) + # print " rv=%04xh cv=%04xh" % (row1val, col1val) + # print " rv=%04xh cv=%04xh" % (row2val, col2val) + res1 = adjust_cell_addr_biff8(row1val, col1val, reldelta, browx, bcolx) + res2 = adjust_cell_addr_biff8(row2val, col2val, reldelta, browx, bcolx) + return res1, res2 + else: + row1val, row2val, col1val, col2val = unpack("<HHBB", data[pos:pos+6]) + # print " rv=%04xh cv=%04xh" % (row1val, col1val) + # print " rv=%04xh cv=%04xh" % (row2val, col2val) + res1 = adjust_cell_addr_biff_le7( + row1val, col1val, reldelta, browx, bcolx) + res2 = adjust_cell_addr_biff_le7( + row2val, col2val, reldelta, browx, bcolx) + return res1, res2 + +def get_externsheet_local_range(bk, refx, blah=0): + try: + info = bk._externsheet_info[refx] + except IndexError: + print("!!! get_externsheet_local_range: refx=%d, not in range(%d)" + % (refx, len(bk._externsheet_info)), file=bk.logfile) + return (-101, -101) + ref_recordx, ref_first_sheetx, ref_last_sheetx = info + if ref_recordx == bk._supbook_addins_inx: + if blah: + print("/// get_externsheet_local_range(refx=%d) -> addins %r" % (refx, info), file=bk.logfile) + assert ref_first_sheetx == 0xFFFE == ref_last_sheetx + return (-5, -5) + if ref_recordx != bk._supbook_locals_inx: + if blah: + print("/// get_externsheet_local_range(refx=%d) -> external %r" % (refx, info), file=bk.logfile) + return (-4, -4) # external reference + if ref_first_sheetx == 0xFFFE == ref_last_sheetx: + if blah: + print("/// get_externsheet_local_range(refx=%d) -> unspecified sheet %r" % (refx, info), file=bk.logfile) + return (-1, -1) # internal reference, any sheet + if ref_first_sheetx == 0xFFFF == ref_last_sheetx: + if blah: + print("/// get_externsheet_local_range(refx=%d) -> deleted sheet(s)" % (refx, ), file=bk.logfile) + return (-2, -2) # internal reference, deleted sheet(s) + nsheets = len(bk._all_sheets_map) + if not(0 <= ref_first_sheetx <= ref_last_sheetx < nsheets): + if blah: + print("/// get_externsheet_local_range(refx=%d) -> %r" % (refx, info), file=bk.logfile) + print("--- first/last sheet not in range(%d)" % nsheets, file=bk.logfile) + return (-102, -102) # stuffed up somewhere :-( + xlrd_sheetx1 = bk._all_sheets_map[ref_first_sheetx] + xlrd_sheetx2 = bk._all_sheets_map[ref_last_sheetx] + if not(0 <= xlrd_sheetx1 <= xlrd_sheetx2): + return (-3, -3) # internal reference, but to a macro sheet + return xlrd_sheetx1, xlrd_sheetx2 + +def get_externsheet_local_range_b57( + bk, raw_extshtx, ref_first_sheetx, ref_last_sheetx, blah=0): + if raw_extshtx > 0: + if blah: + print("/// get_externsheet_local_range_b57(raw_extshtx=%d) -> external" % raw_extshtx, file=bk.logfile) + return (-4, -4) # external reference + if ref_first_sheetx == -1 and ref_last_sheetx == -1: + return (-2, -2) # internal reference, deleted sheet(s) + nsheets = len(bk._all_sheets_map) + if not(0 <= ref_first_sheetx <= ref_last_sheetx < nsheets): + if blah: + print("/// get_externsheet_local_range_b57(%d, %d, %d) -> ???" + % (raw_extshtx, ref_first_sheetx, ref_last_sheetx), file=bk.logfile) + print("--- first/last sheet not in range(%d)" % nsheets, file=bk.logfile) + return (-103, -103) # stuffed up somewhere :-( + xlrd_sheetx1 = bk._all_sheets_map[ref_first_sheetx] + xlrd_sheetx2 = bk._all_sheets_map[ref_last_sheetx] + if not(0 <= xlrd_sheetx1 <= xlrd_sheetx2): + return (-3, -3) # internal reference, but to a macro sheet + return xlrd_sheetx1, xlrd_sheetx2 + +class FormulaError(Exception): + pass + + +class Operand(object): + """ + Used in evaluating formulas. + The following table describes the kinds and how their values + are represented. + + .. raw:: html + + <table border="1" cellpadding="7"> + <tr> + <th>Kind symbol</th> + <th>Kind number</th> + <th>Value representation</th> + </tr> + <tr> + <td>oBOOL</td> + <td align="center">3</td> + <td>integer: 0 => False; 1 => True</td> + </tr> + <tr> + <td>oERR</td> + <td align="center">4</td> + <td>None, or an int error code (same as XL_CELL_ERROR in the Cell class). + </td> + </tr> + <tr> + <td>oMSNG</td> + <td align="center">5</td> + <td>Used by Excel as a placeholder for a missing (not supplied) function + argument. Should *not* appear as a final formula result. Value is None.</td> + </tr> + <tr> + <td>oNUM</td> + <td align="center">2</td> + <td>A float. Note that there is no way of distinguishing dates.</td> + </tr> + <tr> + <td>oREF</td> + <td align="center">-1</td> + <td>The value is either None or a non-empty list of + absolute Ref3D instances.<br> + </td> + </tr> + <tr> + <td>oREL</td> + <td align="center">-2</td> + <td>The value is None or a non-empty list of + fully or partially relative Ref3D instances. + </td> + </tr> + <tr> + <td>oSTRG</td> + <td align="center">1</td> + <td>A Unicode string.</td> + </tr> + <tr> + <td>oUNK</td> + <td align="center">0</td> + <td>The kind is unknown or ambiguous. The value is None</td> + </tr> + </table> + """ + + #: None means that the actual value of the operand is a variable + #: (depends on cell data), not a constant. + value = None + + #: oUNK means that the kind of operand is not known unambiguously. + kind = oUNK + + #: The reconstituted text of the original formula. Function names will be + #: in English irrespective of the original language, which doesn't seem + #: to be recorded anywhere. The separator is ",", not ";" or whatever else + #: might be more appropriate for the end-user's locale; patches welcome. + text = '?' + + def __init__(self, akind=None, avalue=None, arank=0, atext='?'): + if akind is not None: + self.kind = akind + if avalue is not None: + self.value = avalue + self.rank = arank + # rank is an internal gizmo (operator precedence); + # it's used in reconstructing formula text. + self.text = atext + + def __repr__(self): + kind_text = okind_dict.get(self.kind, "?Unknown kind?") + return "Operand(kind=%s, value=%r, text=%r)" \ + % (kind_text, self.value, self.text) + + +class Ref3D(tuple): + """ + Represents an absolute or relative 3-dimensional reference to a box + of one or more cells. + + The ``coords`` attribute is a tuple of the form:: + + (shtxlo, shtxhi, rowxlo, rowxhi, colxlo, colxhi) + + where ``0 <= thingxlo <= thingx < thingxhi``. + + .. note:: + It is quite possible to have ``thingx > nthings``; for example + ``Print_Titles`` could have ``colxhi == 256`` and/or ``rowxhi == 65536`` + irrespective of how many columns/rows are actually used in the worksheet. + The caller will need to decide how to handle this situation. + Keyword: :class:`IndexError` :-) + + The components of the coords attribute are also available as individual + attributes: ``shtxlo``, ``shtxhi``, ``rowxlo``, ``rowxhi``, ``colxlo``, and + ``colxhi``. + + The ``relflags`` attribute is a 6-tuple of flags which indicate whether + the corresponding (sheet|row|col)(lo|hi) is relative (1) or absolute (0). + + .. note:: + There is necessarily no information available as to what cell(s) + the reference could possibly be relative to. The caller must decide what + if any use to make of ``oREL`` operands. + + .. note: + A partially relative reference may well be a typo. + For example, define name ``A1Z10`` as ``$a$1:$z10`` (missing ``$`` after + ``z``) while the cursor is on cell ``Sheet3!A27``. + + The resulting :class:`Ref3D` instance will have + ``coords = (2, 3, 0, -16, 0, 26)`` + and ``relflags = (0, 0, 0, 1, 0, 0).<br> + + So far, only one possibility of a sheet-relative component in + a reference has been noticed: a 2D reference located in the + "current sheet". + + This will appear as ``coords = (0, 1, ...)`` and + ``relflags = (1, 1, ...)``. + + .. versionadded:: 0.6.0 + """ + + def __init__(self, atuple): + self.coords = atuple[0:6] + self.relflags = atuple[6:12] + if not self.relflags: + self.relflags = (0, 0, 0, 0, 0, 0) + (self.shtxlo, self.shtxhi, + self.rowxlo, self.rowxhi, + self.colxlo, self.colxhi) = self.coords + + def __repr__(self): + if not self.relflags or self.relflags == (0, 0, 0, 0, 0, 0): + return "Ref3D(coords=%r)" % (self.coords, ) + else: + return "Ref3D(coords=%r, relflags=%r)" \ + % (self.coords, self.relflags) + +tAdd = 0x03 +tSub = 0x04 +tMul = 0x05 +tDiv = 0x06 +tPower = 0x07 +tConcat = 0x08 +tLT, tLE, tEQ, tGE, tGT, tNE = range(0x09, 0x0F) + + +def nop(x): + return x + +def _opr_pow(x, y): return x ** y + +def _opr_lt(x, y): return x < y +def _opr_le(x, y): return x <= y +def _opr_eq(x, y): return x == y +def _opr_ge(x, y): return x >= y +def _opr_gt(x, y): return x > y +def _opr_ne(x, y): return x != y + +def num2strg(num): + """ + Attempt to emulate Excel's default conversion from number to string. + """ + s = str(num) + if s.endswith(".0"): + s = s[:-2] + return s + +_arith_argdict = {oNUM: nop, oSTRG: float} +_cmp_argdict = {oNUM: nop, oSTRG: nop} +# Seems no conversions done on relops; in Excel, "1" > 9 produces TRUE. +_strg_argdict = {oNUM:num2strg, oSTRG:nop} +binop_rules = { + tAdd: (_arith_argdict, oNUM, opr.add, 30, '+'), + tSub: (_arith_argdict, oNUM, opr.sub, 30, '-'), + tMul: (_arith_argdict, oNUM, opr.mul, 40, '*'), + tDiv: (_arith_argdict, oNUM, opr.truediv, 40, '/'), + tPower: (_arith_argdict, oNUM, _opr_pow, 50, '^',), + tConcat:(_strg_argdict, oSTRG, opr.add, 20, '&'), + tLT: (_cmp_argdict, oBOOL, _opr_lt, 10, '<'), + tLE: (_cmp_argdict, oBOOL, _opr_le, 10, '<='), + tEQ: (_cmp_argdict, oBOOL, _opr_eq, 10, '='), + tGE: (_cmp_argdict, oBOOL, _opr_ge, 10, '>='), + tGT: (_cmp_argdict, oBOOL, _opr_gt, 10, '>'), + tNE: (_cmp_argdict, oBOOL, _opr_ne, 10, '<>'), +} + +unop_rules = { + 0x13: (lambda x: -x, 70, '-', ''), # unary minus + 0x12: (lambda x: x, 70, '+', ''), # unary plus + 0x14: (lambda x: x / 100.0, 60, '', '%'),# percent +} + +LEAF_RANK = 90 +FUNC_RANK = 90 + +STACK_ALARM_LEVEL = 5 +STACK_PANIC_LEVEL = 10 + +def evaluate_name_formula(bk, nobj, namex, blah=0, level=0): + if level > STACK_ALARM_LEVEL: + blah = 1 + data = nobj.raw_formula + fmlalen = nobj.basic_formula_len + bv = bk.biff_version + reldelta = 1 # All defined name formulas use "Method B" [OOo docs] + if blah: + print("::: evaluate_name_formula %r %r %d %d %r level=%d" + % (namex, nobj.name, fmlalen, bv, data, level), file=bk.logfile) + hex_char_dump(data, 0, fmlalen, fout=bk.logfile) + if level > STACK_PANIC_LEVEL: + raise XLRDError("Excessive indirect references in NAME formula") + sztab = szdict[bv] + pos = 0 + stack = [] + any_rel = 0 + any_err = 0 + any_external = 0 + unk_opnd = Operand(oUNK, None) + error_opnd = Operand(oERR, None) + spush = stack.append + + def do_binop(opcd, stk): + assert len(stk) >= 2 + bop = stk.pop() + aop = stk.pop() + argdict, result_kind, func, rank, sym = binop_rules[opcd] + otext = ''.join([ + '('[:aop.rank < rank], + aop.text, + ')'[:aop.rank < rank], + sym, + '('[:bop.rank < rank], + bop.text, + ')'[:bop.rank < rank], + ]) + resop = Operand(result_kind, None, rank, otext) + try: + bconv = argdict[bop.kind] + aconv = argdict[aop.kind] + except KeyError: + stk.append(resop) + return + if bop.value is None or aop.value is None: + stk.append(resop) + return + bval = bconv(bop.value) + aval = aconv(aop.value) + result = func(aval, bval) + if result_kind == oBOOL: + result = 1 if result else 0 + resop.value = result + stk.append(resop) + + def do_unaryop(opcode, result_kind, stk): + assert len(stk) >= 1 + aop = stk.pop() + val = aop.value + func, rank, sym1, sym2 = unop_rules[opcode] + otext = ''.join([ + sym1, + '('[:aop.rank < rank], + aop.text, + ')'[:aop.rank < rank], + sym2, + ]) + if val is not None: + val = func(val) + stk.append(Operand(result_kind, val, rank, otext)) + + def not_in_name_formula(op_arg, oname_arg): + msg = "ERROR *** Token 0x%02x (%s) found in NAME formula" \ + % (op_arg, oname_arg) + raise FormulaError(msg) + + if fmlalen == 0: + stack = [unk_opnd] + + while 0 <= pos < fmlalen: + op = BYTES_ORD(data[pos]) + opcode = op & 0x1f + optype = (op & 0x60) >> 5 + if optype: + opx = opcode + 32 + else: + opx = opcode + oname = onames[opx] # + [" RVA"][optype] + sz = sztab[opx] + if blah: + print("Pos:%d Op:0x%02x Name:t%s Sz:%d opcode:%02xh optype:%02xh" + % (pos, op, oname, sz, opcode, optype), file=bk.logfile) + print("Stack =", stack, file=bk.logfile) + if sz == -2: + msg = 'ERROR *** Unexpected token 0x%02x ("%s"); biff_version=%d' \ + % (op, oname, bv) + raise FormulaError(msg) + if not optype: + if 0x00 <= opcode <= 0x02: # unk_opnd, tExp, tTbl + not_in_name_formula(op, oname) + elif 0x03 <= opcode <= 0x0E: + # Add, Sub, Mul, Div, Power + # tConcat + # tLT, ..., tNE + do_binop(opcode, stack) + elif opcode == 0x0F: # tIsect + if blah: print("tIsect pre", stack, file=bk.logfile) + assert len(stack) >= 2 + bop = stack.pop() + aop = stack.pop() + sym = ' ' + rank = 80 ########## check ####### + otext = ''.join([ + '('[:aop.rank < rank], + aop.text, + ')'[:aop.rank < rank], + sym, + '('[:bop.rank < rank], + bop.text, + ')'[:bop.rank < rank], + ]) + res = Operand(oREF) + res.text = otext + if bop.kind == oERR or aop.kind == oERR: + res.kind = oERR + elif bop.kind == oUNK or aop.kind == oUNK: + # This can happen with undefined + # (go search in the current sheet) labels. + # For example =Bob Sales + # Each label gets a NAME record with an empty formula (!) + # Evaluation of the tName token classifies it as oUNK + # res.kind = oREF + pass + elif bop.kind == oREF == aop.kind: + if aop.value is not None and bop.value is not None: + assert len(aop.value) == 1 + assert len(bop.value) == 1 + coords = do_box_funcs( + tIsectFuncs, aop.value[0], bop.value[0]) + res.value = [Ref3D(coords)] + elif bop.kind == oREL == aop.kind: + res.kind = oREL + if aop.value is not None and bop.value is not None: + assert len(aop.value) == 1 + assert len(bop.value) == 1 + coords = do_box_funcs( + tIsectFuncs, aop.value[0], bop.value[0]) + relfa = aop.value[0].relflags + relfb = bop.value[0].relflags + if relfa == relfb: + res.value = [Ref3D(coords + relfa)] + else: + pass + spush(res) + if blah: print("tIsect post", stack, file=bk.logfile) + elif opcode == 0x10: # tList + if blah: print("tList pre", stack, file=bk.logfile) + assert len(stack) >= 2 + bop = stack.pop() + aop = stack.pop() + sym = ',' + rank = 80 ########## check ####### + otext = ''.join([ + '('[:aop.rank < rank], + aop.text, + ')'[:aop.rank < rank], + sym, + '('[:bop.rank < rank], + bop.text, + ')'[:bop.rank < rank], + ]) + res = Operand(oREF, None, rank, otext) + if bop.kind == oERR or aop.kind == oERR: + res.kind = oERR + elif bop.kind in (oREF, oREL) and aop.kind in (oREF, oREL): + res.kind = oREF + if aop.kind == oREL or bop.kind == oREL: + res.kind = oREL + if aop.value is not None and bop.value is not None: + assert len(aop.value) >= 1 + assert len(bop.value) == 1 + res.value = aop.value + bop.value + else: + pass + spush(res) + if blah: print("tList post", stack, file=bk.logfile) + elif opcode == 0x11: # tRange + if blah: print("tRange pre", stack, file=bk.logfile) + assert len(stack) >= 2 + bop = stack.pop() + aop = stack.pop() + sym = ':' + rank = 80 ########## check ####### + otext = ''.join([ + '('[:aop.rank < rank], + aop.text, + ')'[:aop.rank < rank], + sym, + '('[:bop.rank < rank], + bop.text, + ')'[:bop.rank < rank], + ]) + res = Operand(oREF, None, rank, otext) + if bop.kind == oERR or aop.kind == oERR: + res = oERR + elif bop.kind == oREF == aop.kind: + if aop.value is not None and bop.value is not None: + assert len(aop.value) == 1 + assert len(bop.value) == 1 + coords = do_box_funcs( + tRangeFuncs, aop.value[0], bop.value[0]) + res.value = [Ref3D(coords)] + elif bop.kind == oREL == aop.kind: + res.kind = oREL + if aop.value is not None and bop.value is not None: + assert len(aop.value) == 1 + assert len(bop.value) == 1 + coords = do_box_funcs( + tRangeFuncs, aop.value[0], bop.value[0]) + relfa = aop.value[0].relflags + relfb = bop.value[0].relflags + if relfa == relfb: + res.value = [Ref3D(coords + relfa)] + else: + pass + spush(res) + if blah: print("tRange post", stack, file=bk.logfile) + elif 0x12 <= opcode <= 0x14: # tUplus, tUminus, tPercent + do_unaryop(opcode, oNUM, stack) + elif opcode == 0x15: # tParen + # source cosmetics + pass + elif opcode == 0x16: # tMissArg + spush(Operand(oMSNG, None, LEAF_RANK, '')) + elif opcode == 0x17: # tStr + if bv <= 70: + strg, newpos = unpack_string_update_pos( + data, pos+1, bk.encoding, lenlen=1) + else: + strg, newpos = unpack_unicode_update_pos( + data, pos+1, lenlen=1) + sz = newpos - pos + if blah: print(" sz=%d strg=%r" % (sz, strg), file=bk.logfile) + text = '"' + strg.replace('"', '""') + '"' + spush(Operand(oSTRG, strg, LEAF_RANK, text)) + elif opcode == 0x18: # tExtended + # new with BIFF 8 + assert bv >= 80 + # not in OOo docs + raise FormulaError("tExtended token not implemented") + elif opcode == 0x19: # tAttr + subop, nc = unpack("<BH", data[pos+1:pos+4]) + subname = tAttrNames.get(subop, "??Unknown??") + if subop == 0x04: # Choose + sz = nc * 2 + 6 + elif subop == 0x10: # Sum (single arg) + sz = 4 + if blah: print("tAttrSum", stack, file=bk.logfile) + assert len(stack) >= 1 + aop = stack[-1] + otext = 'SUM(%s)' % aop.text + stack[-1] = Operand(oNUM, None, FUNC_RANK, otext) + else: + sz = 4 + if blah: + print(" subop=%02xh subname=t%s sz=%d nc=%02xh" + % (subop, subname, sz, nc), file=bk.logfile) + elif 0x1A <= opcode <= 0x1B: # tSheet, tEndSheet + assert bv < 50 + raise FormulaError("tSheet & tEndsheet tokens not implemented") + elif 0x1C <= opcode <= 0x1F: # tErr, tBool, tInt, tNum + inx = opcode - 0x1C + nb = [1, 1, 2, 8][inx] + kind = [oERR, oBOOL, oNUM, oNUM][inx] + value, = unpack("<" + "BBHd"[inx], data[pos+1:pos+1+nb]) + if inx == 2: # tInt + value = float(value) + text = str(value) + elif inx == 3: # tNum + text = str(value) + elif inx == 1: # tBool + text = ('FALSE', 'TRUE')[value] + else: + text = '"' +error_text_from_code[value] + '"' + spush(Operand(kind, value, LEAF_RANK, text)) + else: + raise FormulaError("Unhandled opcode: 0x%02x" % opcode) + if sz <= 0: + raise FormulaError("Size not set for opcode 0x%02x" % opcode) + pos += sz + continue + if opcode == 0x00: # tArray + spush(unk_opnd) + elif opcode == 0x01: # tFunc + nb = 1 + int(bv >= 40) + funcx = unpack("<" + " BH"[nb], data[pos+1:pos+1+nb])[0] + func_attrs = func_defs.get(funcx, None) + if not func_attrs: + print("*** formula/tFunc unknown FuncID:%d" + % funcx, file=bk.logfile) + spush(unk_opnd) + else: + func_name, nargs = func_attrs[:2] + if blah: + print(" FuncID=%d name=%s nargs=%d" + % (funcx, func_name, nargs), file=bk.logfile) + assert len(stack) >= nargs + if nargs: + argtext = listsep.join(arg.text for arg in stack[-nargs:]) + otext = "%s(%s)" % (func_name, argtext) + del stack[-nargs:] + else: + otext = func_name + "()" + res = Operand(oUNK, None, FUNC_RANK, otext) + spush(res) + elif opcode == 0x02: #tFuncVar + nb = 1 + int(bv >= 40) + nargs, funcx = unpack("<B" + " BH"[nb], data[pos+1:pos+2+nb]) + prompt, nargs = divmod(nargs, 128) + macro, funcx = divmod(funcx, 32768) + if blah: + print(" FuncID=%d nargs=%d macro=%d prompt=%d" + % (funcx, nargs, macro, prompt), file=bk.logfile) + func_attrs = func_defs.get(funcx, None) + if not func_attrs: + print("*** formula/tFuncVar unknown FuncID:%d" + % funcx, file=bk.logfile) + spush(unk_opnd) + else: + func_name, minargs, maxargs = func_attrs[:3] + if blah: + print(" name: %r, min~max args: %d~%d" + % (func_name, minargs, maxargs), file=bk.logfile) + assert minargs <= nargs <= maxargs + assert len(stack) >= nargs + assert len(stack) >= nargs + argtext = listsep.join(arg.text for arg in stack[-nargs:]) + otext = "%s(%s)" % (func_name, argtext) + res = Operand(oUNK, None, FUNC_RANK, otext) + if funcx == 1: # IF + testarg = stack[-nargs] + if testarg.kind not in (oNUM, oBOOL): + if blah and testarg.kind != oUNK: + print("IF testarg kind?", file=bk.logfile) + elif testarg.value not in (0, 1): + if blah and testarg.value is not None: + print("IF testarg value?", file=bk.logfile) + else: + if nargs == 2 and not testarg.value: + # IF(FALSE, tv) => FALSE + res.kind, res.value = oBOOL, 0 + else: + respos = -nargs + 2 - int(testarg.value) + chosen = stack[respos] + if chosen.kind == oMSNG: + res.kind, res.value = oNUM, 0 + else: + res.kind, res.value = chosen.kind, chosen.value + if blah: + print("$$$$$$ IF => constant", file=bk.logfile) + elif funcx == 100: # CHOOSE + testarg = stack[-nargs] + if testarg.kind == oNUM: + if 1 <= testarg.value < nargs: + chosen = stack[-nargs + int(testarg.value)] + if chosen.kind == oMSNG: + res.kind, res.value = oNUM, 0 + else: + res.kind, res.value = chosen.kind, chosen.value + del stack[-nargs:] + spush(res) + elif opcode == 0x03: #tName + tgtnamex = unpack("<H", data[pos+1:pos+3])[0] - 1 + # Only change with BIFF version is number of trailing UNUSED bytes! + if blah: print(" tgtnamex=%d" % tgtnamex, file=bk.logfile) + tgtobj = bk.name_obj_list[tgtnamex] + if not tgtobj.evaluated: + ### recursive ### + evaluate_name_formula(bk, tgtobj, tgtnamex, blah, level+1) + if tgtobj.macro or tgtobj.binary or tgtobj.any_err: + if blah: + tgtobj.dump( + bk.logfile, + header="!!! tgtobj has problems!!!", + footer="----------- --------", + ) + res = Operand(oUNK, None) + any_err = any_err or tgtobj.macro or tgtobj.binary or tgtobj.any_err + any_rel = any_rel or tgtobj.any_rel + else: + assert len(tgtobj.stack) == 1 + res = copy.deepcopy(tgtobj.stack[0]) + res.rank = LEAF_RANK + if tgtobj.scope == -1: + res.text = tgtobj.name + else: + res.text = "%s!%s" \ + % (bk._sheet_names[tgtobj.scope], tgtobj.name) + if blah: + print(" tName: setting text to", repr(res.text), file=bk.logfile) + spush(res) + elif opcode == 0x04: # tRef + # not_in_name_formula(op, oname) + res = get_cell_addr(data, pos+1, bv, reldelta) + if blah: print(" ", res, file=bk.logfile) + rowx, colx, row_rel, col_rel = res + shx1 = shx2 = 0 ####### N.B. relative to the CURRENT SHEET + any_rel = 1 + coords = (shx1, shx2+1, rowx, rowx+1, colx, colx+1) + if blah: print(" ", coords, file=bk.logfile) + res = Operand(oUNK, None) + if optype == 1: + relflags = (1, 1, row_rel, row_rel, col_rel, col_rel) + res = Operand(oREL, [Ref3D(coords + relflags)]) + spush(res) + elif opcode == 0x05: # tArea + # not_in_name_formula(op, oname) + res1, res2 = get_cell_range_addr(data, pos+1, bv, reldelta) + if blah: print(" ", res1, res2, file=bk.logfile) + rowx1, colx1, row_rel1, col_rel1 = res1 + rowx2, colx2, row_rel2, col_rel2 = res2 + shx1 = shx2 = 0 ####### N.B. relative to the CURRENT SHEET + any_rel = 1 + coords = (shx1, shx2+1, rowx1, rowx2+1, colx1, colx2+1) + if blah: print(" ", coords, file=bk.logfile) + res = Operand(oUNK, None) + if optype == 1: + relflags = (1, 1, row_rel1, row_rel2, col_rel1, col_rel2) + res = Operand(oREL, [Ref3D(coords + relflags)]) + spush(res) + elif opcode == 0x06: # tMemArea + not_in_name_formula(op, oname) + elif opcode == 0x09: # tMemFunc + nb = unpack("<H", data[pos+1:pos+3])[0] + if blah: print(" %d bytes of cell ref formula" % nb, file=bk.logfile) + # no effect on stack + elif opcode == 0x0C: #tRefN + not_in_name_formula(op, oname) + # res = get_cell_addr(data, pos+1, bv, reldelta=1) + # # note *ALL* tRefN usage has signed offset for relative addresses + # any_rel = 1 + # if blah: print >> bk.logfile, " ", res + # spush(res) + elif opcode == 0x0D: #tAreaN + not_in_name_formula(op, oname) + # res = get_cell_range_addr(data, pos+1, bv, reldelta=1) + # # note *ALL* tAreaN usage has signed offset for relative addresses + # any_rel = 1 + # if blah: print >> bk.logfile, " ", res + elif opcode == 0x1A: # tRef3d + if bv >= 80: + res = get_cell_addr(data, pos+3, bv, reldelta) + refx = unpack("<H", data[pos+1:pos+3])[0] + shx1, shx2 = get_externsheet_local_range(bk, refx, blah) + else: + res = get_cell_addr(data, pos+15, bv, reldelta) + raw_extshtx, raw_shx1, raw_shx2 = unpack("<hxxxxxxxxhh", data[pos+1:pos+15]) + if blah: + print("tRef3d", raw_extshtx, raw_shx1, raw_shx2, file=bk.logfile) + shx1, shx2 = get_externsheet_local_range_b57( + bk, raw_extshtx, raw_shx1, raw_shx2, blah) + rowx, colx, row_rel, col_rel = res + is_rel = row_rel or col_rel + any_rel = any_rel or is_rel + coords = (shx1, shx2+1, rowx, rowx+1, colx, colx+1) + any_err |= shx1 < -1 + if blah: print(" ", coords, file=bk.logfile) + res = Operand(oUNK, None) + if is_rel: + relflags = (0, 0, row_rel, row_rel, col_rel, col_rel) + ref3d = Ref3D(coords + relflags) + res.kind = oREL + res.text = rangename3drel(bk, ref3d, r1c1=1) + else: + ref3d = Ref3D(coords) + res.kind = oREF + res.text = rangename3d(bk, ref3d) + res.rank = LEAF_RANK + if optype == 1: + res.value = [ref3d] + spush(res) + elif opcode == 0x1B: # tArea3d + if bv >= 80: + res1, res2 = get_cell_range_addr(data, pos+3, bv, reldelta) + refx = unpack("<H", data[pos+1:pos+3])[0] + shx1, shx2 = get_externsheet_local_range(bk, refx, blah) + else: + res1, res2 = get_cell_range_addr(data, pos+15, bv, reldelta) + raw_extshtx, raw_shx1, raw_shx2 = unpack("<hxxxxxxxxhh", data[pos+1:pos+15]) + if blah: + print("tArea3d", raw_extshtx, raw_shx1, raw_shx2, file=bk.logfile) + shx1, shx2 = get_externsheet_local_range_b57( + bk, raw_extshtx, raw_shx1, raw_shx2, blah) + any_err |= shx1 < -1 + rowx1, colx1, row_rel1, col_rel1 = res1 + rowx2, colx2, row_rel2, col_rel2 = res2 + is_rel = row_rel1 or col_rel1 or row_rel2 or col_rel2 + any_rel = any_rel or is_rel + coords = (shx1, shx2+1, rowx1, rowx2+1, colx1, colx2+1) + if blah: print(" ", coords, file=bk.logfile) + res = Operand(oUNK, None) + if is_rel: + relflags = (0, 0, row_rel1, row_rel2, col_rel1, col_rel2) + ref3d = Ref3D(coords + relflags) + res.kind = oREL + res.text = rangename3drel(bk, ref3d, r1c1=1) + else: + ref3d = Ref3D(coords) + res.kind = oREF + res.text = rangename3d(bk, ref3d) + res.rank = LEAF_RANK + if optype == 1: + res.value = [ref3d] + + spush(res) + elif opcode == 0x19: # tNameX + dodgy = 0 + res = Operand(oUNK, None) + if bv >= 80: + refx, tgtnamex = unpack("<HH", data[pos+1:pos+5]) + tgtnamex -= 1 + origrefx = refx + else: + refx, tgtnamex = unpack("<hxxxxxxxxH", data[pos+1:pos+13]) + tgtnamex -= 1 + origrefx = refx + if refx > 0: + refx -= 1 + elif refx < 0: + refx = -refx - 1 + else: + dodgy = 1 + if blah: + print(" origrefx=%d refx=%d tgtnamex=%d dodgy=%d" + % (origrefx, refx, tgtnamex, dodgy), file=bk.logfile) + if tgtnamex == namex: + if blah: print("!!!! Self-referential !!!!", file=bk.logfile) + dodgy = any_err = 1 + if not dodgy: + if bv >= 80: + shx1, shx2 = get_externsheet_local_range(bk, refx, blah) + elif origrefx > 0: + shx1, shx2 = (-4, -4) # external ref + else: + exty = bk._externsheet_type_b57[refx] + if exty == 4: # non-specific sheet in own doc't + shx1, shx2 = (-1, -1) # internal, any sheet + else: + shx1, shx2 = (-666, -666) + if dodgy or shx1 < -1: + otext = "<<Name #%d in external(?) file #%d>>" \ + % (tgtnamex, origrefx) + res = Operand(oUNK, None, LEAF_RANK, otext) + else: + tgtobj = bk.name_obj_list[tgtnamex] + if not tgtobj.evaluated: + ### recursive ### + evaluate_name_formula(bk, tgtobj, tgtnamex, blah, level+1) + if tgtobj.macro or tgtobj.binary or tgtobj.any_err: + if blah: + tgtobj.dump( + bk.logfile, + header="!!! bad tgtobj !!!", + footer="------------------", + ) + res = Operand(oUNK, None) + any_err = any_err or tgtobj.macro or tgtobj.binary or tgtobj.any_err + any_rel = any_rel or tgtobj.any_rel + else: + assert len(tgtobj.stack) == 1 + res = copy.deepcopy(tgtobj.stack[0]) + res.rank = LEAF_RANK + if tgtobj.scope == -1: + res.text = tgtobj.name + else: + res.text = "%s!%s" \ + % (bk._sheet_names[tgtobj.scope], tgtobj.name) + if blah: + print(" tNameX: setting text to", repr(res.text), file=bk.logfile) + spush(res) + elif opcode in error_opcodes: + any_err = 1 + spush(error_opnd) + else: + if blah: + print("FORMULA: /// Not handled yet: t" + oname, file=bk.logfile) + any_err = 1 + if sz <= 0: + raise FormulaError("Fatal: token size is not positive") + pos += sz + any_rel = not not any_rel + if blah: + fprintf(bk.logfile, "End of formula. level=%d any_rel=%d any_err=%d stack=%r\n", + level, not not any_rel, any_err, stack) + if len(stack) >= 2: + print("*** Stack has unprocessed args", file=bk.logfile) + print(file=bk.logfile) + nobj.stack = stack + if len(stack) != 1: + nobj.result = None + else: + nobj.result = stack[0] + nobj.any_rel = any_rel + nobj.any_err = any_err + nobj.any_external = any_external + nobj.evaluated = 1 + +#### under construction ############################################################################# +def decompile_formula(bk, fmla, fmlalen, + fmlatype=None, browx=None, bcolx=None, + blah=0, level=0, r1c1=0): + if level > STACK_ALARM_LEVEL: + blah = 1 + reldelta = fmlatype in (FMLA_TYPE_SHARED, FMLA_TYPE_NAME, FMLA_TYPE_COND_FMT, FMLA_TYPE_DATA_VAL) + data = fmla + bv = bk.biff_version + if blah: + print("::: decompile_formula len=%d fmlatype=%r browx=%r bcolx=%r reldelta=%d %r level=%d" + % (fmlalen, fmlatype, browx, bcolx, reldelta, data, level), file=bk.logfile) + hex_char_dump(data, 0, fmlalen, fout=bk.logfile) + if level > STACK_PANIC_LEVEL: + raise XLRDError("Excessive indirect references in formula") + sztab = szdict[bv] + pos = 0 + stack = [] + any_rel = 0 + any_err = 0 + unk_opnd = Operand(oUNK, None) + error_opnd = Operand(oERR, None) + spush = stack.append + + def do_binop(opcd, stk): + assert len(stk) >= 2 + bop = stk.pop() + aop = stk.pop() + argdict, result_kind, func, rank, sym = binop_rules[opcd] + otext = ''.join([ + '('[:aop.rank < rank], + aop.text, + ')'[:aop.rank < rank], + sym, + '('[:bop.rank < rank], + bop.text, + ')'[:bop.rank < rank], + ]) + resop = Operand(result_kind, None, rank, otext) + stk.append(resop) + + def do_unaryop(opcode, result_kind, stk): + assert len(stk) >= 1 + aop = stk.pop() + func, rank, sym1, sym2 = unop_rules[opcode] + otext = ''.join([ + sym1, + '('[:aop.rank < rank], + aop.text, + ')'[:aop.rank < rank], + sym2, + ]) + stk.append(Operand(result_kind, None, rank, otext)) + + def unexpected_opcode(op_arg, oname_arg): + msg = "ERROR *** Unexpected token 0x%02x (%s) found in formula type %s" \ + % (op_arg, oname_arg, FMLA_TYPEDESCR_MAP[fmlatype]) + print(msg, file=bk.logfile) + # raise FormulaError(msg) + + if fmlalen == 0: + stack = [unk_opnd] + + while 0 <= pos < fmlalen: + op = BYTES_ORD(data[pos]) + opcode = op & 0x1f + optype = (op & 0x60) >> 5 + if optype: + opx = opcode + 32 + else: + opx = opcode + oname = onames[opx] # + [" RVA"][optype] + sz = sztab[opx] + if blah: + print("Pos:%d Op:0x%02x opname:t%s Sz:%d opcode:%02xh optype:%02xh" + % (pos, op, oname, sz, opcode, optype), file=bk.logfile) + print("Stack =", stack, file=bk.logfile) + if sz == -2: + msg = 'ERROR *** Unexpected token 0x%02x ("%s"); biff_version=%d' \ + % (op, oname, bv) + raise FormulaError(msg) + if _TOKEN_NOT_ALLOWED(opx, 0) & fmlatype: + unexpected_opcode(op, oname) + if not optype: + if opcode <= 0x01: # tExp + if bv >= 30: + fmt = '<x2H' + else: + fmt = '<xHB' + assert pos == 0 and fmlalen == sz and not stack + rowx, colx = unpack(fmt, data) + text = "SHARED FMLA at rowx=%d colx=%d" % (rowx, colx) + spush(Operand(oUNK, None, LEAF_RANK, text)) + if not fmlatype & (FMLA_TYPE_CELL | FMLA_TYPE_ARRAY): + unexpected_opcode(op, oname) + elif 0x03 <= opcode <= 0x0E: + # Add, Sub, Mul, Div, Power + # tConcat + # tLT, ..., tNE + do_binop(opcode, stack) + elif opcode == 0x0F: # tIsect + if blah: print("tIsect pre", stack, file=bk.logfile) + assert len(stack) >= 2 + bop = stack.pop() + aop = stack.pop() + sym = ' ' + rank = 80 ########## check ####### + otext = ''.join([ + '('[:aop.rank < rank], + aop.text, + ')'[:aop.rank < rank], + sym, + '('[:bop.rank < rank], + bop.text, + ')'[:bop.rank < rank], + ]) + res = Operand(oREF) + res.text = otext + if bop.kind == oERR or aop.kind == oERR: + res.kind = oERR + elif bop.kind == oUNK or aop.kind == oUNK: + # This can happen with undefined + # (go search in the current sheet) labels. + # For example =Bob Sales + # Each label gets a NAME record with an empty formula (!) + # Evaluation of the tName token classifies it as oUNK + # res.kind = oREF + pass + elif bop.kind == oREF == aop.kind: + pass + elif bop.kind == oREL == aop.kind: + res.kind = oREL + else: + pass + spush(res) + if blah: print("tIsect post", stack, file=bk.logfile) + elif opcode == 0x10: # tList + if blah: print("tList pre", stack, file=bk.logfile) + assert len(stack) >= 2 + bop = stack.pop() + aop = stack.pop() + sym = ',' + rank = 80 ########## check ####### + otext = ''.join([ + '('[:aop.rank < rank], + aop.text, + ')'[:aop.rank < rank], + sym, + '('[:bop.rank < rank], + bop.text, + ')'[:bop.rank < rank], + ]) + res = Operand(oREF, None, rank, otext) + if bop.kind == oERR or aop.kind == oERR: + res.kind = oERR + elif bop.kind in (oREF, oREL) and aop.kind in (oREF, oREL): + res.kind = oREF + if aop.kind == oREL or bop.kind == oREL: + res.kind = oREL + else: + pass + spush(res) + if blah: print("tList post", stack, file=bk.logfile) + elif opcode == 0x11: # tRange + if blah: print("tRange pre", stack, file=bk.logfile) + assert len(stack) >= 2 + bop = stack.pop() + aop = stack.pop() + sym = ':' + rank = 80 ########## check ####### + otext = ''.join([ + '('[:aop.rank < rank], + aop.text, + ')'[:aop.rank < rank], + sym, + '('[:bop.rank < rank], + bop.text, + ')'[:bop.rank < rank], + ]) + res = Operand(oREF, None, rank, otext) + if bop.kind == oERR or aop.kind == oERR: + res = oERR + elif bop.kind == oREF == aop.kind: + pass + else: + pass + spush(res) + if blah: print("tRange post", stack, file=bk.logfile) + elif 0x12 <= opcode <= 0x14: # tUplus, tUminus, tPercent + do_unaryop(opcode, oNUM, stack) + elif opcode == 0x15: # tParen + # source cosmetics + pass + elif opcode == 0x16: # tMissArg + spush(Operand(oMSNG, None, LEAF_RANK, '')) + elif opcode == 0x17: # tStr + if bv <= 70: + strg, newpos = unpack_string_update_pos( + data, pos+1, bk.encoding, lenlen=1) + else: + strg, newpos = unpack_unicode_update_pos( + data, pos+1, lenlen=1) + sz = newpos - pos + if blah: print(" sz=%d strg=%r" % (sz, strg), file=bk.logfile) + text = '"' + strg.replace('"', '""') + '"' + spush(Operand(oSTRG, None, LEAF_RANK, text)) + elif opcode == 0x18: # tExtended + # new with BIFF 8 + assert bv >= 80 + # not in OOo docs, don't even know how to determine its length + raise FormulaError("tExtended token not implemented") + elif opcode == 0x19: # tAttr + subop, nc = unpack("<BH", data[pos+1:pos+4]) + subname = tAttrNames.get(subop, "??Unknown??") + if subop == 0x04: # Choose + sz = nc * 2 + 6 + elif subop == 0x10: # Sum (single arg) + sz = 4 + if blah: print("tAttrSum", stack, file=bk.logfile) + assert len(stack) >= 1 + aop = stack[-1] + otext = 'SUM(%s)' % aop.text + stack[-1] = Operand(oNUM, None, FUNC_RANK, otext) + else: + sz = 4 + if blah: + print(" subop=%02xh subname=t%s sz=%d nc=%02xh" + % (subop, subname, sz, nc), file=bk.logfile) + elif 0x1A <= opcode <= 0x1B: # tSheet, tEndSheet + assert bv < 50 + raise FormulaError("tSheet & tEndsheet tokens not implemented") + elif 0x1C <= opcode <= 0x1F: # tErr, tBool, tInt, tNum + inx = opcode - 0x1C + nb = [1, 1, 2, 8][inx] + kind = [oERR, oBOOL, oNUM, oNUM][inx] + value, = unpack("<" + "BBHd"[inx], data[pos+1:pos+1+nb]) + if inx == 2: # tInt + value = float(value) + text = str(value) + elif inx == 3: # tNum + text = str(value) + elif inx == 1: # tBool + text = ('FALSE', 'TRUE')[value] + else: + text = '"' +error_text_from_code[value] + '"' + spush(Operand(kind, None, LEAF_RANK, text)) + else: + raise FormulaError("Unhandled opcode: 0x%02x" % opcode) + if sz <= 0: + raise FormulaError("Size not set for opcode 0x%02x" % opcode) + pos += sz + continue + if opcode == 0x00: # tArray + spush(unk_opnd) + elif opcode == 0x01: # tFunc + nb = 1 + int(bv >= 40) + funcx = unpack("<" + " BH"[nb], data[pos+1:pos+1+nb])[0] + func_attrs = func_defs.get(funcx, None) + if not func_attrs: + print("*** formula/tFunc unknown FuncID:%d" % funcx, file=bk.logfile) + spush(unk_opnd) + else: + func_name, nargs = func_attrs[:2] + if blah: + print(" FuncID=%d name=%s nargs=%d" + % (funcx, func_name, nargs), file=bk.logfile) + assert len(stack) >= nargs + if nargs: + argtext = listsep.join(arg.text for arg in stack[-nargs:]) + otext = "%s(%s)" % (func_name, argtext) + del stack[-nargs:] + else: + otext = func_name + "()" + res = Operand(oUNK, None, FUNC_RANK, otext) + spush(res) + elif opcode == 0x02: #tFuncVar + nb = 1 + int(bv >= 40) + nargs, funcx = unpack("<B" + " BH"[nb], data[pos+1:pos+2+nb]) + prompt, nargs = divmod(nargs, 128) + macro, funcx = divmod(funcx, 32768) + if blah: + print(" FuncID=%d nargs=%d macro=%d prompt=%d" + % (funcx, nargs, macro, prompt), file=bk.logfile) + #### TODO #### if funcx == 255: # call add-in function + if funcx == 255: + func_attrs = ("CALL_ADDIN", 1, 30) + else: + func_attrs = func_defs.get(funcx, None) + if not func_attrs: + print("*** formula/tFuncVar unknown FuncID:%d" + % funcx, file=bk.logfile) + spush(unk_opnd) + else: + func_name, minargs, maxargs = func_attrs[:3] + if blah: + print(" name: %r, min~max args: %d~%d" + % (func_name, minargs, maxargs), file=bk.logfile) + assert minargs <= nargs <= maxargs + assert len(stack) >= nargs + assert len(stack) >= nargs + argtext = listsep.join(arg.text for arg in stack[-nargs:]) + otext = "%s(%s)" % (func_name, argtext) + res = Operand(oUNK, None, FUNC_RANK, otext) + del stack[-nargs:] + spush(res) + elif opcode == 0x03: #tName + tgtnamex = unpack("<H", data[pos+1:pos+3])[0] - 1 + # Only change with BIFF version is number of trailing UNUSED bytes! + if blah: print(" tgtnamex=%d" % tgtnamex, file=bk.logfile) + tgtobj = bk.name_obj_list[tgtnamex] + if tgtobj.scope == -1: + otext = tgtobj.name + else: + otext = "%s!%s" % (bk._sheet_names[tgtobj.scope], tgtobj.name) + if blah: + print(" tName: setting text to", repr(otext), file=bk.logfile) + res = Operand(oUNK, None, LEAF_RANK, otext) + spush(res) + elif opcode == 0x04: # tRef + res = get_cell_addr(data, pos+1, bv, reldelta, browx, bcolx) + if blah: print(" ", res, file=bk.logfile) + rowx, colx, row_rel, col_rel = res + is_rel = row_rel or col_rel + if is_rel: + okind = oREL + else: + okind = oREF + otext = cellnamerel(rowx, colx, row_rel, col_rel, browx, bcolx, r1c1) + res = Operand(okind, None, LEAF_RANK, otext) + spush(res) + elif opcode == 0x05: # tArea + res1, res2 = get_cell_range_addr( + data, pos+1, bv, reldelta, browx, bcolx) + if blah: print(" ", res1, res2, file=bk.logfile) + rowx1, colx1, row_rel1, col_rel1 = res1 + rowx2, colx2, row_rel2, col_rel2 = res2 + coords = (rowx1, rowx2+1, colx1, colx2+1) + relflags = (row_rel1, row_rel2, col_rel1, col_rel2) + if sum(relflags): # relative + okind = oREL + else: + okind = oREF + if blah: print(" ", coords, relflags, file=bk.logfile) + otext = rangename2drel(coords, relflags, browx, bcolx, r1c1) + res = Operand(okind, None, LEAF_RANK, otext) + spush(res) + elif opcode == 0x06: # tMemArea + not_in_name_formula(op, oname) + elif opcode == 0x09: # tMemFunc + nb = unpack("<H", data[pos+1:pos+3])[0] + if blah: print(" %d bytes of cell ref formula" % nb, file=bk.logfile) + # no effect on stack + elif opcode == 0x0C: #tRefN + res = get_cell_addr(data, pos+1, bv, reldelta, browx, bcolx) + # note *ALL* tRefN usage has signed offset for relative addresses + any_rel = 1 + if blah: print(" ", res, file=bk.logfile) + rowx, colx, row_rel, col_rel = res + is_rel = row_rel or col_rel + if is_rel: + okind = oREL + else: + okind = oREF + otext = cellnamerel(rowx, colx, row_rel, col_rel, browx, bcolx, r1c1) + res = Operand(okind, None, LEAF_RANK, otext) + spush(res) + elif opcode == 0x0D: #tAreaN + # res = get_cell_range_addr(data, pos+1, bv, reldelta, browx, bcolx) + # # note *ALL* tAreaN usage has signed offset for relative addresses + # any_rel = 1 + # if blah: print >> bk.logfile, " ", res + res1, res2 = get_cell_range_addr( + data, pos+1, bv, reldelta, browx, bcolx) + if blah: print(" ", res1, res2, file=bk.logfile) + rowx1, colx1, row_rel1, col_rel1 = res1 + rowx2, colx2, row_rel2, col_rel2 = res2 + coords = (rowx1, rowx2+1, colx1, colx2+1) + relflags = (row_rel1, row_rel2, col_rel1, col_rel2) + if sum(relflags): # relative + okind = oREL + else: + okind = oREF + if blah: print(" ", coords, relflags, file=bk.logfile) + otext = rangename2drel(coords, relflags, browx, bcolx, r1c1) + res = Operand(okind, None, LEAF_RANK, otext) + spush(res) + elif opcode == 0x1A: # tRef3d + if bv >= 80: + res = get_cell_addr(data, pos+3, bv, reldelta, browx, bcolx) + refx = unpack("<H", data[pos+1:pos+3])[0] + shx1, shx2 = get_externsheet_local_range(bk, refx, blah) + else: + res = get_cell_addr(data, pos+15, bv, reldelta, browx, bcolx) + raw_extshtx, raw_shx1, raw_shx2 = unpack("<hxxxxxxxxhh", data[pos+1:pos+15]) + if blah: + print("tRef3d", raw_extshtx, raw_shx1, raw_shx2, file=bk.logfile) + shx1, shx2 = get_externsheet_local_range_b57( + bk, raw_extshtx, raw_shx1, raw_shx2, blah) + rowx, colx, row_rel, col_rel = res + is_rel = row_rel or col_rel + any_rel = any_rel or is_rel + coords = (shx1, shx2+1, rowx, rowx+1, colx, colx+1) + any_err |= shx1 < -1 + if blah: print(" ", coords, file=bk.logfile) + res = Operand(oUNK, None) + if is_rel: + relflags = (0, 0, row_rel, row_rel, col_rel, col_rel) + ref3d = Ref3D(coords + relflags) + res.kind = oREL + res.text = rangename3drel(bk, ref3d, browx, bcolx, r1c1) + else: + ref3d = Ref3D(coords) + res.kind = oREF + res.text = rangename3d(bk, ref3d) + res.rank = LEAF_RANK + res.value = None + spush(res) + elif opcode == 0x1B: # tArea3d + if bv >= 80: + res1, res2 = get_cell_range_addr(data, pos+3, bv, reldelta) + refx = unpack("<H", data[pos+1:pos+3])[0] + shx1, shx2 = get_externsheet_local_range(bk, refx, blah) + else: + res1, res2 = get_cell_range_addr(data, pos+15, bv, reldelta) + raw_extshtx, raw_shx1, raw_shx2 = unpack("<hxxxxxxxxhh", data[pos+1:pos+15]) + if blah: + print("tArea3d", raw_extshtx, raw_shx1, raw_shx2, file=bk.logfile) + shx1, shx2 = get_externsheet_local_range_b57( + bk, raw_extshtx, raw_shx1, raw_shx2, blah) + any_err |= shx1 < -1 + rowx1, colx1, row_rel1, col_rel1 = res1 + rowx2, colx2, row_rel2, col_rel2 = res2 + is_rel = row_rel1 or col_rel1 or row_rel2 or col_rel2 + any_rel = any_rel or is_rel + coords = (shx1, shx2+1, rowx1, rowx2+1, colx1, colx2+1) + if blah: print(" ", coords, file=bk.logfile) + res = Operand(oUNK, None) + if is_rel: + relflags = (0, 0, row_rel1, row_rel2, col_rel1, col_rel2) + ref3d = Ref3D(coords + relflags) + res.kind = oREL + res.text = rangename3drel(bk, ref3d, browx, bcolx, r1c1) + else: + ref3d = Ref3D(coords) + res.kind = oREF + res.text = rangename3d(bk, ref3d) + res.rank = LEAF_RANK + spush(res) + elif opcode == 0x19: # tNameX + dodgy = 0 + res = Operand(oUNK, None) + if bv >= 80: + refx, tgtnamex = unpack("<HH", data[pos+1:pos+5]) + tgtnamex -= 1 + origrefx = refx + else: + refx, tgtnamex = unpack("<hxxxxxxxxH", data[pos+1:pos+13]) + tgtnamex -= 1 + origrefx = refx + if refx > 0: + refx -= 1 + elif refx < 0: + refx = -refx - 1 + else: + dodgy = 1 + if blah: + print(" origrefx=%d refx=%d tgtnamex=%d dodgy=%d" + % (origrefx, refx, tgtnamex, dodgy), file=bk.logfile) + # if tgtnamex == namex: + # if blah: print >> bk.logfile, "!!!! Self-referential !!!!" + # dodgy = any_err = 1 + if not dodgy: + if bv >= 80: + shx1, shx2 = get_externsheet_local_range(bk, refx, blah) + elif origrefx > 0: + shx1, shx2 = (-4, -4) # external ref + else: + exty = bk._externsheet_type_b57[refx] + if exty == 4: # non-specific sheet in own doc't + shx1, shx2 = (-1, -1) # internal, any sheet + else: + shx1, shx2 = (-666, -666) + okind = oUNK + ovalue = None + if shx1 == -5: # addin func name + okind = oSTRG + ovalue = bk.addin_func_names[tgtnamex] + otext = '"' + ovalue.replace('"', '""') + '"' + elif dodgy or shx1 < -1: + otext = "<<Name #%d in external(?) file #%d>>" \ + % (tgtnamex, origrefx) + else: + tgtobj = bk.name_obj_list[tgtnamex] + if tgtobj.scope == -1: + otext = tgtobj.name + else: + otext = "%s!%s" \ + % (bk._sheet_names[tgtobj.scope], tgtobj.name) + if blah: + print(" tNameX: setting text to", repr(res.text), file=bk.logfile) + res = Operand(okind, ovalue, LEAF_RANK, otext) + spush(res) + elif opcode in error_opcodes: + any_err = 1 + spush(error_opnd) + else: + if blah: + print("FORMULA: /// Not handled yet: t" + oname, file=bk.logfile) + any_err = 1 + if sz <= 0: + raise FormulaError("Fatal: token size is not positive") + pos += sz + any_rel = not not any_rel + if blah: + print("End of formula. level=%d any_rel=%d any_err=%d stack=%r" % + (level, not not any_rel, any_err, stack), file=bk.logfile) + if len(stack) >= 2: + print("*** Stack has unprocessed args", file=bk.logfile) + print(file=bk.logfile) + + if len(stack) != 1: + result = None + else: + result = stack[0].text + return result + +#### under deconstruction ### +def dump_formula(bk, data, fmlalen, bv, reldelta, blah=0, isname=0): + if blah: + print("dump_formula", fmlalen, bv, len(data), file=bk.logfile) + hex_char_dump(data, 0, fmlalen, fout=bk.logfile) + assert bv >= 80 #### this function needs updating #### + sztab = szdict[bv] + pos = 0 + stack = [] + any_rel = 0 + any_err = 0 + spush = stack.append + while 0 <= pos < fmlalen: + op = BYTES_ORD(data[pos]) + opcode = op & 0x1f + optype = (op & 0x60) >> 5 + if optype: + opx = opcode + 32 + else: + opx = opcode + oname = onames[opx] # + [" RVA"][optype] + + sz = sztab[opx] + if blah: + print("Pos:%d Op:0x%02x Name:t%s Sz:%d opcode:%02xh optype:%02xh" + % (pos, op, oname, sz, opcode, optype), file=bk.logfile) + if not optype: + if 0x01 <= opcode <= 0x02: # tExp, tTbl + # reference to a shared formula or table record + rowx, colx = unpack("<HH", data[pos+1:pos+5]) + if blah: print(" ", (rowx, colx), file=bk.logfile) + elif opcode == 0x10: # tList + if blah: print("tList pre", stack, file=bk.logfile) + assert len(stack) >= 2 + bop = stack.pop() + aop = stack.pop() + spush(aop + bop) + if blah: print("tlist post", stack, file=bk.logfile) + elif opcode == 0x11: # tRange + if blah: print("tRange pre", stack, file=bk.logfile) + assert len(stack) >= 2 + bop = stack.pop() + aop = stack.pop() + assert len(aop) == 1 + assert len(bop) == 1 + result = do_box_funcs(tRangeFuncs, aop[0], bop[0]) + spush(result) + if blah: print("tRange post", stack, file=bk.logfile) + elif opcode == 0x0F: # tIsect + if blah: print("tIsect pre", stack, file=bk.logfile) + assert len(stack) >= 2 + bop = stack.pop() + aop = stack.pop() + assert len(aop) == 1 + assert len(bop) == 1 + result = do_box_funcs(tIsectFuncs, aop[0], bop[0]) + spush(result) + if blah: print("tIsect post", stack, file=bk.logfile) + elif opcode == 0x19: # tAttr + subop, nc = unpack("<BH", data[pos+1:pos+4]) + subname = tAttrNames.get(subop, "??Unknown??") + if subop == 0x04: # Choose + sz = nc * 2 + 6 + else: + sz = 4 + if blah: print(" subop=%02xh subname=t%s sz=%d nc=%02xh" % (subop, subname, sz, nc), file=bk.logfile) + elif opcode == 0x17: # tStr + if bv <= 70: + nc = BYTES_ORD(data[pos+1]) + strg = data[pos+2:pos+2+nc] # left in 8-bit encoding + sz = nc + 2 + else: + strg, newpos = unpack_unicode_update_pos(data, pos+1, lenlen=1) + sz = newpos - pos + if blah: print(" sz=%d strg=%r" % (sz, strg), file=bk.logfile) + else: + if sz <= 0: + print("**** Dud size; exiting ****", file=bk.logfile) + return + pos += sz + continue + if opcode == 0x00: # tArray + pass + elif opcode == 0x01: # tFunc + nb = 1 + int(bv >= 40) + funcx = unpack("<" + " BH"[nb], data[pos+1:pos+1+nb]) + if blah: print(" FuncID=%d" % funcx, file=bk.logfile) + elif opcode == 0x02: #tFuncVar + nb = 1 + int(bv >= 40) + nargs, funcx = unpack("<B" + " BH"[nb], data[pos+1:pos+2+nb]) + prompt, nargs = divmod(nargs, 128) + macro, funcx = divmod(funcx, 32768) + if blah: print(" FuncID=%d nargs=%d macro=%d prompt=%d" % (funcx, nargs, macro, prompt), file=bk.logfile) + elif opcode == 0x03: #tName + namex = unpack("<H", data[pos+1:pos+3]) + # Only change with BIFF version is the number of trailing UNUSED bytes!!! + if blah: print(" namex=%d" % namex, file=bk.logfile) + elif opcode == 0x04: # tRef + res = get_cell_addr(data, pos+1, bv, reldelta) + if blah: print(" ", res, file=bk.logfile) + elif opcode == 0x05: # tArea + res = get_cell_range_addr(data, pos+1, bv, reldelta) + if blah: print(" ", res, file=bk.logfile) + elif opcode == 0x09: # tMemFunc + nb = unpack("<H", data[pos+1:pos+3])[0] + if blah: print(" %d bytes of cell ref formula" % nb, file=bk.logfile) + elif opcode == 0x0C: #tRefN + res = get_cell_addr(data, pos+1, bv, reldelta=1) + # note *ALL* tRefN usage has signed offset for relative addresses + any_rel = 1 + if blah: print(" ", res, file=bk.logfile) + elif opcode == 0x0D: #tAreaN + res = get_cell_range_addr(data, pos+1, bv, reldelta=1) + # note *ALL* tAreaN usage has signed offset for relative addresses + any_rel = 1 + if blah: print(" ", res, file=bk.logfile) + elif opcode == 0x1A: # tRef3d + refx = unpack("<H", data[pos+1:pos+3])[0] + res = get_cell_addr(data, pos+3, bv, reldelta) + if blah: print(" ", refx, res, file=bk.logfile) + rowx, colx, row_rel, col_rel = res + any_rel = any_rel or row_rel or col_rel + shx1, shx2 = get_externsheet_local_range(bk, refx, blah) + any_err |= shx1 < -1 + coords = (shx1, shx2+1, rowx, rowx+1, colx, colx+1) + if blah: print(" ", coords, file=bk.logfile) + if optype == 1: spush([coords]) + elif opcode == 0x1B: # tArea3d + refx = unpack("<H", data[pos+1:pos+3])[0] + res1, res2 = get_cell_range_addr(data, pos+3, bv, reldelta) + if blah: print(" ", refx, res1, res2, file=bk.logfile) + rowx1, colx1, row_rel1, col_rel1 = res1 + rowx2, colx2, row_rel2, col_rel2 = res2 + any_rel = any_rel or row_rel1 or col_rel1 or row_rel2 or col_rel2 + shx1, shx2 = get_externsheet_local_range(bk, refx, blah) + any_err |= shx1 < -1 + coords = (shx1, shx2+1, rowx1, rowx2+1, colx1, colx2+1) + if blah: print(" ", coords, file=bk.logfile) + if optype == 1: spush([coords]) + elif opcode == 0x19: # tNameX + refx, namex = unpack("<HH", data[pos+1:pos+5]) + if blah: print(" refx=%d namex=%d" % (refx, namex), file=bk.logfile) + elif opcode in error_opcodes: + any_err = 1 + else: + if blah: print("FORMULA: /// Not handled yet: t" + oname, file=bk.logfile) + any_err = 1 + if sz <= 0: + print("**** Dud size; exiting ****", file=bk.logfile) + return + pos += sz + if blah: + print("End of formula. any_rel=%d any_err=%d stack=%r" % + (not not any_rel, any_err, stack), file=bk.logfile) + if len(stack) >= 2: + print("*** Stack has unprocessed args", file=bk.logfile) + +# === Some helper functions for displaying cell references === + +# I'm aware of only one possibility of a sheet-relative component in +# a reference: a 2D reference located in the "current sheet". +# xlrd stores this internally with bounds of (0, 1, ...) and +# relative flags of (1, 1, ...). These functions display the +# sheet component as empty, just like Excel etc. + +def rownamerel(rowx, rowxrel, browx=None, r1c1=0): + # if no base rowx is provided, we have to return r1c1 + if browx is None: + r1c1 = True + if not rowxrel: + if r1c1: + return "R%d" % (rowx+1) + return "$%d" % (rowx+1) + if r1c1: + if rowx: + return "R[%d]" % rowx + return "R" + return "%d" % ((browx + rowx) % 65536 + 1) + +def colnamerel(colx, colxrel, bcolx=None, r1c1=0): + # if no base colx is provided, we have to return r1c1 + if bcolx is None: + r1c1 = True + if not colxrel: + if r1c1: + return "C%d" % (colx + 1) + return "$" + colname(colx) + if r1c1: + if colx: + return "C[%d]" % colx + return "C" + return colname((bcolx + colx) % 256) + +def cellname(rowx, colx): + """Utility function: ``(5, 7)`` => ``'H6'``""" + return "%s%d" % (colname(colx), rowx+1) + +def cellnameabs(rowx, colx, r1c1=0): + """Utility function: ``(5, 7)`` => ``'$H$6'``""" + if r1c1: + return "R%dC%d" % (rowx+1, colx+1) + return "$%s$%d" % (colname(colx), rowx+1) + +def cellnamerel(rowx, colx, rowxrel, colxrel, browx=None, bcolx=None, r1c1=0): + if not rowxrel and not colxrel: + return cellnameabs(rowx, colx, r1c1) + if (rowxrel and browx is None) or (colxrel and bcolx is None): + # must flip the whole cell into R1C1 mode + r1c1 = True + c = colnamerel(colx, colxrel, bcolx, r1c1) + r = rownamerel(rowx, rowxrel, browx, r1c1) + if r1c1: + return r + c + return c + r + +def colname(colx): + """Utility function: ``7`` => ``'H'``, ``27`` => ``'AB'``""" + alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + if colx <= 25: + return alphabet[colx] + else: + xdiv26, xmod26 = divmod(colx, 26) + return alphabet[xdiv26 - 1] + alphabet[xmod26] + +def rangename2d(rlo, rhi, clo, chi, r1c1=0): + """ ``(5, 20, 7, 10)`` => ``'$H$6:$J$20'`` """ + if r1c1: + return + if rhi == rlo+1 and chi == clo+1: + return cellnameabs(rlo, clo, r1c1) + return "%s:%s" % (cellnameabs(rlo, clo, r1c1), cellnameabs(rhi-1, chi-1, r1c1)) + +def rangename2drel(rlo_rhi_clo_chi, rlorel_rhirel_clorel_chirel, browx=None, bcolx=None, r1c1=0): + rlo, rhi, clo, chi = rlo_rhi_clo_chi + rlorel, rhirel, clorel, chirel = rlorel_rhirel_clorel_chirel + if (rlorel or rhirel) and browx is None: + r1c1 = True + if (clorel or chirel) and bcolx is None: + r1c1 = True + return "%s:%s" % ( + cellnamerel(rlo, clo, rlorel, clorel, browx, bcolx, r1c1), + cellnamerel(rhi-1, chi-1, rhirel, chirel, browx, bcolx, r1c1), + ) + + +def rangename3d(book, ref3d): + """ + Utility function: + ``Ref3D(1, 4, 5, 20, 7, 10)`` => + ``'Sheet2:Sheet3!$H$6:$J$20'`` + (assuming Excel's default sheetnames) + """ + coords = ref3d.coords + return "%s!%s" % ( + sheetrange(book, *coords[:2]), + rangename2d(*coords[2:6])) + +def rangename3drel(book, ref3d, browx=None, bcolx=None, r1c1=0): + """ + Utility function: + ``Ref3D(coords=(0, 1, -32, -22, -13, 13), relflags=(0, 0, 1, 1, 1, 1))`` + + In R1C1 mode => ``'Sheet1!R[-32]C[-13]:R[-23]C[12]'`` + + In A1 mode => depends on base cell ``(browx, bcolx)`` + """ + coords = ref3d.coords + relflags = ref3d.relflags + shdesc = sheetrangerel(book, coords[:2], relflags[:2]) + rngdesc = rangename2drel(coords[2:6], relflags[2:6], browx, bcolx, r1c1) + if not shdesc: + return rngdesc + return "%s!%s" % (shdesc, rngdesc) + +def quotedsheetname(shnames, shx): + if shx >= 0: + shname = shnames[shx] + else: + shname = { + -1: "?internal; any sheet?", + -2: "internal; deleted sheet", + -3: "internal; macro sheet", + -4: "<<external>>", + }.get(shx, "?error %d?" % shx) + if "'" in shname: + return "'" + shname.replace("'", "''") + "'" + if " " in shname: + return "'" + shname + "'" + return shname + +def sheetrange(book, slo, shi): + shnames = book.sheet_names() + shdesc = quotedsheetname(shnames, slo) + if slo != shi-1: + shdesc += ":" + quotedsheetname(shnames, shi-1) + return shdesc + +def sheetrangerel(book, srange, srangerel): + slo, shi = srange + slorel, shirel = srangerel + if not slorel and not shirel: + return sheetrange(book, slo, shi) + assert (slo == 0 == shi-1) and slorel and shirel + return "" + +# ============================================================== diff --git a/.venv/lib/python3.12/site-packages/xlrd/info.py b/.venv/lib/python3.12/site-packages/xlrd/info.py new file mode 100644 index 00000000..f26b6bb2 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlrd/info.py @@ -0,0 +1 @@ +__version__ = __VERSION__ = "2.0.1" diff --git a/.venv/lib/python3.12/site-packages/xlrd/sheet.py b/.venv/lib/python3.12/site-packages/xlrd/sheet.py new file mode 100644 index 00000000..a8313980 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlrd/sheet.py @@ -0,0 +1,2490 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2005-2013 Stephen John Machin, Lingfo Pty Ltd +# This module is part of the xlrd package, which is released under a +# BSD-style licence. + +from __future__ import print_function + +from array import array +from struct import calcsize, unpack + +from .biffh import * +from .formatting import Format, nearest_colour_index +from .formula import ( + FMLA_TYPE_CELL, FMLA_TYPE_SHARED, decompile_formula, dump_formula, + rangename2d, +) +from .timemachine import * + +DEBUG = 0 +OBJ_MSO_DEBUG = 0 + +_WINDOW2_options = ( + # Attribute names and initial values to use in case + # a WINDOW2 record is not written. + ("show_formulas", 0), + ("show_grid_lines", 1), + ("show_sheet_headers", 1), + ("panes_are_frozen", 0), + ("show_zero_values", 1), + ("automatic_grid_line_colour", 1), + ("columns_from_right_to_left", 0), + ("show_outline_symbols", 1), + ("remove_splits_if_pane_freeze_is_removed", 0), + # Multiple sheets can be selected, but only one can be active + # (hold down Ctrl and click multiple tabs in the file in OOo) + ("sheet_selected", 0), + # "sheet_visible" should really be called "sheet_active" + # and is 1 when this sheet is the sheet displayed when the file + # is open. More than likely only one sheet should ever be set as + # visible. + # This would correspond to the Book's sheet_active attribute, but + # that doesn't exist as WINDOW1 records aren't currently processed. + # The real thing is the visibility attribute from the BOUNDSHEET record. + ("sheet_visible", 0), + ("show_in_page_break_preview", 0), +) + + + +class Sheet(BaseObject): + """ + Contains the data for one worksheet. + + In the cell access functions, ``rowx`` is a row index, counting from + zero, and ``colx`` is a column index, counting from zero. + Negative values for row/column indexes and slice positions are supported in + the expected fashion. + + For information about cell types and cell values, refer to the documentation + of the :class:`Cell` class. + + .. warning:: + + You don't instantiate this class yourself. You access :class:`Sheet` + objects via the :class:`~xlrd.book.Book` object that + was returned when you called :func:`xlrd.open_workbook`. + """ + + #: Name of sheet. + name = '' + + #: A reference to the :class:`~xlrd.book.Book` object to which this sheet + #: belongs. + #: + #: Example usage: ``some_sheet.book.datemode`` + book = None + + #: Number of rows in sheet. A row index is in ``range(thesheet.nrows)``. + nrows = 0 + + #: Nominal number of columns in sheet. It is one more than the maximum + #: column index found, ignoring trailing empty cells. + #: See also the ``ragged_rows`` parameter to :func:`~xlrd.open_workbook` + #: and :meth:`~xlrd.sheet.Sheet.row_len`. + ncols = 0 + + + #: The map from a column index to a :class:`Colinfo` object. Often there is + #: an entry in ``COLINFO`` records for all column indexes in ``range(257)``. + #: + #: .. note:: + #: xlrd ignores the entry for the non-existent + #: 257th column. + #: + #: On the other hand, there may be no entry for unused columns. + #: + #: .. versionadded:: 0.6.1 + #: + #: Populated only if ``open_workbook(..., formatting_info=True)`` + colinfo_map = {} + + #: The map from a row index to a :class:`Rowinfo` object. + #: + #: ..note:: + #: It is possible to have missing entries -- at least one source of + #: XLS files doesn't bother writing ``ROW`` records. + #: + #: .. versionadded:: 0.6.1 + #: + #: Populated only if ``open_workbook(..., formatting_info=True)`` + rowinfo_map = {} + + #: List of address ranges of cells containing column labels. + #: These are set up in Excel by Insert > Name > Labels > Columns. + #: + #: .. versionadded:: 0.6.0 + #: + #: How to deconstruct the list: + #: + #: .. code-block:: python + #: + #: for crange in thesheet.col_label_ranges: + #: rlo, rhi, clo, chi = crange + #: for rx in xrange(rlo, rhi): + #: for cx in xrange(clo, chi): + #: print "Column label at (rowx=%d, colx=%d) is %r" \ + #: (rx, cx, thesheet.cell_value(rx, cx)) + col_label_ranges = [] + + #: List of address ranges of cells containing row labels. + #: For more details, see :attr:`col_label_ranges`. + #: + #: .. versionadded:: 0.6.0 + row_label_ranges = [] + + #: List of address ranges of cells which have been merged. + #: These are set up in Excel by Format > Cells > Alignment, then ticking + #: the "Merge cells" box. + #: + #: .. note:: + #: The upper limits are exclusive: i.e. ``[2, 3, 7, 9]`` only + #: spans two cells. + #: + #: .. note:: Extracted only if ``open_workbook(..., formatting_info=True)`` + #: + #: .. versionadded:: 0.6.1 + #: + #: How to deconstruct the list: + #: + #: .. code-block:: python + #: + #: for crange in thesheet.merged_cells: + #: rlo, rhi, clo, chi = crange + #: for rowx in xrange(rlo, rhi): + #: for colx in xrange(clo, chi): + #: # cell (rlo, clo) (the top left one) will carry the data + #: # and formatting info; the remainder will be recorded as + #: # blank cells, but a renderer will apply the formatting info + #: # for the top left cell (e.g. border, pattern) to all cells in + #: # the range. + merged_cells = [] + + #: Mapping of ``(rowx, colx)`` to list of ``(offset, font_index)`` tuples. + #: The offset defines where in the string the font begins to be used. + #: Offsets are expected to be in ascending order. + #: If the first offset is not zero, the meaning is that the cell's ``XF``'s + #: font should be used from offset 0. + #: + #: This is a sparse mapping. There is no entry for cells that are not + #: formatted with rich text. + #: + #: How to use: + #: + #: .. code-block:: python + #: + #: runlist = thesheet.rich_text_runlist_map.get((rowx, colx)) + #: if runlist: + #: for offset, font_index in runlist: + #: # do work here. + #: pass + #: + #: .. versionadded:: 0.7.2 + #: + #: Populated only if ``open_workbook(..., formatting_info=True)`` + rich_text_runlist_map = {} + + #: Default column width from ``DEFCOLWIDTH`` record, else ``None``. + #: From the OOo docs: + #: + #: Column width in characters, using the width of the zero character + #: from default font (first FONT record in the file). Excel adds some + #: extra space to the default width, depending on the default font and + #: default font size. The algorithm how to exactly calculate the resulting + #: column width is not known. + #: Example: The default width of 8 set in this record results in a column + #: width of 8.43 using Arial font with a size of 10 points. + #: + #: For the default hierarchy, refer to the :class:`Colinfo` class. + #: + #: .. versionadded:: 0.6.1 + defcolwidth = None + + #: Default column width from ``STANDARDWIDTH`` record, else ``None``. + #: + #: From the OOo docs: + #: + #: Default width of the columns in 1/256 of the width of the zero + #: character, using default font (first FONT record in the file). + #: + #: For the default hierarchy, refer to the :class:`Colinfo` class. + #: + #: .. versionadded:: 0.6.1 + standardwidth = None + + #: Default value to be used for a row if there is + #: no ``ROW`` record for that row. + #: From the *optional* ``DEFAULTROWHEIGHT`` record. + default_row_height = None + + #: Default value to be used for a row if there is + #: no ``ROW`` record for that row. + #: From the *optional* ``DEFAULTROWHEIGHT`` record. + default_row_height_mismatch = None + + #: Default value to be used for a row if there is + #: no ``ROW`` record for that row. + #: From the *optional* ``DEFAULTROWHEIGHT`` record. + default_row_hidden = None + + #: Default value to be used for a row if there is + #: no ``ROW`` record for that row. + #: From the *optional* ``DEFAULTROWHEIGHT`` record. + default_additional_space_above = None + + #: Default value to be used for a row if there is + #: no ``ROW`` record for that row. + #: From the *optional* ``DEFAULTROWHEIGHT`` record. + default_additional_space_below = None + + #: Visibility of the sheet: + #: :: + #: + #: 0 = visible + #: 1 = hidden (can be unhidden by user -- Format -> Sheet -> Unhide) + #: 2 = "very hidden" (can be unhidden only by VBA macro). + visibility = 0 + + #: A 256-element tuple corresponding to the contents of the GCW record for + #: this sheet. If no such record, treat as all bits zero. + #: Applies to BIFF4-7 only. See docs of the :class:`Colinfo` class for + #: discussion. + gcw = (0, ) * 256 + + #: A list of :class:`Hyperlink` objects corresponding to ``HLINK`` records + #: found in the worksheet. + #: + #: .. versionadded:: 0.7.2 + hyperlink_list = [] + + #: A sparse mapping from ``(rowx, colx)`` to an item in + #: :attr:`~xlrd.sheet.Sheet.hyperlink_list`. + #: Cells not covered by a hyperlink are not mapped. + #: It is possible using the Excel UI to set up a hyperlink that + #: covers a larger-than-1x1 rectangle of cells. + #: Hyperlink rectangles may overlap (Excel doesn't check). + #: When a multiply-covered cell is clicked on, the hyperlink that is + #: activated + #: (and the one that is mapped here) is the last in + #: :attr:`~xlrd.sheet.Sheet.hyperlink_list`. + #: + #: .. versionadded:: 0.7.2 + hyperlink_map = {} + + #: A sparse mapping from ``(rowx, colx)`` to a :class:`Note` object. + #: Cells not containing a note ("comment") are not mapped. + #: + #: .. versionadded:: 0.7.2 + cell_note_map = {} + + #: Number of columns in left pane (frozen panes; for split panes, see + #: comments in code) + vert_split_pos = 0 + + #: Number of rows in top pane (frozen panes; for split panes, see comments + #: in code) + horz_split_pos = 0 + + #: Index of first visible row in bottom frozen/split pane + horz_split_first_visible = 0 + + #: Index of first visible column in right frozen/split pane + vert_split_first_visible = 0 + + #: Frozen panes: ignore it. Split panes: explanation and diagrams in + #: OOo docs. + split_active_pane = 0 + + #: Boolean specifying if a ``PANE`` record was present, ignore unless you're + #: ``xlutils.copy`` + has_pane_record = 0 + + #: A list of the horizontal page breaks in this sheet. + #: Breaks are tuples in the form + #: ``(index of row after break, start col index, end col index)``. + #: + #: Populated only if ``open_workbook(..., formatting_info=True)`` + #: + #: .. versionadded:: 0.7.2 + horizontal_page_breaks = [] + + #: A list of the vertical page breaks in this sheet. + #: Breaks are tuples in the form + #: ``(index of col after break, start row index, end row index)``. + #: + #: Populated only if ``open_workbook(..., formatting_info=True)`` + #: + #: .. versionadded:: 0.7.2 + vertical_page_breaks = [] + + def __init__(self, book, position, name, number): + self.book = book + self.biff_version = book.biff_version + self._position = position + self.logfile = book.logfile + self.bt = array('B', [XL_CELL_EMPTY]) + self.bf = array('h', [-1]) + self.name = name + self.number = number + self.verbosity = book.verbosity + self.formatting_info = book.formatting_info + self.ragged_rows = book.ragged_rows + if self.ragged_rows: + self.put_cell = self.put_cell_ragged + else: + self.put_cell = self.put_cell_unragged + self._xf_index_to_xl_type_map = book._xf_index_to_xl_type_map + self.nrows = 0 # actual, including possibly empty cells + self.ncols = 0 + self._maxdatarowx = -1 # highest rowx containing a non-empty cell + self._maxdatacolx = -1 # highest colx containing a non-empty cell + self._dimnrows = 0 # as per DIMENSIONS record + self._dimncols = 0 + self._cell_values = [] + self._cell_types = [] + self._cell_xf_indexes = [] + self.defcolwidth = None + self.standardwidth = None + self.default_row_height = None + self.default_row_height_mismatch = 0 + self.default_row_hidden = 0 + self.default_additional_space_above = 0 + self.default_additional_space_below = 0 + self.colinfo_map = {} + self.rowinfo_map = {} + self.col_label_ranges = [] + self.row_label_ranges = [] + self.merged_cells = [] + self.rich_text_runlist_map = {} + self.horizontal_page_breaks = [] + self.vertical_page_breaks = [] + self._xf_index_stats = [0, 0, 0, 0] + self.visibility = book._sheet_visibility[number] # from BOUNDSHEET record + for attr, defval in _WINDOW2_options: + setattr(self, attr, defval) + self.first_visible_rowx = 0 + self.first_visible_colx = 0 + self.gridline_colour_index = 0x40 + self.gridline_colour_rgb = None # pre-BIFF8 + self.hyperlink_list = [] + self.hyperlink_map = {} + self.cell_note_map = {} + + # Values calculated by xlrd to predict the mag factors that + # will actually be used by Excel to display your worksheet. + # Pass these values to xlwt when writing XLS files. + # Warning 1: Behaviour of OOo Calc and Gnumeric has been observed to differ from Excel's. + # Warning 2: A value of zero means almost exactly what it says. Your sheet will be + # displayed as a very tiny speck on the screen. xlwt will reject attempts to set + # a mag_factor that is not (10 <= mag_factor <= 400). + self.cooked_page_break_preview_mag_factor = 60 + self.cooked_normal_view_mag_factor = 100 + + # Values (if any) actually stored on the XLS file + self.cached_page_break_preview_mag_factor = 0 # default (60%), from WINDOW2 record + self.cached_normal_view_mag_factor = 0 # default (100%), from WINDOW2 record + self.scl_mag_factor = None # from SCL record + + self._ixfe = None # BIFF2 only + self._cell_attr_to_xfx = {} # BIFF2.0 only + + if self.biff_version >= 80: + self.utter_max_rows = 65536 + else: + self.utter_max_rows = 16384 + self.utter_max_cols = 256 + + self._first_full_rowx = -1 + + # self._put_cell_exceptions = 0 + # self._put_cell_row_widenings = 0 + # self._put_cell_rows_appended = 0 + # self._put_cell_cells_appended = 0 + + def cell(self, rowx, colx): + """ + :class:`Cell` object in the given row and column. + """ + if self.formatting_info: + xfx = self.cell_xf_index(rowx, colx) + else: + xfx = None + return Cell( + self._cell_types[rowx][colx], + self._cell_values[rowx][colx], + xfx, + ) + + def cell_value(self, rowx, colx): + "Value of the cell in the given row and column." + return self._cell_values[rowx][colx] + + def cell_type(self, rowx, colx): + """ + Type of the cell in the given row and column. + + Refer to the documentation of the :class:`Cell` class. + """ + return self._cell_types[rowx][colx] + + def cell_xf_index(self, rowx, colx): + """ + XF index of the cell in the given row and column. + This is an index into :attr:`~xlrd.book.Book.xf_list`. + + .. versionadded:: 0.6.1 + """ + self.req_fmt_info() + xfx = self._cell_xf_indexes[rowx][colx] + if xfx > -1: + self._xf_index_stats[0] += 1 + return xfx + # Check for a row xf_index + try: + xfx = self.rowinfo_map[rowx].xf_index + if xfx > -1: + self._xf_index_stats[1] += 1 + return xfx + except KeyError: + pass + # Check for a column xf_index + try: + xfx = self.colinfo_map[colx].xf_index + if xfx == -1: xfx = 15 + self._xf_index_stats[2] += 1 + return xfx + except KeyError: + # If all else fails, 15 is used as hardwired global default xf_index. + self._xf_index_stats[3] += 1 + return 15 + + def row_len(self, rowx): + """ + Returns the effective number of cells in the given row. For use with + ``open_workbook(ragged_rows=True)`` which is likely to produce rows + with fewer than :attr:`~Sheet.ncols` cells. + + .. versionadded:: 0.7.2 + """ + return len(self._cell_values[rowx]) + + def row(self, rowx): + """ + Returns a sequence of the :class:`Cell` objects in the given row. + """ + return [ + self.cell(rowx, colx) + for colx in xrange(len(self._cell_values[rowx])) + ] + + def __getitem__(self, item): + """ + Takes either rowindex or (rowindex, colindex) as an index, + and returns either row or cell respectively. + """ + try: + rowix, colix = item + except TypeError: + # it's not a tuple (or of right size), let's try indexing as is + # if this is a problem, let this error propagate back + return self.row(item) + else: + return self.cell(rowix, colix) + + def get_rows(self): + "Returns a generator for iterating through each row." + return (self.row(index) for index in range(self.nrows)) + + # makes `for row in sheet` natural and intuitive + __iter__ = get_rows + + def row_types(self, rowx, start_colx=0, end_colx=None): + """ + Returns a slice of the types of the cells in the given row. + """ + if end_colx is None: + return self._cell_types[rowx][start_colx:] + return self._cell_types[rowx][start_colx:end_colx] + + def row_values(self, rowx, start_colx=0, end_colx=None): + """ + Returns a slice of the values of the cells in the given row. + """ + if end_colx is None: + return self._cell_values[rowx][start_colx:] + return self._cell_values[rowx][start_colx:end_colx] + + def row_slice(self, rowx, start_colx=0, end_colx=None): + """ + Returns a slice of the :class:`Cell` objects in the given row. + """ + nc = len(self._cell_values[rowx]) + if start_colx < 0: + start_colx += nc + if start_colx < 0: + start_colx = 0 + if end_colx is None or end_colx > nc: + end_colx = nc + elif end_colx < 0: + end_colx += nc + return [ + self.cell(rowx, colx) + for colx in xrange(start_colx, end_colx) + ] + + def col_slice(self, colx, start_rowx=0, end_rowx=None): + """ + Returns a slice of the :class:`Cell` objects in the given column. + """ + nr = self.nrows + if start_rowx < 0: + start_rowx += nr + if start_rowx < 0: + start_rowx = 0 + if end_rowx is None or end_rowx > nr: + end_rowx = nr + elif end_rowx < 0: + end_rowx += nr + return [ + self.cell(rowx, colx) + for rowx in xrange(start_rowx, end_rowx) + ] + + def col_values(self, colx, start_rowx=0, end_rowx=None): + """ + Returns a slice of the values of the cells in the given column. + """ + nr = self.nrows + if start_rowx < 0: + start_rowx += nr + if start_rowx < 0: + start_rowx = 0 + if end_rowx is None or end_rowx > nr: + end_rowx = nr + elif end_rowx < 0: + end_rowx += nr + return [ + self._cell_values[rowx][colx] + for rowx in xrange(start_rowx, end_rowx) + ] + + def col_types(self, colx, start_rowx=0, end_rowx=None): + """ + Returns a slice of the types of the cells in the given column. + """ + nr = self.nrows + if start_rowx < 0: + start_rowx += nr + if start_rowx < 0: + start_rowx = 0 + if end_rowx is None or end_rowx > nr: + end_rowx = nr + elif end_rowx < 0: + end_rowx += nr + return [ + self._cell_types[rowx][colx] + for rowx in xrange(start_rowx, end_rowx) + ] + + col = col_slice + + # === Following methods are used in building the worksheet. + # === They are not part of the API. + + def tidy_dimensions(self): + if self.verbosity >= 3: + fprintf( + self.logfile, + "tidy_dimensions: nrows=%d ncols=%d \n", + self.nrows, self.ncols, + ) + if 1 and self.merged_cells: + nr = nc = 0 + umaxrows = self.utter_max_rows + umaxcols = self.utter_max_cols + for crange in self.merged_cells: + rlo, rhi, clo, chi = crange + if not (0 <= rlo < rhi <= umaxrows) or not (0 <= clo < chi <= umaxcols): + fprintf(self.logfile, + "*** WARNING: sheet #%d (%r), MERGEDCELLS bad range %r\n", + self.number, self.name, crange) + if rhi > nr: nr = rhi + if chi > nc: nc = chi + if nc > self.ncols: + self.ncols = nc + self._first_full_rowx = -2 + if nr > self.nrows: + # we put one empty cell at (nr-1,0) to make sure + # we have the right number of rows. The ragged rows + # will sort out the rest if needed. + self.put_cell(nr-1, 0, XL_CELL_EMPTY, UNICODE_LITERAL(''), -1) + if (self.verbosity >= 1 and + (self.nrows != self._dimnrows or self.ncols != self._dimncols)): + fprintf( + self.logfile, + "NOTE *** sheet %d (%r): DIMENSIONS R,C = %d,%d should be %d,%d\n", + self.number, + self.name, + self._dimnrows, + self._dimncols, + self.nrows, + self.ncols, + ) + if not self.ragged_rows: + # fix ragged rows + ncols = self.ncols + s_cell_types = self._cell_types + s_cell_values = self._cell_values + s_cell_xf_indexes = self._cell_xf_indexes + s_fmt_info = self.formatting_info + # for rowx in xrange(self.nrows): + if self._first_full_rowx == -2: + ubound = self.nrows + else: + ubound = self._first_full_rowx + for rowx in xrange(ubound): + trow = s_cell_types[rowx] + rlen = len(trow) + nextra = ncols - rlen + if nextra > 0: + s_cell_values[rowx][rlen:] = [UNICODE_LITERAL('')] * nextra + trow[rlen:] = self.bt * nextra + if s_fmt_info: + s_cell_xf_indexes[rowx][rlen:] = self.bf * nextra + + def put_cell_ragged(self, rowx, colx, ctype, value, xf_index): + if ctype is None: + # we have a number, so look up the cell type + ctype = self._xf_index_to_xl_type_map[xf_index] + assert 0 <= colx < self.utter_max_cols + assert 0 <= rowx < self.utter_max_rows + fmt_info = self.formatting_info + + try: + nr = rowx + 1 + if self.nrows < nr: + + scta = self._cell_types.append + scva = self._cell_values.append + scxa = self._cell_xf_indexes.append + bt = self.bt + bf = self.bf + for _unused in xrange(self.nrows, nr): + scta(bt * 0) + scva([]) + if fmt_info: + scxa(bf * 0) + self.nrows = nr + + types_row = self._cell_types[rowx] + values_row = self._cell_values[rowx] + if fmt_info: + fmt_row = self._cell_xf_indexes[rowx] + ltr = len(types_row) + if colx >= self.ncols: + self.ncols = colx + 1 + num_empty = colx - ltr + if not num_empty: + # most common case: colx == previous colx + 1 + # self._put_cell_cells_appended += 1 + types_row.append(ctype) + values_row.append(value) + if fmt_info: + fmt_row.append(xf_index) + return + if num_empty > 0: + num_empty += 1 + # self._put_cell_row_widenings += 1 + # types_row.extend(self.bt * num_empty) + # values_row.extend([UNICODE_LITERAL('')] * num_empty) + # if fmt_info: + # fmt_row.extend(self.bf * num_empty) + types_row[ltr:] = self.bt * num_empty + values_row[ltr:] = [UNICODE_LITERAL('')] * num_empty + if fmt_info: + fmt_row[ltr:] = self.bf * num_empty + types_row[colx] = ctype + values_row[colx] = value + if fmt_info: + fmt_row[colx] = xf_index + except: + print("put_cell", rowx, colx, file=self.logfile) + raise + + def put_cell_unragged(self, rowx, colx, ctype, value, xf_index): + if ctype is None: + # we have a number, so look up the cell type + ctype = self._xf_index_to_xl_type_map[xf_index] + # assert 0 <= colx < self.utter_max_cols + # assert 0 <= rowx < self.utter_max_rows + try: + self._cell_types[rowx][colx] = ctype + self._cell_values[rowx][colx] = value + if self.formatting_info: + self._cell_xf_indexes[rowx][colx] = xf_index + except IndexError: + # print >> self.logfile, "put_cell extending", rowx, colx + # self.extend_cells(rowx+1, colx+1) + # self._put_cell_exceptions += 1 + nr = rowx + 1 + nc = colx + 1 + assert 1 <= nc <= self.utter_max_cols + assert 1 <= nr <= self.utter_max_rows + if nc > self.ncols: + self.ncols = nc + # The row self._first_full_rowx and all subsequent rows + # are guaranteed to have length == self.ncols. Thus the + # "fix ragged rows" section of the tidy_dimensions method + # doesn't need to examine them. + if nr < self.nrows: + # cell data is not in non-descending row order *AND* + # self.ncols has been bumped up. + # This very rare case ruins this optimisation. + self._first_full_rowx = -2 + elif rowx > self._first_full_rowx > -2: + self._first_full_rowx = rowx + if nr <= self.nrows: + # New cell is in an existing row, so extend that row (if necessary). + # Note that nr < self.nrows means that the cell data + # is not in ascending row order!! + trow = self._cell_types[rowx] + nextra = self.ncols - len(trow) + if nextra > 0: + # self._put_cell_row_widenings += 1 + trow.extend(self.bt * nextra) + if self.formatting_info: + self._cell_xf_indexes[rowx].extend(self.bf * nextra) + self._cell_values[rowx].extend([UNICODE_LITERAL('')] * nextra) + else: + scta = self._cell_types.append + scva = self._cell_values.append + scxa = self._cell_xf_indexes.append + fmt_info = self.formatting_info + nc = self.ncols + bt = self.bt + bf = self.bf + for _unused in xrange(self.nrows, nr): + # self._put_cell_rows_appended += 1 + scta(bt * nc) + scva([UNICODE_LITERAL('')] * nc) + if fmt_info: + scxa(bf * nc) + self.nrows = nr + # === end of code from extend_cells() + try: + self._cell_types[rowx][colx] = ctype + self._cell_values[rowx][colx] = value + if self.formatting_info: + self._cell_xf_indexes[rowx][colx] = xf_index + except: + print("put_cell", rowx, colx, file=self.logfile) + raise + except: + print("put_cell", rowx, colx, file=self.logfile) + raise + + + # === Methods after this line neither know nor care about how cells are stored. + + def read(self, bk): + global rc_stats + DEBUG = 0 + blah = DEBUG or self.verbosity >= 2 + blah_rows = DEBUG or self.verbosity >= 4 + blah_formulas = 0 and blah + r1c1 = 0 + oldpos = bk._position + bk._position = self._position + XL_SHRFMLA_ETC_ETC = ( + XL_SHRFMLA, XL_ARRAY, XL_TABLEOP, XL_TABLEOP2, + XL_ARRAY2, XL_TABLEOP_B2, + ) + self_put_cell = self.put_cell + local_unpack = unpack + bk_get_record_parts = bk.get_record_parts + bv = self.biff_version + fmt_info = self.formatting_info + do_sst_rich_text = fmt_info and bk._rich_text_runlist_map + rowinfo_sharing_dict = {} + txos = {} + eof_found = 0 + while 1: + # if DEBUG: print "SHEET.READ: about to read from position %d" % bk._position + rc, data_len, data = bk_get_record_parts() + # if rc in rc_stats: + # rc_stats[rc] += 1 + # else: + # rc_stats[rc] = 1 + # if DEBUG: print "SHEET.READ: op 0x%04x, %d bytes %r" % (rc, data_len, data) + if rc == XL_NUMBER: + # [:14] in following stmt ignores extraneous rubbish at end of record. + # Sample file testEON-8.xls supplied by Jan Kraus. + rowx, colx, xf_index, d = local_unpack('<HHHd', data[:14]) + # if xf_index == 0: + # fprintf(self.logfile, + # "NUMBER: r=%d c=%d xfx=%d %f\n", rowx, colx, xf_index, d) + self_put_cell(rowx, colx, None, d, xf_index) + elif rc == XL_LABELSST: + rowx, colx, xf_index, sstindex = local_unpack('<HHHi', data) + # print "LABELSST", rowx, colx, sstindex, bk._sharedstrings[sstindex] + self_put_cell(rowx, colx, XL_CELL_TEXT, bk._sharedstrings[sstindex], xf_index) + if do_sst_rich_text: + runlist = bk._rich_text_runlist_map.get(sstindex) + if runlist: + self.rich_text_runlist_map[(rowx, colx)] = runlist + elif rc == XL_LABEL: + rowx, colx, xf_index = local_unpack('<HHH', data[0:6]) + if bv < BIFF_FIRST_UNICODE: + strg = unpack_string(data, 6, bk.encoding or bk.derive_encoding(), lenlen=2) + else: + strg = unpack_unicode(data, 6, lenlen=2) + self_put_cell(rowx, colx, XL_CELL_TEXT, strg, xf_index) + elif rc == XL_RSTRING: + rowx, colx, xf_index = local_unpack('<HHH', data[0:6]) + if bv < BIFF_FIRST_UNICODE: + strg, pos = unpack_string_update_pos(data, 6, bk.encoding or bk.derive_encoding(), lenlen=2) + nrt = BYTES_ORD(data[pos]) + pos += 1 + runlist = [] + for _unused in xrange(nrt): + runlist.append(unpack('<BB', data[pos:pos+2])) + pos += 2 + assert pos == len(data) + else: + strg, pos = unpack_unicode_update_pos(data, 6, lenlen=2) + nrt = unpack('<H', data[pos:pos+2])[0] + pos += 2 + runlist = [] + for _unused in xrange(nrt): + runlist.append(unpack('<HH', data[pos:pos+4])) + pos += 4 + assert pos == len(data) + self_put_cell(rowx, colx, XL_CELL_TEXT, strg, xf_index) + self.rich_text_runlist_map[(rowx, colx)] = runlist + elif rc == XL_RK: + rowx, colx, xf_index = local_unpack('<HHH', data[:6]) + d = unpack_RK(data[6:10]) + self_put_cell(rowx, colx, None, d, xf_index) + elif rc == XL_MULRK: + mulrk_row, mulrk_first = local_unpack('<HH', data[0:4]) + mulrk_last, = local_unpack('<H', data[-2:]) + pos = 4 + for colx in xrange(mulrk_first, mulrk_last+1): + xf_index, = local_unpack('<H', data[pos:pos+2]) + d = unpack_RK(data[pos+2:pos+6]) + pos += 6 + self_put_cell(mulrk_row, colx, None, d, xf_index) + elif rc == XL_ROW: + # Version 0.6.0a3: ROW records are just not worth using (for memory allocation). + # Version 0.6.1: now used for formatting info. + if not fmt_info: continue + rowx, bits1, bits2 = local_unpack('<H4xH4xi', data[0:16]) + if not(0 <= rowx < self.utter_max_rows): + print("*** NOTE: ROW record has row index %d; " + "should have 0 <= rowx < %d -- record ignored!" + % (rowx, self.utter_max_rows), file=self.logfile) + continue + key = (bits1, bits2) + r = rowinfo_sharing_dict.get(key) + if r is None: + rowinfo_sharing_dict[key] = r = Rowinfo() + # Using upkbits() is far too slow on a file + # with 30 sheets each with 10K rows :-( + # upkbits(r, bits1, ( + # ( 0, 0x7FFF, 'height'), + # (15, 0x8000, 'has_default_height'), + # )) + # upkbits(r, bits2, ( + # ( 0, 0x00000007, 'outline_level'), + # ( 4, 0x00000010, 'outline_group_starts_ends'), + # ( 5, 0x00000020, 'hidden'), + # ( 6, 0x00000040, 'height_mismatch'), + # ( 7, 0x00000080, 'has_default_xf_index'), + # (16, 0x0FFF0000, 'xf_index'), + # (28, 0x10000000, 'additional_space_above'), + # (29, 0x20000000, 'additional_space_below'), + # )) + # So: + r.height = bits1 & 0x7fff + r.has_default_height = (bits1 >> 15) & 1 + r.outline_level = bits2 & 7 + r.outline_group_starts_ends = (bits2 >> 4) & 1 + r.hidden = (bits2 >> 5) & 1 + r.height_mismatch = (bits2 >> 6) & 1 + r.has_default_xf_index = (bits2 >> 7) & 1 + r.xf_index = (bits2 >> 16) & 0xfff + r.additional_space_above = (bits2 >> 28) & 1 + r.additional_space_below = (bits2 >> 29) & 1 + if not r.has_default_xf_index: + r.xf_index = -1 + self.rowinfo_map[rowx] = r + if 0 and r.xf_index > -1: + fprintf(self.logfile, + "**ROW %d %d %d\n", + self.number, rowx, r.xf_index) + if blah_rows: + print('ROW', rowx, bits1, bits2, file=self.logfile) + r.dump(self.logfile, + header="--- sh #%d, rowx=%d ---" % (self.number, rowx)) + elif rc in XL_FORMULA_OPCODES: # 06, 0206, 0406 + # DEBUG = 1 + # if DEBUG: print "FORMULA: rc: 0x%04x data: %r" % (rc, data) + if bv >= 50: + rowx, colx, xf_index, result_str, flags = local_unpack('<HHH8sH', data[0:16]) + elif bv >= 30: + rowx, colx, xf_index, result_str, flags = local_unpack('<HHH8sH', data[0:16]) + else: # BIFF2 + rowx, colx, cell_attr, result_str, flags = local_unpack('<HH3s8sB', data[0:16]) + xf_index = self.fixed_BIFF2_xfindex(cell_attr, rowx, colx) + if blah_formulas: # testing formula dumper + #### XXXX FIXME + fprintf(self.logfile, "FORMULA: rowx=%d colx=%d\n", rowx, colx) + fmlalen = local_unpack("<H", data[20:22])[0] + decompile_formula(bk, data[22:], fmlalen, FMLA_TYPE_CELL, + browx=rowx, bcolx=colx, blah=1, r1c1=r1c1) + if result_str[6:8] == b"\xFF\xFF": + first_byte = BYTES_ORD(result_str[0]) + if first_byte == 0: + # need to read next record (STRING) + gotstring = 0 + # if flags & 8: + if 1: # "flags & 8" applies only to SHRFMLA + # actually there's an optional SHRFMLA or ARRAY etc record to skip over + rc2, data2_len, data2 = bk.get_record_parts() + if rc2 == XL_STRING or rc2 == XL_STRING_B2: + gotstring = 1 + elif rc2 == XL_ARRAY: + row1x, rownx, col1x, colnx, array_flags, tokslen = \ + local_unpack("<HHBBBxxxxxH", data2[:14]) + if blah_formulas: + fprintf(self.logfile, "ARRAY: %d %d %d %d %d\n", + row1x, rownx, col1x, colnx, array_flags) + # dump_formula(bk, data2[14:], tokslen, bv, reldelta=0, blah=1) + elif rc2 == XL_SHRFMLA: + row1x, rownx, col1x, colnx, nfmlas, tokslen = \ + local_unpack("<HHBBxBH", data2[:10]) + if blah_formulas: + fprintf(self.logfile, "SHRFMLA (sub): %d %d %d %d %d\n", + row1x, rownx, col1x, colnx, nfmlas) + decompile_formula(bk, data2[10:], tokslen, FMLA_TYPE_SHARED, + blah=1, browx=rowx, bcolx=colx, r1c1=r1c1) + elif rc2 not in XL_SHRFMLA_ETC_ETC: + raise XLRDError( + "Expected SHRFMLA, ARRAY, TABLEOP* or STRING record; found 0x%04x" % rc2) + # if DEBUG: print "gotstring:", gotstring + # now for the STRING record + if not gotstring: + rc2, _unused_len, data2 = bk.get_record_parts() + if rc2 not in (XL_STRING, XL_STRING_B2): + raise XLRDError("Expected STRING record; found 0x%04x" % rc2) + # if DEBUG: print "STRING: data=%r BIFF=%d cp=%d" % (data2, self.biff_version, bk.encoding) + strg = self.string_record_contents(data2) + self.put_cell(rowx, colx, XL_CELL_TEXT, strg, xf_index) + # if DEBUG: print "FORMULA strg %r" % strg + elif first_byte == 1: + # boolean formula result + value = BYTES_ORD(result_str[2]) + self_put_cell(rowx, colx, XL_CELL_BOOLEAN, value, xf_index) + elif first_byte == 2: + # Error in cell + value = BYTES_ORD(result_str[2]) + self_put_cell(rowx, colx, XL_CELL_ERROR, value, xf_index) + elif first_byte == 3: + # empty ... i.e. empty (zero-length) string, NOT an empty cell. + self_put_cell(rowx, colx, XL_CELL_TEXT, "", xf_index) + else: + raise XLRDError("unexpected special case (0x%02x) in FORMULA" % first_byte) + else: + # it is a number + d = local_unpack('<d', result_str)[0] + self_put_cell(rowx, colx, None, d, xf_index) + elif rc == XL_BOOLERR: + rowx, colx, xf_index, value, is_err = local_unpack('<HHHBB', data[:8]) + # Note OOo Calc 2.0 writes 9-byte BOOLERR records. + # OOo docs say 8. Excel writes 8. + cellty = (XL_CELL_BOOLEAN, XL_CELL_ERROR)[is_err] + # if DEBUG: print "XL_BOOLERR", rowx, colx, xf_index, value, is_err + self_put_cell(rowx, colx, cellty, value, xf_index) + elif rc == XL_COLINFO: + if not fmt_info: continue + c = Colinfo() + first_colx, last_colx, c.width, c.xf_index, flags \ + = local_unpack("<HHHHH", data[:10]) + #### Colinfo.width is denominated in 256ths of a character, + #### *not* in characters. + if not(0 <= first_colx <= last_colx <= 256): + # Note: 256 instead of 255 is a common mistake. + # We silently ignore the non-existing 257th column in that case. + print("*** NOTE: COLINFO record has first col index %d, last %d; " + "should have 0 <= first <= last <= 255 -- record ignored!" + % (first_colx, last_colx), file=self.logfile) + del c + continue + upkbits(c, flags, ( + ( 0, 0x0001, 'hidden'), + ( 1, 0x0002, 'bit1_flag'), + # *ALL* colinfos created by Excel in "default" cases are 0x0002!! + # Maybe it's "locked" by analogy with XFProtection data. + ( 8, 0x0700, 'outline_level'), + (12, 0x1000, 'collapsed'), + )) + for colx in xrange(first_colx, last_colx+1): + if colx > 255: break # Excel does 0 to 256 inclusive + self.colinfo_map[colx] = c + if 0: + fprintf(self.logfile, + "**COL %d %d %d\n", + self.number, colx, c.xf_index) + if blah: + fprintf( + self.logfile, + "COLINFO sheet #%d cols %d-%d: wid=%d xf_index=%d flags=0x%04x\n", + self.number, first_colx, last_colx, c.width, c.xf_index, flags, + ) + c.dump(self.logfile, header='===') + elif rc == XL_DEFCOLWIDTH: + self.defcolwidth, = local_unpack("<H", data[:2]) + if 0: print('DEFCOLWIDTH', self.defcolwidth, file=self.logfile) + elif rc == XL_STANDARDWIDTH: + if data_len != 2: + print('*** ERROR *** STANDARDWIDTH', data_len, repr(data), file=self.logfile) + self.standardwidth, = local_unpack("<H", data[:2]) + if 0: print('STANDARDWIDTH', self.standardwidth, file=self.logfile) + elif rc == XL_GCW: + if not fmt_info: continue # useless w/o COLINFO + assert data_len == 34 + assert data[0:2] == b"\x20\x00" + iguff = unpack("<8i", data[2:34]) + gcw = [] + for bits in iguff: + for j in xrange(32): + gcw.append(bits & 1) + bits >>= 1 + self.gcw = tuple(gcw) + if 0: + showgcw = "".join(map(lambda x: "F "[x], gcw)).rstrip().replace(' ', '.') + print("GCW:", showgcw, file=self.logfile) + elif rc == XL_BLANK: + if not fmt_info: continue + rowx, colx, xf_index = local_unpack('<HHH', data[:6]) + # if 0: print >> self.logfile, "BLANK", rowx, colx, xf_index + self_put_cell(rowx, colx, XL_CELL_BLANK, '', xf_index) + elif rc == XL_MULBLANK: # 00BE + if not fmt_info: continue + nitems = data_len >> 1 + result = local_unpack("<%dH" % nitems, data) + rowx, mul_first = result[:2] + mul_last = result[-1] + # print >> self.logfile, "MULBLANK", rowx, mul_first, mul_last, data_len, nitems, mul_last + 4 - mul_first + assert nitems == mul_last + 4 - mul_first + pos = 2 + for colx in xrange(mul_first, mul_last + 1): + self_put_cell(rowx, colx, XL_CELL_BLANK, '', result[pos]) + pos += 1 + elif rc == XL_DIMENSION or rc == XL_DIMENSION2: + if data_len == 0: + # Four zero bytes after some other record. See github issue 64. + continue + # if data_len == 10: + # Was crashing on BIFF 4.0 file w/o the two trailing unused bytes. + # Reported by Ralph Heimburger. + if bv < 80: + dim_tuple = local_unpack('<HxxH', data[2:8]) + else: + dim_tuple = local_unpack('<ixxH', data[4:12]) + self.nrows, self.ncols = 0, 0 + self._dimnrows, self._dimncols = dim_tuple + if bv in (21, 30, 40) and self.book.xf_list and not self.book._xf_epilogue_done: + self.book.xf_epilogue() + if blah: + fprintf( + self.logfile, + "sheet %d(%r) DIMENSIONS: ncols=%d nrows=%d\n", + self.number, self.name, self._dimncols, self._dimnrows + ) + elif rc == XL_HLINK: + self.handle_hlink(data) + elif rc == XL_QUICKTIP: + self.handle_quicktip(data) + elif rc == XL_EOF: + DEBUG = 0 + if DEBUG: print("SHEET.READ: EOF", file=self.logfile) + eof_found = 1 + break + elif rc == XL_OBJ: + # handle SHEET-level objects; note there's a separate Book.handle_obj + saved_obj = self.handle_obj(data) + if saved_obj: saved_obj_id = saved_obj.id + else: saved_obj_id = None + elif rc == XL_MSO_DRAWING: + self.handle_msodrawingetc(rc, data_len, data) + elif rc == XL_TXO: + txo = self.handle_txo(data) + if txo and saved_obj_id: + txos[saved_obj_id] = txo + saved_obj_id = None + elif rc == XL_NOTE: + self.handle_note(data, txos) + elif rc == XL_FEAT11: + self.handle_feat11(data) + elif rc in bofcodes: ##### EMBEDDED BOF ##### + version, boftype = local_unpack('<HH', data[0:4]) + if boftype != 0x20: # embedded chart + print("*** Unexpected embedded BOF (0x%04x) at offset %d: version=0x%04x type=0x%04x" + % (rc, bk._position - data_len - 4, version, boftype), file=self.logfile) + while 1: + code, data_len, data = bk.get_record_parts() + if code == XL_EOF: + break + if DEBUG: print("---> found EOF", file=self.logfile) + elif rc == XL_COUNTRY: + bk.handle_country(data) + elif rc == XL_LABELRANGES: + pos = 0 + pos = unpack_cell_range_address_list_update_pos( + self.row_label_ranges, data, pos, bv, addr_size=8, + ) + pos = unpack_cell_range_address_list_update_pos( + self.col_label_ranges, data, pos, bv, addr_size=8, + ) + assert pos == data_len + elif rc == XL_ARRAY: + row1x, rownx, col1x, colnx, array_flags, tokslen = \ + local_unpack("<HHBBBxxxxxH", data[:14]) + if blah_formulas: + print("ARRAY:", row1x, rownx, col1x, colnx, array_flags, file=self.logfile) + # dump_formula(bk, data[14:], tokslen, bv, reldelta=0, blah=1) + elif rc == XL_SHRFMLA: + row1x, rownx, col1x, colnx, nfmlas, tokslen = \ + local_unpack("<HHBBxBH", data[:10]) + if blah_formulas: + print("SHRFMLA (main):", row1x, rownx, col1x, colnx, nfmlas, file=self.logfile) + decompile_formula(bk, data[10:], tokslen, FMLA_TYPE_SHARED, + blah=1, browx=rowx, bcolx=colx, r1c1=r1c1) + elif rc == XL_CONDFMT: + if not fmt_info: continue + assert bv >= 80 + num_CFs, needs_recalc, browx1, browx2, bcolx1, bcolx2 = \ + unpack("<6H", data[0:12]) + if self.verbosity >= 1: + fprintf( + self.logfile, + "\n*** WARNING: Ignoring CONDFMT (conditional formatting) record\n" + "*** in Sheet %d (%r).\n" + "*** %d CF record(s); needs_recalc_or_redraw = %d\n" + "*** Bounding box is %s\n", + self.number, self.name, num_CFs, needs_recalc, + rangename2d(browx1, browx2+1, bcolx1, bcolx2+1), + ) + olist = [] # updated by the function + pos = unpack_cell_range_address_list_update_pos( + olist, data, 12, bv, addr_size=8) + # print >> self.logfile, repr(result), len(result) + if self.verbosity >= 1: + fprintf( + self.logfile, + "*** %d individual range(s):\n" + "*** %s\n", + len(olist), + ", ".join(rangename2d(*coords) for coords in olist), + ) + elif rc == XL_CF: + if not fmt_info: continue + cf_type, cmp_op, sz1, sz2, flags = unpack("<BBHHi", data[0:10]) + font_block = (flags >> 26) & 1 + bord_block = (flags >> 28) & 1 + patt_block = (flags >> 29) & 1 + if self.verbosity >= 1: + fprintf( + self.logfile, + "\n*** WARNING: Ignoring CF (conditional formatting) sub-record.\n" + "*** cf_type=%d, cmp_op=%d, sz1=%d, sz2=%d, flags=0x%08x\n" + "*** optional data blocks: font=%d, border=%d, pattern=%d\n", + cf_type, cmp_op, sz1, sz2, flags, + font_block, bord_block, patt_block, + ) + # hex_char_dump(data, 0, data_len, fout=self.logfile) + pos = 12 + if font_block: + (font_height, font_options, weight, escapement, underline, + font_colour_index, two_bits, font_esc, font_underl) = unpack("<64x i i H H B 3x i 4x i i i 18x", data[pos:pos+118]) + font_style = (two_bits > 1) & 1 + posture = (font_options > 1) & 1 + font_canc = (two_bits > 7) & 1 + cancellation = (font_options > 7) & 1 + if self.verbosity >= 1: + fprintf( + self.logfile, + "*** Font info: height=%d, weight=%d, escapement=%d,\n" + "*** underline=%d, colour_index=%d, esc=%d, underl=%d,\n" + "*** style=%d, posture=%d, canc=%d, cancellation=%d\n", + font_height, weight, escapement, underline, + font_colour_index, font_esc, font_underl, + font_style, posture, font_canc, cancellation, + ) + pos += 118 + if bord_block: + pos += 8 + if patt_block: + pos += 4 + fmla1 = data[pos:pos+sz1] + pos += sz1 + if blah and sz1: + fprintf(self.logfile, "*** formula 1:\n") + dump_formula(bk, fmla1, sz1, bv, reldelta=0, blah=1) + fmla2 = data[pos:pos+sz2] + pos += sz2 + assert pos == data_len + if blah and sz2: + fprintf(self.logfile, "*** formula 2:\n") + dump_formula(bk, fmla2, sz2, bv, reldelta=0, blah=1) + elif rc == XL_DEFAULTROWHEIGHT: + if data_len == 4: + bits, self.default_row_height = unpack("<HH", data[:4]) + elif data_len == 2: + self.default_row_height, = unpack("<H", data) + bits = 0 + fprintf(self.logfile, + "*** WARNING: DEFAULTROWHEIGHT record len is 2, " + "should be 4; assuming BIFF2 format\n") + else: + bits = 0 + fprintf(self.logfile, + "*** WARNING: DEFAULTROWHEIGHT record len is %d, " + "should be 4; ignoring this record\n", + data_len) + self.default_row_height_mismatch = bits & 1 + self.default_row_hidden = (bits >> 1) & 1 + self.default_additional_space_above = (bits >> 2) & 1 + self.default_additional_space_below = (bits >> 3) & 1 + elif rc == XL_MERGEDCELLS: + if not fmt_info: continue + pos = unpack_cell_range_address_list_update_pos( + self.merged_cells, data, 0, bv, addr_size=8) + if blah: + fprintf(self.logfile, + "MERGEDCELLS: %d ranges\n", (pos - 2) // 8) + assert pos == data_len, \ + "MERGEDCELLS: pos=%d data_len=%d" % (pos, data_len) + elif rc == XL_WINDOW2: + if bv >= 80 and data_len >= 14: + ( + options, + self.first_visible_rowx, self.first_visible_colx, + self.gridline_colour_index, + self.cached_page_break_preview_mag_factor, + self.cached_normal_view_mag_factor + ) = unpack("<HHHHxxHH", data[:14]) + else: + assert bv >= 30 # BIFF3-7 + ( + options, + self.first_visible_rowx, self.first_visible_colx, + ) = unpack("<HHH", data[:6]) + self.gridline_colour_rgb = unpack("<BBB", data[6:9]) + self.gridline_colour_index = nearest_colour_index( + self.book.colour_map, self.gridline_colour_rgb, debug=0) + # options -- Bit, Mask, Contents: + # 0 0001H 0 = Show formula results 1 = Show formulas + # 1 0002H 0 = Do not show grid lines 1 = Show grid lines + # 2 0004H 0 = Do not show sheet headers 1 = Show sheet headers + # 3 0008H 0 = Panes are not frozen 1 = Panes are frozen (freeze) + # 4 0010H 0 = Show zero values as empty cells 1 = Show zero values + # 5 0020H 0 = Manual grid line colour 1 = Automatic grid line colour + # 6 0040H 0 = Columns from left to right 1 = Columns from right to left + # 7 0080H 0 = Do not show outline symbols 1 = Show outline symbols + # 8 0100H 0 = Keep splits if pane freeze is removed 1 = Remove splits if pane freeze is removed + # 9 0200H 0 = Sheet not selected 1 = Sheet selected (BIFF5-BIFF8) + # 10 0400H 0 = Sheet not visible 1 = Sheet visible (BIFF5-BIFF8) + # 11 0800H 0 = Show in normal view 1 = Show in page break preview (BIFF8) + # The freeze flag specifies, if a following PANE record (6.71) describes unfrozen or frozen panes. + for attr, _unused_defval in _WINDOW2_options: + setattr(self, attr, options & 1) + options >>= 1 + elif rc == XL_SCL: + num, den = unpack("<HH", data) + result = 0 + if den: + result = (num * 100) // den + if not(10 <= result <= 400): + if DEBUG or self.verbosity >= 0: + print( + "WARNING *** SCL rcd sheet %d: should have 0.1 <= num/den <= 4; got %d/%d" + % (self.number, num, den), + file=self.logfile, + ) + result = 100 + self.scl_mag_factor = result + elif rc == XL_PANE: + ( + self.vert_split_pos, + self.horz_split_pos, + self.horz_split_first_visible, + self.vert_split_first_visible, + self.split_active_pane, + ) = unpack("<HHHHB", data[:9]) + self.has_pane_record = 1 + elif rc == XL_HORIZONTALPAGEBREAKS: + if not fmt_info: continue + num_breaks, = local_unpack("<H", data[:2]) + assert num_breaks * (2 + 4 * (bv >= 80)) + 2 == data_len + pos = 2 + if bv < 80: + while pos < data_len: + self.horizontal_page_breaks.append((local_unpack("<H", data[pos:pos+2])[0], 0, 255)) + pos += 2 + else: + while pos < data_len: + self.horizontal_page_breaks.append(local_unpack("<HHH", data[pos:pos+6])) + pos += 6 + elif rc == XL_VERTICALPAGEBREAKS: + if not fmt_info: continue + num_breaks, = local_unpack("<H", data[:2]) + assert num_breaks * (2 + 4 * (bv >= 80)) + 2 == data_len + pos = 2 + if bv < 80: + while pos < data_len: + self.vertical_page_breaks.append((local_unpack("<H", data[pos:pos+2])[0], 0, 65535)) + pos += 2 + else: + while pos < data_len: + self.vertical_page_breaks.append(local_unpack("<HHH", data[pos:pos+6])) + pos += 6 + #### all of the following are for BIFF <= 4W + elif bv <= 45: + if rc == XL_FORMAT or rc == XL_FORMAT2: + bk.handle_format(data, rc) + elif rc == XL_FONT or rc == XL_FONT_B3B4: + bk.handle_font(data) + elif rc == XL_STYLE: + if not self.book._xf_epilogue_done: + self.book.xf_epilogue() + bk.handle_style(data) + elif rc == XL_PALETTE: + bk.handle_palette(data) + elif rc == XL_BUILTINFMTCOUNT: + bk.handle_builtinfmtcount(data) + elif rc == XL_XF4 or rc == XL_XF3 or rc == XL_XF2: #### N.B. not XL_XF + bk.handle_xf(data) + elif rc == XL_DATEMODE: + bk.handle_datemode(data) + elif rc == XL_CODEPAGE: + bk.handle_codepage(data) + elif rc == XL_FILEPASS: + bk.handle_filepass(data) + elif rc == XL_WRITEACCESS: + bk.handle_writeaccess(data) + elif rc == XL_IXFE: + self._ixfe = local_unpack('<H', data)[0] + elif rc == XL_NUMBER_B2: + rowx, colx, cell_attr, d = local_unpack('<HH3sd', data) + self_put_cell(rowx, colx, None, d, self.fixed_BIFF2_xfindex(cell_attr, rowx, colx)) + elif rc == XL_INTEGER: + rowx, colx, cell_attr, d = local_unpack('<HH3sH', data) + self_put_cell(rowx, colx, None, float(d), self.fixed_BIFF2_xfindex(cell_attr, rowx, colx)) + elif rc == XL_LABEL_B2: + rowx, colx, cell_attr = local_unpack('<HH3s', data[0:7]) + strg = unpack_string(data, 7, bk.encoding or bk.derive_encoding(), lenlen=1) + self_put_cell(rowx, colx, XL_CELL_TEXT, strg, self.fixed_BIFF2_xfindex(cell_attr, rowx, colx)) + elif rc == XL_BOOLERR_B2: + rowx, colx, cell_attr, value, is_err = local_unpack('<HH3sBB', data) + cellty = (XL_CELL_BOOLEAN, XL_CELL_ERROR)[is_err] + # if DEBUG: print "XL_BOOLERR_B2", rowx, colx, cell_attr, value, is_err + self_put_cell(rowx, colx, cellty, value, self.fixed_BIFF2_xfindex(cell_attr, rowx, colx)) + elif rc == XL_BLANK_B2: + if not fmt_info: continue + rowx, colx, cell_attr = local_unpack('<HH3s', data[:7]) + self_put_cell(rowx, colx, XL_CELL_BLANK, '', self.fixed_BIFF2_xfindex(cell_attr, rowx, colx)) + elif rc == XL_EFONT: + bk.handle_efont(data) + elif rc == XL_ROW_B2: + if not fmt_info: continue + rowx, bits1, bits2 = local_unpack('<H4xH2xB', data[0:11]) + if not(0 <= rowx < self.utter_max_rows): + print("*** NOTE: ROW_B2 record has row index %d; " + "should have 0 <= rowx < %d -- record ignored!" + % (rowx, self.utter_max_rows), file=self.logfile) + continue + if not (bits2 & 1): # has_default_xf_index is false + xf_index = -1 + elif data_len == 18: + # Seems the XF index in the cell_attr is dodgy + xfx = local_unpack('<H', data[16:18])[0] + xf_index = self.fixed_BIFF2_xfindex(cell_attr=None, rowx=rowx, colx=-1, true_xfx=xfx) + else: + cell_attr = data[13:16] + xf_index = self.fixed_BIFF2_xfindex(cell_attr, rowx, colx=-1) + key = (bits1, bits2, xf_index) + r = rowinfo_sharing_dict.get(key) + if r is None: + rowinfo_sharing_dict[key] = r = Rowinfo() + r.height = bits1 & 0x7fff + r.has_default_height = (bits1 >> 15) & 1 + r.has_default_xf_index = bits2 & 1 + r.xf_index = xf_index + # r.outline_level = 0 # set in __init__ + # r.outline_group_starts_ends = 0 # set in __init__ + # r.hidden = 0 # set in __init__ + # r.height_mismatch = 0 # set in __init__ + # r.additional_space_above = 0 # set in __init__ + # r.additional_space_below = 0 # set in __init__ + self.rowinfo_map[rowx] = r + if 0 and r.xf_index > -1: + fprintf(self.logfile, + "**ROW %d %d %d\n", + self.number, rowx, r.xf_index) + if blah_rows: + print('ROW_B2', rowx, bits1, file=self.logfile) + r.dump(self.logfile, + header="--- sh #%d, rowx=%d ---" % (self.number, rowx)) + elif rc == XL_COLWIDTH: # BIFF2 only + if not fmt_info: continue + first_colx, last_colx, width\ + = local_unpack("<BBH", data[:4]) + if not(first_colx <= last_colx): + print("*** NOTE: COLWIDTH record has first col index %d, last %d; " + "should have first <= last -- record ignored!" + % (first_colx, last_colx), file=self.logfile) + continue + for colx in xrange(first_colx, last_colx+1): + if colx in self.colinfo_map: + c = self.colinfo_map[colx] + else: + c = Colinfo() + self.colinfo_map[colx] = c + c.width = width + if blah: + fprintf( + self.logfile, + "COLWIDTH sheet #%d cols %d-%d: wid=%d\n", + self.number, first_colx, last_colx, width, + ) + elif rc == XL_COLUMNDEFAULT: # BIFF2 only + if not fmt_info: continue + first_colx, last_colx = local_unpack("<HH", data[:4]) + #### Warning OOo docs wrong; first_colx <= colx < last_colx + if blah: + fprintf( + self.logfile, + "COLUMNDEFAULT sheet #%d cols in range(%d, %d)\n", + self.number, first_colx, last_colx, + ) + if not(0 <= first_colx < last_colx <= 256): + print("*** NOTE: COLUMNDEFAULT record has first col index %d, last %d; " + "should have 0 <= first < last <= 256" + % (first_colx, last_colx), file=self.logfile) + last_colx = min(last_colx, 256) + for colx in xrange(first_colx, last_colx): + offset = 4 + 3 * (colx - first_colx) + cell_attr = data[offset:offset+3] + xf_index = self.fixed_BIFF2_xfindex(cell_attr, rowx=-1, colx=colx) + if colx in self.colinfo_map: + c = self.colinfo_map[colx] + else: + c = Colinfo() + self.colinfo_map[colx] = c + c.xf_index = xf_index + elif rc == XL_WINDOW2_B2: # BIFF 2 only + attr_names = ("show_formulas", "show_grid_lines", "show_sheet_headers", + "panes_are_frozen", "show_zero_values") + for attr, char in zip(attr_names, data[0:5]): + setattr(self, attr, int(char != b'\0')) + ( + self.first_visible_rowx, self.first_visible_colx, + self.automatic_grid_line_colour, + ) = unpack("<HHB", data[5:10]) + self.gridline_colour_rgb = unpack("<BBB", data[10:13]) + self.gridline_colour_index = nearest_colour_index( + self.book.colour_map, self.gridline_colour_rgb, debug=0) + else: + # if DEBUG: print "SHEET.READ: Unhandled record type %02x %d bytes %r" % (rc, data_len, data) + pass + if not eof_found: + raise XLRDError("Sheet %d (%r) missing EOF record" + % (self.number, self.name)) + self.tidy_dimensions() + self.update_cooked_mag_factors() + bk._position = oldpos + return 1 + + def string_record_contents(self, data): + bv = self.biff_version + bk = self.book + lenlen = (bv >= 30) + 1 + nchars_expected = unpack("<" + "BH"[lenlen - 1], data[:lenlen])[0] + offset = lenlen + if bv < 80: + enc = bk.encoding or bk.derive_encoding() + nchars_found = 0 + result = UNICODE_LITERAL("") + while 1: + if bv >= 80: + flag = BYTES_ORD(data[offset]) & 1 + enc = ("latin_1", "utf_16_le")[flag] + offset += 1 + chunk = unicode(data[offset:], enc) + result += chunk + nchars_found += len(chunk) + if nchars_found == nchars_expected: + return result + if nchars_found > nchars_expected: + msg = ("STRING/CONTINUE: expected %d chars, found %d" + % (nchars_expected, nchars_found)) + raise XLRDError(msg) + rc, _unused_len, data = bk.get_record_parts() + if rc != XL_CONTINUE: + raise XLRDError( + "Expected CONTINUE record; found record-type 0x%04X" % rc) + offset = 0 + + def update_cooked_mag_factors(self): + # Cached values are used ONLY for the non-active view mode. + # When the user switches to the non-active view mode, + # if the cached value for that mode is not valid, + # Excel pops up a window which says: + # "The number must be between 10 and 400. Try again by entering a number in this range." + # When the user hits OK, it drops into the non-active view mode + # but uses the magn from the active mode. + # NOTE: definition of "valid" depends on mode ... see below + blah = DEBUG or self.verbosity > 0 + if self.show_in_page_break_preview: + if self.scl_mag_factor is None: # no SCL record + self.cooked_page_break_preview_mag_factor = 100 # Yes, 100, not 60, NOT a typo + else: + self.cooked_page_break_preview_mag_factor = self.scl_mag_factor + zoom = self.cached_normal_view_mag_factor + if not (10 <= zoom <=400): + if blah: + print( + "WARNING *** WINDOW2 rcd sheet %d: Bad cached_normal_view_mag_factor: %d" + % (self.number, self.cached_normal_view_mag_factor), + file=self.logfile, + ) + zoom = self.cooked_page_break_preview_mag_factor + self.cooked_normal_view_mag_factor = zoom + else: + # normal view mode + if self.scl_mag_factor is None: # no SCL record + self.cooked_normal_view_mag_factor = 100 + else: + self.cooked_normal_view_mag_factor = self.scl_mag_factor + zoom = self.cached_page_break_preview_mag_factor + if not zoom: + # VALID, defaults to 60 + zoom = 60 + elif not (10 <= zoom <= 400): + if blah: + print( + "WARNING *** WINDOW2 rcd sheet %r: Bad cached_page_break_preview_mag_factor: %r" + % (self.number, self.cached_page_break_preview_mag_factor), + file=self.logfile, + ) + zoom = self.cooked_normal_view_mag_factor + self.cooked_page_break_preview_mag_factor = zoom + + def fixed_BIFF2_xfindex(self, cell_attr, rowx, colx, true_xfx=None): + DEBUG = 0 + blah = DEBUG or self.verbosity >= 2 + if self.biff_version == 21: + if self.book.xf_list: + if true_xfx is not None: + xfx = true_xfx + else: + xfx = BYTES_ORD(cell_attr[0]) & 0x3F + if xfx == 0x3F: + if self._ixfe is None: + raise XLRDError("BIFF2 cell record has XF index 63 but no preceding IXFE record.") + xfx = self._ixfe + # OOo docs are capable of interpretation that each + # cell record is preceded immediately by its own IXFE record. + # Empirical evidence is that (sensibly) an IXFE record applies to all + # following cell records until another IXFE comes along. + return xfx + # Have either Excel 2.0, or broken 2.1 w/o XF records -- same effect. + self.biff_version = self.book.biff_version = 20 + #### check that XF slot in cell_attr is zero + xfx_slot = BYTES_ORD(cell_attr[0]) & 0x3F + assert xfx_slot == 0 + xfx = self._cell_attr_to_xfx.get(cell_attr) + if xfx is not None: + return xfx + if blah: + fprintf(self.logfile, "New cell_attr %r at (%r, %r)\n", cell_attr, rowx, colx) + if not self.book.xf_list: + for xfx in xrange(16): + self.insert_new_BIFF20_xf(cell_attr=b"\x40\x00\x00", style=xfx < 15) + xfx = self.insert_new_BIFF20_xf(cell_attr=cell_attr) + return xfx + + def insert_new_BIFF20_xf(self, cell_attr, style=0): + DEBUG = 0 + blah = DEBUG or self.verbosity >= 2 + book = self.book + xfx = len(book.xf_list) + xf = self.fake_XF_from_BIFF20_cell_attr(cell_attr, style) + xf.xf_index = xfx + book.xf_list.append(xf) + if blah: + xf.dump(self.logfile, header="=== Faked XF %d ===" % xfx, footer="======") + if xf.format_key not in book.format_map: + if xf.format_key: + msg = "ERROR *** XF[%d] unknown format key (%d, 0x%04x)\n" + fprintf(self.logfile, msg, + xf.xf_index, xf.format_key, xf.format_key) + fmt = Format(xf.format_key, FUN, UNICODE_LITERAL("General")) + book.format_map[xf.format_key] = fmt + book.format_list.append(fmt) + cellty_from_fmtty = { + FNU: XL_CELL_NUMBER, + FUN: XL_CELL_NUMBER, + FGE: XL_CELL_NUMBER, + FDT: XL_CELL_DATE, + FTX: XL_CELL_NUMBER, # Yes, a number can be formatted as text. + } + fmt = book.format_map[xf.format_key] + cellty = cellty_from_fmtty[fmt.type] + self._xf_index_to_xl_type_map[xf.xf_index] = cellty + self._cell_attr_to_xfx[cell_attr] = xfx + return xfx + + def fake_XF_from_BIFF20_cell_attr(self, cell_attr, style=0): + from .formatting import XF, XFAlignment, XFBorder, XFBackground, XFProtection + xf = XF() + xf.alignment = XFAlignment() + xf.alignment.indent_level = 0 + xf.alignment.shrink_to_fit = 0 + xf.alignment.text_direction = 0 + xf.border = XFBorder() + xf.border.diag_up = 0 + xf.border.diag_down = 0 + xf.border.diag_colour_index = 0 + xf.border.diag_line_style = 0 # no line + xf.background = XFBackground() + xf.protection = XFProtection() + (prot_bits, font_and_format, halign_etc) = unpack('<BBB', cell_attr) + xf.format_key = font_and_format & 0x3F + xf.font_index = (font_and_format & 0xC0) >> 6 + upkbits(xf.protection, prot_bits, ( + (6, 0x40, 'cell_locked'), + (7, 0x80, 'formula_hidden'), + )) + xf.alignment.hor_align = halign_etc & 0x07 + for mask, side in ((0x08, 'left'), (0x10, 'right'), (0x20, 'top'), (0x40, 'bottom')): + if halign_etc & mask: + colour_index, line_style = 8, 1 # black, thin + else: + colour_index, line_style = 0, 0 # none, none + setattr(xf.border, side + '_colour_index', colour_index) + setattr(xf.border, side + '_line_style', line_style) + bg = xf.background + if halign_etc & 0x80: + bg.fill_pattern = 17 + else: + bg.fill_pattern = 0 + bg.background_colour_index = 9 # white + bg.pattern_colour_index = 8 # black + xf.parent_style_index = (0x0FFF, 0)[style] + xf.alignment.vert_align = 2 # bottom + xf.alignment.rotation = 0 + attr_stems = [ + 'format', + 'font', + 'alignment', + 'border', + 'background', + 'protection', + ] + for attr_stem in attr_stems: + attr = "_" + attr_stem + "_flag" + setattr(xf, attr, 1) + return xf + + def req_fmt_info(self): + if not self.formatting_info: + raise XLRDError("Feature requires open_workbook(..., formatting_info=True)") + + def computed_column_width(self, colx): + """ + Determine column display width. + + :param colx: + Index of the queried column, range 0 to 255. + Note that it is possible to find out the width that will be used to + display columns with no cell information e.g. column IV (colx=255). + + :return: + The column width that will be used for displaying + the given column by Excel, in units of 1/256th of the width of a + standard character (the digit zero in the first font). + + .. versionadded:: 0.6.1 + """ + self.req_fmt_info() + if self.biff_version >= 80: + colinfo = self.colinfo_map.get(colx, None) + if colinfo is not None: + return colinfo.width + if self.standardwidth is not None: + return self.standardwidth + elif self.biff_version >= 40: + if self.gcw[colx]: + if self.standardwidth is not None: + return self.standardwidth + else: + colinfo = self.colinfo_map.get(colx, None) + if colinfo is not None: + return colinfo.width + elif self.biff_version == 30: + colinfo = self.colinfo_map.get(colx, None) + if colinfo is not None: + return colinfo.width + # All roads lead to Rome and the DEFCOLWIDTH ... + if self.defcolwidth is not None: + return self.defcolwidth * 256 + return 8 * 256 # 8 is what Excel puts in a DEFCOLWIDTH record + + def handle_hlink(self, data): + # DEBUG = 1 + if DEBUG: print("\n=== hyperlink ===", file=self.logfile) + record_size = len(data) + h = Hyperlink() + h.frowx, h.lrowx, h.fcolx, h.lcolx, guid0, dummy, options = unpack('<HHHH16s4si', data[:32]) + assert guid0 == b"\xD0\xC9\xEA\x79\xF9\xBA\xCE\x11\x8C\x82\x00\xAA\x00\x4B\xA9\x0B" + assert dummy == b"\x02\x00\x00\x00" + if DEBUG: print("options: %08X" % options, file=self.logfile) + offset = 32 + + def get_nul_terminated_unicode(buf, ofs): + nb = unpack('<L', buf[ofs:ofs+4])[0] * 2 + ofs += 4 + uc = unicode(buf[ofs:ofs+nb], 'UTF-16le')[:-1] + ofs += nb + return uc, ofs + + if options & 0x14: # has a description + h.desc, offset = get_nul_terminated_unicode(data, offset) + + if options & 0x80: # has a target + h.target, offset = get_nul_terminated_unicode(data, offset) + + if (options & 1) and not (options & 0x100): # HasMoniker and not MonikerSavedAsString + # an OLEMoniker structure + clsid, = unpack('<16s', data[offset:offset + 16]) + if DEBUG: fprintf(self.logfile, "clsid=%r\n", clsid) + offset += 16 + if clsid == b"\xE0\xC9\xEA\x79\xF9\xBA\xCE\x11\x8C\x82\x00\xAA\x00\x4B\xA9\x0B": + # E0H C9H EAH 79H F9H BAH CEH 11H 8CH 82H 00H AAH 00H 4BH A9H 0BH + # URL Moniker + h.type = UNICODE_LITERAL('url') + nbytes = unpack('<L', data[offset:offset + 4])[0] + offset += 4 + h.url_or_path = unicode(data[offset:offset + nbytes], 'UTF-16le') + if DEBUG: fprintf(self.logfile, "initial url=%r len=%d\n", h.url_or_path, len(h.url_or_path)) + endpos = h.url_or_path.find('\x00') + if DEBUG: print("endpos=%d" % endpos, file=self.logfile) + h.url_or_path = h.url_or_path[:endpos] + true_nbytes = 2 * (endpos + 1) + offset += true_nbytes + extra_nbytes = nbytes - true_nbytes + extra_data = data[offset:offset + extra_nbytes] + offset += extra_nbytes + if DEBUG: + fprintf( + self.logfile, + "url=%r\nextra=%r\nnbytes=%d true_nbytes=%d extra_nbytes=%d\n", + h.url_or_path, extra_data, nbytes, true_nbytes, extra_nbytes, + ) + assert extra_nbytes in (24, 0) + elif clsid == b"\x03\x03\x00\x00\x00\x00\x00\x00\xC0\x00\x00\x00\x00\x00\x00\x46": + # file moniker + h.type = UNICODE_LITERAL('local file') + uplevels, nbytes = unpack("<Hi", data[offset:offset + 6]) + offset += 6 + shortpath = b"..\\" * uplevels + data[offset:offset + nbytes - 1] #### BYTES, not unicode + if DEBUG: fprintf(self.logfile, "uplevels=%d shortpath=%r\n", uplevels, shortpath) + offset += nbytes + offset += 24 # OOo: "unknown byte sequence" + # above is version 0xDEAD + 20 reserved zero bytes + sz = unpack('<i', data[offset:offset + 4])[0] + if DEBUG: print("sz=%d" % sz, file=self.logfile) + offset += 4 + if sz: + xl = unpack('<i', data[offset:offset + 4])[0] + offset += 4 + offset += 2 # "unknown byte sequence" MS: 0x0003 + extended_path = unicode(data[offset:offset + xl], 'UTF-16le') # not zero-terminated + offset += xl + h.url_or_path = extended_path + else: + h.url_or_path = shortpath + #### MS KLUDGE WARNING #### + # The "shortpath" is bytes encoded in the **UNKNOWN** creator's "ANSI" encoding. + else: + fprintf(self.logfile, "*** unknown clsid %r\n", clsid) + elif options & 0x163 == 0x103: # UNC + h.type = UNICODE_LITERAL('unc') + h.url_or_path, offset = get_nul_terminated_unicode(data, offset) + elif options & 0x16B == 8: + h.type = UNICODE_LITERAL('workbook') + else: + h.type = UNICODE_LITERAL('unknown') + + if options & 0x8: # has textmark + h.textmark, offset = get_nul_terminated_unicode(data, offset) + + if DEBUG: + h.dump(header="... object dump ...") + print("offset=%d record_size=%d" % (offset, record_size)) + + extra_nbytes = record_size - offset + if extra_nbytes > 0: + fprintf( + self.logfile, + "*** WARNING: hyperlink at R%dC%d has %d extra data bytes: %s\n", + h.frowx + 1, + h.fcolx + 1, + extra_nbytes, + REPR(data[-extra_nbytes:]), + ) + # Seen: b"\x00\x00" also b"A\x00", b"V\x00" + elif extra_nbytes < 0: + raise XLRDError("Bug or corrupt file, send copy of input file for debugging") + + self.hyperlink_list.append(h) + for rowx in xrange(h.frowx, h.lrowx+1): + for colx in xrange(h.fcolx, h.lcolx+1): + self.hyperlink_map[rowx, colx] = h + + def handle_quicktip(self, data): + rcx, frowx, lrowx, fcolx, lcolx = unpack('<5H', data[:10]) + assert rcx == XL_QUICKTIP + assert self.hyperlink_list + h = self.hyperlink_list[-1] + assert (frowx, lrowx, fcolx, lcolx) == (h.frowx, h.lrowx, h.fcolx, h.lcolx) + assert data[-2:] == b'\x00\x00' + h.quicktip = unicode(data[10:-2], 'utf_16_le') + + def handle_msodrawingetc(self, recid, data_len, data): + if not OBJ_MSO_DEBUG: + return + DEBUG = 1 + if self.biff_version < 80: + return + o = MSODrawing() + pos = 0 + while pos < data_len: + tmp, fbt, cb = unpack('<HHI', data[pos:pos+8]) + ver = tmp & 0xF + inst = (tmp >> 4) & 0xFFF + if ver == 0xF: + ndb = 0 # container + else: + ndb = cb + if DEBUG: + hex_char_dump(data, pos, ndb + 8, base=0, fout=self.logfile) + fprintf(self.logfile, + "fbt:0x%04X inst:%d ver:0x%X cb:%d (0x%04X)\n", + fbt, inst, ver, cb, cb) + if fbt == 0xF010: # Client Anchor + assert ndb == 18 + (o.anchor_unk, + o.anchor_colx_lo, o.anchor_rowx_lo, + o.anchor_colx_hi, o.anchor_rowx_hi) = unpack('<Hiiii', data[pos+8:pos+8+ndb]) + elif fbt == 0xF011: # Client Data + # must be followed by an OBJ record + assert cb == 0 + assert pos + 8 == data_len + else: + pass + pos += ndb + 8 + else: + # didn't break out of while loop + assert pos == data_len + if DEBUG: + o.dump(self.logfile, header="=== MSODrawing ===", footer= " ") + + + def handle_obj(self, data): + if self.biff_version < 80: + return None + o = MSObj() + data_len = len(data) + pos = 0 + if OBJ_MSO_DEBUG: + fprintf(self.logfile, "... OBJ record len=%d...\n", data_len) + while pos < data_len: + ft, cb = unpack('<HH', data[pos:pos+4]) + if OBJ_MSO_DEBUG: + fprintf(self.logfile, "pos=%d ft=0x%04X cb=%d\n", pos, ft, cb) + hex_char_dump(data, pos, cb + 4, base=0, fout=self.logfile) + if pos == 0 and not (ft == 0x15 and cb == 18): + if self.verbosity: + fprintf(self.logfile, "*** WARNING Ignoring antique or corrupt OBJECT record\n") + return None + if ft == 0x15: # ftCmo ... s/b first + assert pos == 0 + o.type, o.id, option_flags = unpack('<HHH', data[pos+4:pos+10]) + upkbits(o, option_flags, ( + ( 0, 0x0001, 'locked'), + ( 4, 0x0010, 'printable'), + ( 8, 0x0100, 'autofilter'), # not documented in Excel 97 dev kit + ( 9, 0x0200, 'scrollbar_flag'), # not documented in Excel 97 dev kit + (13, 0x2000, 'autofill'), + (14, 0x4000, 'autoline'), + )) + elif ft == 0x00: + if data[pos:data_len] == b'\0' * (data_len - pos): + # ignore "optional reserved" data at end of record + break + msg = "Unexpected data at end of OBJECT record" + fprintf(self.logfile, "*** ERROR %s\n" % msg) + hex_char_dump(data, pos, data_len - pos, base=0, fout=self.logfile) + raise XLRDError(msg) + elif ft == 0x0C: # Scrollbar + values = unpack('<5H', data[pos+8:pos+18]) + for value, tag in zip(values, ('value', 'min', 'max', 'inc', 'page')): + setattr(o, 'scrollbar_' + tag, value) + elif ft == 0x0D: # "Notes structure" [used for cell comments] + # not documented in Excel 97 dev kit + if OBJ_MSO_DEBUG: fprintf(self.logfile, "*** OBJ record has ft==0x0D 'notes' structure\n") + elif ft == 0x13: # list box data + if o.autofilter: # non standard exit. NOT documented + break + else: + pass + pos += cb + 4 + else: + # didn't break out of while loop + pass + if OBJ_MSO_DEBUG: + o.dump(self.logfile, header="=== MSOBj ===", footer= " ") + return o + + def handle_note(self, data, txos): + if OBJ_MSO_DEBUG: + fprintf(self.logfile, '... NOTE record ...\n') + hex_char_dump(data, 0, len(data), base=0, fout=self.logfile) + o = Note() + data_len = len(data) + if self.biff_version < 80: + o.rowx, o.colx, expected_bytes = unpack('<HHH', data[:6]) + nb = len(data) - 6 + assert nb <= expected_bytes + pieces = [data[6:]] + expected_bytes -= nb + while expected_bytes > 0: + rc2, data2_len, data2 = self.book.get_record_parts() + assert rc2 == XL_NOTE + dummy_rowx, nb = unpack('<H2xH', data2[:6]) + assert dummy_rowx == 0xFFFF + assert nb == data2_len - 6 + pieces.append(data2[6:]) + expected_bytes -= nb + assert expected_bytes == 0 + enc = self.book.encoding or self.book.derive_encoding() + o.text = unicode(b''.join(pieces), enc) + o.rich_text_runlist = [(0, 0)] + o.show = 0 + o.row_hidden = 0 + o.col_hidden = 0 + o.author = UNICODE_LITERAL('') + o._object_id = None + self.cell_note_map[o.rowx, o.colx] = o + return + # Excel 8.0+ + o.rowx, o.colx, option_flags, o._object_id = unpack('<4H', data[:8]) + o.show = (option_flags >> 1) & 1 + o.row_hidden = (option_flags >> 7) & 1 + o.col_hidden = (option_flags >> 8) & 1 + # XL97 dev kit book says NULL [sic] bytes padding between string count and string data + # to ensure that string is word-aligned. Appears to be nonsense. + o.author, endpos = unpack_unicode_update_pos(data, 8, lenlen=2) + # There is a random/undefined byte after the author string (not counted in the + # string length). + # Issue 4 on github: Google Spreadsheet doesn't write the undefined byte. + assert (data_len - endpos) in (0, 1) + if OBJ_MSO_DEBUG: + o.dump(self.logfile, header="=== Note ===", footer= " ") + txo = txos.get(o._object_id) + if txo: + o.text = txo.text + o.rich_text_runlist = txo.rich_text_runlist + self.cell_note_map[o.rowx, o.colx] = o + + def handle_txo(self, data): + if self.biff_version < 80: + return + o = MSTxo() + fmt = '<HH6sHHH' + fmtsize = calcsize(fmt) + option_flags, o.rot, controlInfo, cchText, cbRuns, o.ifntEmpty = unpack(fmt, data[:fmtsize]) + o.fmla = data[fmtsize:] + upkbits(o, option_flags, ( + ( 3, 0x000E, 'horz_align'), + ( 6, 0x0070, 'vert_align'), + ( 9, 0x0200, 'lock_text'), + (14, 0x4000, 'just_last'), + (15, 0x8000, 'secret_edit'), + )) + totchars = 0 + o.text = UNICODE_LITERAL('') + while totchars < cchText: + rc2, data2_len, data2 = self.book.get_record_parts() + assert rc2 == XL_CONTINUE + if OBJ_MSO_DEBUG: + hex_char_dump(data2, 0, data2_len, base=0, fout=self.logfile) + nb = BYTES_ORD(data2[0]) # 0 means latin1, 1 means utf_16_le + nchars = data2_len - 1 + if nb: + assert nchars % 2 == 0 + nchars //= 2 + utext, endpos = unpack_unicode_update_pos(data2, 0, known_len=nchars) + assert endpos == data2_len + o.text += utext + totchars += nchars + o.rich_text_runlist = [] + totruns = 0 + while totruns < cbRuns: # counts of BYTES, not runs + rc3, data3_len, data3 = self.book.get_record_parts() + # print totruns, cbRuns, rc3, data3_len, repr(data3) + assert rc3 == XL_CONTINUE + assert data3_len % 8 == 0 + for pos in xrange(0, data3_len, 8): + run = unpack('<HH4x', data3[pos:pos+8]) + o.rich_text_runlist.append(run) + totruns += 8 + # remove trailing entries that point to the end of the string + while o.rich_text_runlist and o.rich_text_runlist[-1][0] == cchText: + del o.rich_text_runlist[-1] + if OBJ_MSO_DEBUG: + o.dump(self.logfile, header="=== MSTxo ===", footer= " ") + print(o.rich_text_runlist, file=self.logfile) + return o + + def handle_feat11(self, data): + if not OBJ_MSO_DEBUG: + return + # rt: Record type; this matches the BIFF rt in the first two bytes of the record; =0872h + # grbitFrt: FRT cell reference flag (see table below for details) + # Ref0: Range reference to a worksheet cell region if grbitFrt=1 (bitFrtRef). Otherwise blank. + # isf: Shared feature type index =5 for Table + # fHdr: =0 since this is for feat not feat header + # reserved0: Reserved for future use =0 for Table + # cref: Count of ref ranges this feature is on + # cbFeatData: Count of byte for the current feature data. + # reserved1: =0 currently not used + # Ref1: Repeat of Ref0. UNDOCUMENTED + rt, grbitFrt, Ref0, isf, fHdr, reserved0, cref, cbFeatData, reserved1, Ref1 = unpack('<HH8sHBiHiH8s', data[0:35]) + assert reserved0 == 0 + assert reserved1 == 0 + assert isf == 5 + assert rt == 0x872 + assert fHdr == 0 + assert Ref1 == Ref0 + print(self.logfile, "FEAT11: grbitFrt=%d Ref0=%r cref=%d cbFeatData=%d\n", grbitFrt, Ref0, cref, cbFeatData) + # lt: Table data source type: + # =0 for Excel Worksheet Table =1 for read-write SharePoint linked List + # =2 for XML mapper Table =3 for Query Table + # idList: The ID of the Table (unique per worksheet) + # crwHeader: How many header/title rows the Table has at the top + # crwTotals: How many total rows the Table has at the bottom + # idFieldNext: Next id to try when assigning a unique id to a new field + # cbFSData: The size of the Fixed Data portion of the Table data structure. + # rupBuild: the rupBuild that generated the record + # unusedShort: UNUSED short that can be used later. The value is reserved during round-tripping. + # listFlags: Collection of bit flags: (see listFlags' bit setting table below for detail.) + # lPosStmCache: Table data stream position of cached data + # cbStmCache: Count of bytes of cached data + # cchStmCache: Count of characters of uncompressed cached data in the stream + # lem: Table edit mode (see List (Table) Editing Mode (lem) setting table below for details.) + # rgbHashParam: Hash value for SharePoint Table + # cchName: Count of characters in the Table name string rgbName + (lt, idList, crwHeader, crwTotals, idFieldNext, cbFSData, + rupBuild, unusedShort, listFlags, lPosStmCache, cbStmCache, + cchStmCache, lem, rgbHashParam, cchName) = unpack('<iiiiiiHHiiiii16sH', data[35:35+66]) + print("lt=%d idList=%d crwHeader=%d crwTotals=%d idFieldNext=%d cbFSData=%d\n" + "rupBuild=%d unusedShort=%d listFlags=%04X lPosStmCache=%d cbStmCache=%d\n" + "cchStmCache=%d lem=%d rgbHashParam=%r cchName=%d" % ( + lt, idList, crwHeader, crwTotals, idFieldNext, cbFSData, + rupBuild, unusedShort,listFlags, lPosStmCache, cbStmCache, + cchStmCache, lem, rgbHashParam, cchName), file=self.logfile) + + def __repr__(self): + return "Sheet {:>2}:<{}>".format(self.number, self.name) + + +class MSODrawing(BaseObject): + pass + + +class MSObj(BaseObject): + pass + + +class MSTxo(BaseObject): + pass + + +class Note(BaseObject): + """ + Represents a user "comment" or "note". + Note objects are accessible through :attr:`Sheet.cell_note_map`. + + .. versionadded:: 0.7.2 + """ + + #: Author of note + author = UNICODE_LITERAL('') + + #: ``True`` if the containing column is hidden + col_hidden = 0 + + #: Column index + colx = 0 + + #: List of ``(offset_in_string, font_index)`` tuples. + #: Unlike :attr:`Sheet.rich_text_runlist_map`, the first offset should + #: always be 0. + rich_text_runlist = None + + #: True if the containing row is hidden + row_hidden = 0 + + #: Row index + rowx = 0 + + #: True if note is always shown + show = 0 + + #: Text of the note + text = UNICODE_LITERAL('') + + +class Hyperlink(BaseObject): + """ + Contains the attributes of a hyperlink. + Hyperlink objects are accessible through :attr:`Sheet.hyperlink_list` + and :attr:`Sheet.hyperlink_map`. + + .. versionadded:: 0.7.2 + """ + + #: Index of first row + frowx = None + + #: Index of last row + lrowx = None + + #: Index of first column + fcolx = None + + #: Index of last column + lcolx = None + + #: Type of hyperlink. Unicode string, one of 'url', 'unc', + #: 'local file', 'workbook', 'unknown' + type = None + + #: The URL or file-path, depending in the type. Unicode string, except + #: in the rare case of a local but non-existent file with non-ASCII + #: characters in the name, in which case only the "8.3" filename is + #: available, as a :class:`bytes` (3.x) or :class:`str` (2.x) string, + #: *with unknown encoding.* + url_or_path = None + + #: Description. + #: This is displayed in the cell, + #: and should be identical to the cell value. Unicode string, or ``None``. + #: It seems impossible NOT to have a description created by the Excel UI. + desc = None + + #: Target frame. Unicode string. + #: + #: .. note:: + #: No cases of this have been seen in the wild. + #: It seems impossible to create one in the Excel UI. + target = None + + #: The piece after the "#" in + #: "http://docs.python.org/library#struct_module", or the ``Sheet1!A1:Z99`` + #: part when type is "workbook". + textmark = None + + #: The text of the "quick tip" displayed when the cursor + #: hovers over the hyperlink. + quicktip = None + +# === helpers === + +def unpack_RK(rk_str): + flags = BYTES_ORD(rk_str[0]) + if flags & 2: + # There's a SIGNED 30-bit integer in there! + i, = unpack('<i', rk_str) + i >>= 2 # div by 4 to drop the 2 flag bits + if flags & 1: + return i / 100.0 + return float(i) + else: + # It's the most significant 30 bits of an IEEE 754 64-bit FP number + d, = unpack('<d', b'\0\0\0\0' + BYTES_LITERAL(chr(flags & 252)) + rk_str[1:4]) + if flags & 1: + return d / 100.0 + return d + +##### =============== Cell ======================================== ##### + +cellty_from_fmtty = { + FNU: XL_CELL_NUMBER, + FUN: XL_CELL_NUMBER, + FGE: XL_CELL_NUMBER, + FDT: XL_CELL_DATE, + FTX: XL_CELL_NUMBER, # Yes, a number can be formatted as text. +} + +ctype_text = { + XL_CELL_EMPTY: 'empty', + XL_CELL_TEXT: 'text', + XL_CELL_NUMBER: 'number', + XL_CELL_DATE: 'xldate', + XL_CELL_BOOLEAN: 'bool', + XL_CELL_ERROR: 'error', + XL_CELL_BLANK: 'blank', +} + + +class Cell(BaseObject): + """ + Contains the data for one cell. + + .. warning:: + You don't call this class yourself. You access :class:`Cell` objects + via methods of the :class:`Sheet` object(s) that you found in the + :class:`~xlrd.book.Book` object that was returned when you called + :func:`~xlrd.open_workbook` + + Cell objects have three attributes: ``ctype`` is an int, ``value`` + (which depends on ``ctype``) and ``xf_index``. + If ``formatting_info`` is not enabled when the workbook is opened, + ``xf_index`` will be ``None``. + + The following table describes the types of cells and how their values + are represented in Python. + + .. raw:: html + + <table border="1" cellpadding="7"> + <tr> + <th>Type symbol</th> + <th>Type number</th> + <th>Python value</th> + </tr> + <tr> + <td>XL_CELL_EMPTY</td> + <td align="center">0</td> + <td>empty string ''</td> + </tr> + <tr> + <td>XL_CELL_TEXT</td> + <td align="center">1</td> + <td>a Unicode string</td> + </tr> + <tr> + <td>XL_CELL_NUMBER</td> + <td align="center">2</td> + <td>float</td> + </tr> + <tr> + <td>XL_CELL_DATE</td> + <td align="center">3</td> + <td>float</td> + </tr> + <tr> + <td>XL_CELL_BOOLEAN</td> + <td align="center">4</td> + <td>int; 1 means TRUE, 0 means FALSE</td> + </tr> + <tr> + <td>XL_CELL_ERROR</td> + <td align="center">5</td> + <td>int representing internal Excel codes; for a text representation, + refer to the supplied dictionary error_text_from_code</td> + </tr> + <tr> + <td>XL_CELL_BLANK</td> + <td align="center">6</td> + <td>empty string ''. Note: this type will appear only when + open_workbook(..., formatting_info=True) is used.</td> + </tr> + </table> + """ + + __slots__ = ['ctype', 'value', 'xf_index'] + + def __init__(self, ctype, value, xf_index=None): + self.ctype = ctype + self.value = value + self.xf_index = xf_index + + def __repr__(self): + if self.xf_index is None: + return "%s:%r" % (ctype_text[self.ctype], self.value) + else: + return "%s:%r (XF:%r)" % (ctype_text[self.ctype], self.value, self.xf_index) + +empty_cell = Cell(XL_CELL_EMPTY, UNICODE_LITERAL('')) + +##### =============== Colinfo and Rowinfo ============================== ##### + + +class Colinfo(BaseObject): + """ + Width and default formatting information that applies to one or + more columns in a sheet. Derived from ``COLINFO`` records. + + Here is the default hierarchy for width, according to the OOo docs: + + In BIFF3, if a ``COLINFO`` record is missing for a column, + the width specified in the record ``DEFCOLWIDTH`` is used instead. + + In BIFF4-BIFF7, the width set in this ``COLINFO`` record is only used, + if the corresponding bit for this column is cleared in the ``GCW`` + record, otherwise the column width set in the ``DEFCOLWIDTH`` record + is used (the ``STANDARDWIDTH`` record is always ignored in this case [#f1]_). + + In BIFF8, if a ``COLINFO`` record is missing for a column, + the width specified in the record ``STANDARDWIDTH`` is used. + If this ``STANDARDWIDTH`` record is also missing, + the column width of the record ``DEFCOLWIDTH`` is used instead. + + .. [#f1] The docs on the ``GCW`` record say this: + + If a bit is set, the corresponding column uses the width set in the + ``STANDARDWIDTH`` record. If a bit is cleared, the corresponding column + uses the width set in the ``COLINFO`` record for this column. + + If a bit is set, and the worksheet does not contain the ``STANDARDWIDTH`` + record, or if the bit is cleared, and the worksheet does not contain the + ``COLINFO`` record, the ``DEFCOLWIDTH`` record of the worksheet will be + used instead. + + xlrd goes with the GCW version of the story. + Reference to the source may be useful: see + :meth:`Sheet.computed_column_width`. + + .. versionadded:: 0.6.1 + """ + + #: Width of the column in 1/256 of the width of the zero character, + #: using default font (first ``FONT`` record in the file). + width = 0 + + #: XF index to be used for formatting empty cells. + xf_index = -1 + + #: 1 = column is hidden + hidden = 0 + + #: Value of a 1-bit flag whose purpose is unknown + #: but is often seen set to 1 + bit1_flag = 0 + + #: Outline level of the column, in ``range(7)``. + #: (0 = no outline) + outline_level = 0 + + #: 1 = column is collapsed + collapsed = 0 + +_USE_SLOTS = 1 + + +class Rowinfo(BaseObject): + """ + Height and default formatting information that applies to a row in a sheet. + Derived from ``ROW`` records. + + .. versionadded:: 0.6.1 + """ + + if _USE_SLOTS: + __slots__ = ( + "height", + "has_default_height", + "outline_level", + "outline_group_starts_ends", + "hidden", + "height_mismatch", + "has_default_xf_index", + "xf_index", + "additional_space_above", + "additional_space_below", + ) + + def __init__(self): + #: Height of the row, in twips. One twip == 1/20 of a point. + self.height = None + + #: 0 = Row has custom height; 1 = Row has default height. + self.has_default_height = None + + #: Outline level of the row (0 to 7) + self.outline_level = None + + #: 1 = Outline group starts or ends here (depending on where the + #: outline buttons are located, see ``WSBOOL`` record, which is not + #: parsed by xlrd), *and* is collapsed. + self.outline_group_starts_ends = None + + #: 1 = Row is hidden (manually, or by a filter or outline group) + self.hidden = None + + #: 1 = Row height and default font height do not match. + self.height_mismatch = None + + #: 1 = the xf_index attribute is usable; 0 = ignore it. + self.has_default_xf_index = None + + #: Index to default :class:`~xlrd.formatting.XF` record for empty cells + #: in this row. Don't use this if ``has_default_xf_index == 0``. + self.xf_index = None + + #: This flag is set if the upper border of at least one cell in this + #: row or if the lower border of at least one cell in the row above is + #: formatted with a thick line style. Thin and medium line styles are + #: not taken into account. + self.additional_space_above = None + + #: This flag is set if the lower border of at least one cell in this row + #: or if the upper border of at least one cell in the row below is + #: formatted with a medium or thick line style. Thin line styles are not + #: taken into account. + self.additional_space_below = None + + def __getstate__(self): + return ( + self.height, + self.has_default_height, + self.outline_level, + self.outline_group_starts_ends, + self.hidden, + self.height_mismatch, + self.has_default_xf_index, + self.xf_index, + self.additional_space_above, + self.additional_space_below, + ) + + def __setstate__(self, state): + ( + self.height, + self.has_default_height, + self.outline_level, + self.outline_group_starts_ends, + self.hidden, + self.height_mismatch, + self.has_default_xf_index, + self.xf_index, + self.additional_space_above, + self.additional_space_below, + ) = state diff --git a/.venv/lib/python3.12/site-packages/xlrd/timemachine.py b/.venv/lib/python3.12/site-packages/xlrd/timemachine.py new file mode 100644 index 00000000..a519299e --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlrd/timemachine.py @@ -0,0 +1,53 @@ +## +# <p>Copyright (c) 2006-2012 Stephen John Machin, Lingfo Pty Ltd</p> +# <p>This module is part of the xlrd package, which is released under a BSD-style licence.</p> +## + +# timemachine.py -- adaptation for single codebase. +# Currently supported: 2.6 to 2.7, 3.2+ +# usage: from timemachine import * + +from __future__ import print_function + +import sys + +python_version = sys.version_info[:2] # e.g. version 2.6 -> (2, 6) + +if python_version >= (3, 0): + # Python 3 + BYTES_LITERAL = lambda x: x.encode('latin1') + UNICODE_LITERAL = lambda x: x + BYTES_ORD = lambda byte: byte + from io import BytesIO as BYTES_IO + def fprintf(f, fmt, *vargs): + fmt = fmt.replace("%r", "%a") + if fmt.endswith('\n'): + print(fmt[:-1] % vargs, file=f) + else: + print(fmt % vargs, end=' ', file=f) + EXCEL_TEXT_TYPES = (str, bytes, bytearray) # xlwt: isinstance(obj, EXCEL_TEXT_TYPES) + REPR = ascii + xrange = range + unicode = lambda b, enc: b.decode(enc) + ensure_unicode = lambda s: s + unichr = chr +else: + # Python 2 + BYTES_LITERAL = lambda x: x + UNICODE_LITERAL = lambda x: x.decode('latin1') + BYTES_ORD = ord + from cStringIO import StringIO as BYTES_IO + def fprintf(f, fmt, *vargs): + if fmt.endswith('\n'): + print(fmt[:-1] % vargs, file=f) + else: + print(fmt % vargs, end=' ', file=f) + try: + EXCEL_TEXT_TYPES = basestring # xlwt: isinstance(obj, EXCEL_TEXT_TYPES) + except NameError: + EXCEL_TEXT_TYPES = (str, unicode) + REPR = repr + xrange = xrange + # following used only to overcome 2.x ElementTree gimmick which + # returns text as `str` if it's ascii, otherwise `unicode` + ensure_unicode = unicode # used only in xlsx.py diff --git a/.venv/lib/python3.12/site-packages/xlrd/xldate.py b/.venv/lib/python3.12/site-packages/xlrd/xldate.py new file mode 100644 index 00000000..d84c6508 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/xlrd/xldate.py @@ -0,0 +1,248 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2005-2008 Stephen John Machin, Lingfo Pty Ltd +# This module is part of the xlrd package, which is released under a +# BSD-style licence. +# No part of the content of this file was derived from the works of David Giffin. +""" +Tools for working with dates and times in Excel files. + +The conversion from ``days`` to ``(year, month, day)`` starts with +an integral "julian day number" aka JDN. +FWIW: + +- JDN 0 corresponds to noon on Monday November 24 in Gregorian year -4713. + +More importantly: + +- Noon on Gregorian 1900-03-01 (day 61 in the 1900-based system) is JDN 2415080.0 +- Noon on Gregorian 1904-01-02 (day 1 in the 1904-based system) is JDN 2416482.0 + +""" +import datetime + +_JDN_delta = (2415080 - 61, 2416482 - 1) +assert _JDN_delta[1] - _JDN_delta[0] == 1462 + +# Pre-calculate the datetime epochs for efficiency. +epoch_1904 = datetime.datetime(1904, 1, 1) +epoch_1900 = datetime.datetime(1899, 12, 31) +epoch_1900_minus_1 = datetime.datetime(1899, 12, 30) + +# This is equivalent to 10000-01-01: +_XLDAYS_TOO_LARGE = (2958466, 2958466 - 1462) + + +class XLDateError(ValueError): + "A base class for all datetime-related errors." + + +class XLDateNegative(XLDateError): + "``xldate < 0.00``" + + +class XLDateAmbiguous(XLDateError): + "The 1900 leap-year problem ``(datemode == 0 and 1.0 <= xldate < 61.0)``" + + +class XLDateTooLarge(XLDateError): + "Gregorian year 10000 or later" + + +class XLDateBadDatemode(XLDateError): + "``datemode`` arg is neither 0 nor 1" + + +class XLDateBadTuple(XLDateError): + pass + + +def xldate_as_tuple(xldate, datemode): + """ + Convert an Excel number (presumed to represent a date, a datetime or a time) into + a tuple suitable for feeding to datetime or mx.DateTime constructors. + + :param xldate: The Excel number + :param datemode: 0: 1900-based, 1: 1904-based. + :raises xlrd.xldate.XLDateNegative: + :raises xlrd.xldate.XLDateAmbiguous: + + :raises xlrd.xldate.XLDateTooLarge: + :raises xlrd.xldate.XLDateBadDatemode: + :raises xlrd.xldate.XLDateError: + :returns: Gregorian ``(year, month, day, hour, minute, nearest_second)``. + + .. warning:: + + When using this function to interpret the contents of a workbook, you + should pass in the :attr:`~xlrd.book.Book.datemode` + attribute of that workbook. Whether the workbook has ever been anywhere + near a Macintosh is irrelevant. + + .. admonition:: Special case + + If ``0.0 <= xldate < 1.0``, it is assumed to represent a time; + ``(0, 0, 0, hour, minute, second)`` will be returned. + + .. note:: + + ``1904-01-01`` is not regarded as a valid date in the ``datemode==1`` + system; its "serial number" is zero. + """ + if datemode not in (0, 1): + raise XLDateBadDatemode(datemode) + if xldate == 0.00: + return (0, 0, 0, 0, 0, 0) + if xldate < 0.00: + raise XLDateNegative(xldate) + xldays = int(xldate) + frac = xldate - xldays + seconds = int(round(frac * 86400.0)) + assert 0 <= seconds <= 86400 + if seconds == 86400: + hour = minute = second = 0 + xldays += 1 + else: + # second = seconds % 60; minutes = seconds // 60 + minutes, second = divmod(seconds, 60) + # minute = minutes % 60; hour = minutes // 60 + hour, minute = divmod(minutes, 60) + if xldays >= _XLDAYS_TOO_LARGE[datemode]: + raise XLDateTooLarge(xldate) + + if xldays == 0: + return (0, 0, 0, hour, minute, second) + + if xldays < 61 and datemode == 0: + raise XLDateAmbiguous(xldate) + + jdn = xldays + _JDN_delta[datemode] + yreg = ((((jdn * 4 + 274277) // 146097) * 3 // 4) + jdn + 1363) * 4 + 3 + mp = ((yreg % 1461) // 4) * 535 + 333 + d = ((mp % 16384) // 535) + 1 + # mp /= 16384 + mp >>= 14 + if mp >= 10: + return ((yreg // 1461) - 4715, mp - 9, d, hour, minute, second) + else: + return ((yreg // 1461) - 4716, mp + 3, d, hour, minute, second) + + +def xldate_as_datetime(xldate, datemode): + """ + Convert an Excel date/time number into a :class:`datetime.datetime` object. + + :param xldate: The Excel number + :param datemode: 0: 1900-based, 1: 1904-based. + + :returns: A :class:`datetime.datetime` object. + """ + + # Set the epoch based on the 1900/1904 datemode. + if datemode: + epoch = epoch_1904 + else: + if xldate < 60: + epoch = epoch_1900 + else: + # Workaround Excel 1900 leap year bug by adjusting the epoch. + epoch = epoch_1900_minus_1 + + # The integer part of the Excel date stores the number of days since + # the epoch and the fractional part stores the percentage of the day. + days = int(xldate) + fraction = xldate - days + + # Get the the integer and decimal seconds in Excel's millisecond resolution. + seconds = int(round(fraction * 86400000.0)) + seconds, milliseconds = divmod(seconds, 1000) + + return epoch + datetime.timedelta(days, seconds, 0, milliseconds) + + +# === conversions from date/time to xl numbers + +def _leap(y): + if y % 4: return 0 + if y % 100: return 1 + if y % 400: return 0 + return 1 + +_days_in_month = (None, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) + + +def xldate_from_date_tuple(date_tuple, datemode): + """ + Convert a date tuple (year, month, day) to an Excel date. + + :param year: Gregorian year. + :param month: ``1 <= month <= 12`` + :param day: ``1 <= day <= last day of that (year, month)`` + :param datemode: 0: 1900-based, 1: 1904-based. + :raises xlrd.xldate.XLDateAmbiguous: + :raises xlrd.xldate.XLDateBadDatemode: + :raises xlrd.xldate.XLDateBadTuple: + ``(year, month, day)`` is too early/late or has invalid component(s) + :raises xlrd.xldate.XLDateError: + """ + year, month, day = date_tuple + + if datemode not in (0, 1): + raise XLDateBadDatemode(datemode) + + if year == 0 and month == 0 and day == 0: + return 0.00 + + if not (1900 <= year <= 9999): + raise XLDateBadTuple("Invalid year: %r" % ((year, month, day),)) + if not (1 <= month <= 12): + raise XLDateBadTuple("Invalid month: %r" % ((year, month, day),)) + if (day < 1 or + (day > _days_in_month[month] and not(day == 29 and month == 2 and _leap(year)))): + raise XLDateBadTuple("Invalid day: %r" % ((year, month, day),)) + + Yp = year + 4716 + M = month + if M <= 2: + Yp = Yp - 1 + Mp = M + 9 + else: + Mp = M - 3 + jdn = (1461 * Yp // 4) + ((979 * Mp + 16) // 32) + \ + day - 1364 - (((Yp + 184) // 100) * 3 // 4) + xldays = jdn - _JDN_delta[datemode] + if xldays <= 0: + raise XLDateBadTuple("Invalid (year, month, day): %r" % ((year, month, day),)) + if xldays < 61 and datemode == 0: + raise XLDateAmbiguous("Before 1900-03-01: %r" % ((year, month, day),)) + return float(xldays) + + +def xldate_from_time_tuple(time_tuple): + """ + Convert a time tuple ``(hour, minute, second)`` to an Excel "date" value + (fraction of a day). + + :param hour: ``0 <= hour < 24`` + :param minute: ``0 <= minute < 60`` + :param second: ``0 <= second < 60`` + :raises xlrd.xldate.XLDateBadTuple: Out-of-range hour, minute, or second + """ + hour, minute, second = time_tuple + if 0 <= hour < 24 and 0 <= minute < 60 and 0 <= second < 60: + return ((second / 60.0 + minute) / 60.0 + hour) / 24.0 + raise XLDateBadTuple("Invalid (hour, minute, second): %r" % ((hour, minute, second),)) + + +def xldate_from_datetime_tuple(datetime_tuple, datemode): + """ + Convert a datetime tuple ``(year, month, day, hour, minute, second)`` to an + Excel date value. + For more details, refer to other xldate_from_*_tuple functions. + + :param datetime_tuple: ``(year, month, day, hour, minute, second)`` + :param datemode: 0: 1900-based, 1: 1904-based. + """ + return ( + xldate_from_date_tuple(datetime_tuple[:3], datemode) + + xldate_from_time_tuple(datetime_tuple[3:]) + ) |