# Purpose: table, consisting of basic R12 entities # Created: 18.03.2010, 2018 adapted for ezdxf # Copyright (c) 2010-2018, Manfred Moitzi # License: MIT License """ Table object like a HTML-Table, as composite pf basic DXF R12 entities. Cells can contain MText or BLOCK references, or you can create your own cell type by extending the CustomCell() class. Cells can span over columns and rows. Text cells can contain text with an arbitrary rotation angle, or letters can be stacked from top to bottom. BlockCells contains block references (INSERT) created from a block definition (BLOCK), if the block definition contains attribute definitions (ATTDEF), attribs will be added to the block reference (ATTRIB). """ from copy import deepcopy from abc import abstractmethod from ezdxf.lldxf import const from .mtext import MText DEFAULT_TABLE_BGLAYER = 'TABLEBACKGROUND' DEFAULT_TABLE_FGLAYER = 'TABLECONTENT' DEFAULT_TABLE_GRIDLAYER = 'TABLEGRID' DEFAULT_TABLE_HEIGHT = 1.0 DEFAULT_TABLE_WIDTH = 2.5 DEFAULT_TEXTSTYLE = 'STANDARD' DEFAULT_CELL_TEXT_HEIGHT = 0.7 DEFAULT_CELL_LINESPACING = 1.5 DEFAULT_CELL_XSCALE = 1.0 DEFAULT_CELL_YSCALE = 1.0 DEFAULT_CELL_TEXTCOLOR = const.BYLAYER DEFAULT_CELL_BG_COLOR = None DEFAULT_CELL_HMARGIN = 0.1 DEFAULT_CELL_VMARGIN = 0.1 DEFAULT_BORDER_COLOR = 5 DEFAULT_BORDER_LINETYPE = "BYLAYER" DEFAULT_BORDER_STATUS = True DEFAULT_BORDER_PRIORITY = 50 VISIBLE = 1 HIDDEN = 0 class Table(object): """A HTML-table like object. The table object contains the table data cells. """ name = 'TABLE' def __init__(self, insert, nrows, ncols, default_grid=True): """ Args: insert: insert point as tuple (x, y[, z]) nrows: row count ncols: column count default_grid: draw a solid line grid if True, else draw only explicit defined borders, default grid has a priority of 50. """ self.insert = insert self.nrows = nrows self.ncols = ncols self.row_heights = [DEFAULT_TABLE_HEIGHT] * nrows self.col_widths = [DEFAULT_TABLE_WIDTH] * ncols self.bglayer = DEFAULT_TABLE_BGLAYER self.fglayer = DEFAULT_TABLE_FGLAYER self.gridlayer = DEFAULT_TABLE_GRIDLAYER self.styles = {'default': Style.get_default_cell_style()} if not default_grid: default_style = self.get_cell_style('default') default_style.set_border_status(False, False, False, False) self._cells = {} # data cells self.frames = [] # border frame objects # data contains the resulting dxf entities self.data = None self.empty_cell = Cell(self) # represents all empty cells def set_col_width(self, column, value): """ Set column width to value (in drawing units). Args: column: zero based column index value: new column width in drawing units """ self.col_widths[column] = float(value) def set_row_height(self, row, value): """ Set row height to value (in drawing units). Args: row: zero based row index value: new row height in drawing units """ self.row_heights[row] = float(value) def text_cell(self, row, col, text, span=(1, 1), style='default'): """ Create a new text cell at position (row, col), with 'text' as content, text can be a multi-line text, use ``'\\n'`` as line separator. The cell spans over **span** cells and has the cell style with the name **style**. """ cell = TextCell(self, text, style=style, span=span) return self.set_cell(row, col, cell) def block_cell(self, row, col, blockdef, span=(1, 1), attribs=None, style='default'): """ Create a new block cell at position (row, col). Content is a block reference inserted by an INSERT entity, attributes will be added if the block definition contains ATTDEF. Assignments are defined by attribs-key to attdef-tag association. Example: attribs = {'num': 1} if an ATTDEF with tag=='num' in the block definition exists, an attrib with text=str(1) will be created and added to the insert entity. The cell spans over 'span' cells and has the cell style with the name 'style'. """ if attribs is None: attribs = {} cell = BlockCell(self, blockdef, style=style, attribs=attribs, span=span) return self.set_cell(row, col, cell) def set_cell(self, row, col, cell): """ Insert a cell at position (row, col). """ row, col = self.validate_index(row, col) self._cells[row, col] = cell return cell def get_cell(self, row, col): """ Get cell at position (row, col). """ row, col = self.validate_index(row, col) try: return self._cells[row, col] except KeyError: return self.empty_cell # empty cell with default style def validate_index(self, row, col): row = int(row) col = int(col) if row < 0 or row >= self.nrows or \ col < 0 or col >= self.ncols: raise IndexError('cell index out of range') return row, col def frame(self, row, col, width=1, height=1, style='default'): """ Create a Frame object which frames the cell area starting at(row, col) covering 'width' columns and 'height' rows. """ frame = Frame(self, pos=(row, col), span=(height, width), style=style) self.frames.append(frame) return frame def new_cell_style(self, name, **kwargs): """ Create a new Style object 'name'. Args: kwargs: see Style.get_default_cell_style() """ style = deepcopy(self.get_cell_style('default')) style.update(kwargs) if 'align' in kwargs: align = kwargs.get('align') halign, valign = const.TEXT_ALIGN_FLAGS.get(align) style['halign'] = halign style['valign'] = valign else: halign = kwargs.get('halign') valign = kwargs.get('valign') style['align'] = const.TEXT_ALIGNMENT_BY_FLAGS.get(halign, valign) self.styles[name] = style return style def new_border_style(self, color=const.BYLAYER, status=True, priority=100, linetype="BYLAYER"): """ Create a new border style. Args: status: True for visible, else False color: dxf color index linetype: linetype name, BYLAYER if None priority: drawing priority - higher values covers lower values """ border_style = Style.get_default_border_style() border_style['color'] = color border_style['linetype'] = linetype border_style['status'] = status border_style['priority'] = priority return border_style def get_cell_style(self, name): """ Get cell style by name. """ return self.styles[name] def iter_visible_cells(self, visibility_map): """ Iterate over all visible cells. Returns: a generator which yields all visible cells as tuples: (row , col, cell) """ return ((row, col, self.get_cell(row, col)) for row, col in visibility_map) def render(self, layout, insert=None): """ Render table to layout object. """ _insert = self.insert if insert is not None: self.insert = insert visibility_map = VisibilityMap(self) grid = Grid(self) grid.render_lines(layout, visibility_map) for row, col, cell in self.iter_visible_cells(visibility_map): grid.render_cell_background(layout, row, col, cell) grid.render_cell_content(layout, row, col, cell) self.insert = _insert class VisibilityMap(object): """ Stores the visibility of the table cells. """ def __init__(self, table): """ Create the visibility map for table. """ self.table = table self._hidden_cells = {} self._create_visibility_map() def _create_visibility_map(self): """ Set visibility for all existing cells. """ for row, col in iter(self): cell = self.table.get_cell(row, col) self._set_span_visibility(row, col, cell.span) def _set_span_visibility(self, row, col, span): """ Set the visibility of the given cell. The cell itself is visible, all other cells in the span-range (tuple: width, height) are invisible, they are covered by the main cell (row, col). """ if span != (1, 1): nrows, ncols = span for rowx in range(nrows): for colx in range(ncols): # switch all cells in span range to invisible self.hide(row+rowx, col+colx) # switch content cell visible self.show(row, col) def show(self, row, col): """ Show cell (row, col). """ try: del self._hidden_cells[(row, col)] except KeyError: pass def hide(self, row, col): """ Hide cell (row, col). """ self._hidden_cells[(row, col)] = HIDDEN def iter_all_cells(self): """ Iterate over all cell indices, yields (row, col) tuples. """ for row in range(self.table.nrows): for col in range(self.table.ncols): yield row, col def is_visible_cell(self, row, col): """ True if cell (row, col) is visible, else False. """ return (row, col) not in self._hidden_cells def __iter__(self): """ Iterate over all visible cells. """ return ((row, col) for (row, col) in self.iter_all_cells() if self.is_visible_cell(row, col)) class Style(dict): """ Cell style object. """ @staticmethod def get_default_cell_style(): return Style({ # textstyle is ignored by block cells 'textstyle': 'STANDARD', # text height in drawing units, ignored by block cells 'textheight': DEFAULT_CELL_TEXT_HEIGHT, # line spacing in percent = *, ignored by block cells 'linespacing': DEFAULT_CELL_LINESPACING, # text stretch or block reference x-axis scaling factor 'xscale': DEFAULT_CELL_XSCALE, # block reference y-axis scaling factor, ignored by text cells 'yscale': DEFAULT_CELL_YSCALE, # dxf color index, ignored by block cells 'textcolor': DEFAULT_CELL_TEXTCOLOR, # text or block rotation in degrees 'rotation': 0., # Letters are stacked top-to-bottom, but not rotated 'stacked': False, # simple combined align parameter, like 'TOP_CENTER', see also MText.VALID_ALIGN 'align': 'TOP_CENTER', # higher priority than 'haling' and 'valign' # horizontal alignment (const.LEFT, const.CENTER, const.RIGHT) 'halign': const.CENTER, # vertical alignment (const.TOP, const.MIDDLE, const.BOTTOM) 'valign': const.TOP, # left and right margin in drawing units 'hmargin': DEFAULT_CELL_HMARGIN, # top and bottom margin 'vmargin': DEFAULT_CELL_VMARGIN, # background color, dxf color index, ignored by block cells 'bgcolor': DEFAULT_CELL_BG_COLOR, # left border style 'left': Style.get_default_border_style(), # top border style 'top': Style.get_default_border_style(), # right border style 'right': Style.get_default_border_style(), # bottom border style 'bottom': Style.get_default_border_style(), }) @staticmethod def get_default_border_style(): return { # border status, True for visible, False for hidden 'status': DEFAULT_BORDER_STATUS, # dxf color index 'color': DEFAULT_BORDER_COLOR, # linetype name, BYLAYER if None 'linetype': DEFAULT_BORDER_LINETYPE, # drawing priority, higher values cover lower values 'priority': DEFAULT_BORDER_PRIORITY, } def set_border_status(self, left=True, right=True, top=True, bottom=True): """ Set status of all cell borders at once. """ for border, status in (('left', left), ('right', right), ('top', top), ('bottom', bottom)): self[border]['status'] = status def set_border_style(self, style, left=True, right=True, top=True, bottom=True): """ Set border styles of all cell borders at once. """ for border, status in (('left', left), ('right', right), ('top', top), ('bottom', bottom)): if status: self[border] = style class Grid(object): """ Grid contains the graphical representation of the table. """ def __init__(self, table): self.table = table # contains the x-axis coords of the grid lines between the data columns. self.col_pos = self._calc_col_pos() # contains the y-axis coords of the grid lines between the data rows. self.row_pos = self._calc_row_pos() # contains the horizontal border elements, list of border styles # get index with _border_index(row, col), which means the border element # above row, col, and row-indices are [0 .. nrows+1], nrows+1 for the # grid line below the last row; list contains only the border style with # the highest priority. self._hborders = None # created in _init_borders # same as _hborders but for the vertical borders, # col-indices are [0 .. ncols+1], ncols+1 for the last grid line right # of the last column self._vborders = None # created in _init_borders # border style to delete borders inside of merged cells self.noborder = dict(status=False, priority=999, linetype="BYLAYER", color=0) def _init_borders(self, hborder, vborder): """ Init the _hborders with and _vborders with . """ # has more elements than necessary, but it unifies the # index calculation for _vborders and _hborders. # exact values are: # hborder_count = ncols * (nrows+1), hindex = ncols * + # vborder_count = nrows * (ncols+1), vindex = (ncols+1) * + border_count = (self.table.nrows+1) * (self.table.ncols+1) self._hborders = [hborder] * border_count self._vborders = [vborder] * border_count def _border_index(self, row, col): """ Calculate linear index for border arrays _hborders and _vborders. """ return row * (self.table.ncols+1) + col def set_hborder(self, row, col, border_style): """ Set for the horizontal border element above , . """ return self._set_border_style(self._hborders, row, col, border_style) def set_vborder(self, row, col, border_style): """ Set for the vertical border element left of , . """ return self._set_border_style(self._vborders, row, col, border_style) def _set_border_style(self, borders, row, col, border_style): """ Set for , in . """ border_index = self._border_index(row, col) actual_borderstyle = borders[border_index] if border_style['priority'] >= actual_borderstyle['priority']: borders[border_index] = border_style def get_hborder(self, row, col): """ Get the horizontal border element above , . Last grid line (below ) is the element above of . """ return self._get_border(self._hborders, row, col) def get_vborder(self, row, col): """ Get the vertical border element left of , . Last grid line (right of ) is the element left of . """ return self._get_border(self._vborders, row, col) def _get_border(self, borders, row, col): """ Get border element at , from . """ return borders[self._border_index(row, col)] def _sum_fields(self, start_value, fields, append, sign=1.): """ Adds step-by-step the fields-values, starting with , and appends the resulting values to an other object with the append-method. """ position = start_value append(position) for element in fields: position += element * sign append(position) def _calc_col_pos(self): """ Calculate the x-axis coords of the grid lines between the columns. """ col_pos = [] start_x = self.table.insert[0] self._sum_fields(start_x, self.table.col_widths, col_pos.append) return col_pos def _calc_row_pos(self): """ Calculate the y-axis coords of the grid lines between the rows. """ row_pos = [] start_y = self.table.insert[1] self._sum_fields(start_y, self.table.row_heights, row_pos.append, -1.) return row_pos def cell_coords(self, row, col, span): """ Get the coordinates of the cell , as absolute drawing units. :return: a tuple (left, right, top, bottom) """ top = self.row_pos[row] bottom = self.row_pos[row+span[0]] left = self.col_pos[col] right = self.col_pos[col+span[1]] return left, right, top, bottom def render_cell_background(self, layout, row, col, cell): """ Render the cell background for , as SOLID entity. """ style = cell.style if style['bgcolor'] is None: return # get cell coords in absolute drawing units left, right, top, bottom = self.cell_coords(row, col, cell.span) ltop = (left, top) lbot = (left, bottom) rtop = (right, top) rbot = (right, bottom) layout.add_solid( points=(ltop, lbot, rtop, rbot), dxfattribs={ 'color': style['bgcolor'], 'layer': self.table.bglayer, }) def render_cell_content(self, layout, row, col, cell): """ Render the cell content for , into layout object. """ # get cell coords in absolute drawing units coords = self.cell_coords(row, col, cell.span) cell.render(layout, coords, self.table.fglayer) def render_lines(self, layout, visibility_map): """ Render all grid lines into layout object. """ # Init borders with default_style top- and left border. default_style = self.table.get_cell_style('default') hborder = default_style['top'] vborder = default_style['left'] self._init_borders(hborder, vborder) self._set_frames(self.table.frames) self._set_borders(self.table.iter_visible_cells(visibility_map)) self._render_borders(layout, self.table) def _set_borders(self, visible_cells): """ Set borders of the visible cells. """ for row, col, cell in visible_cells: bottom_row = row + cell.span[0] right_col = col + cell.span[1] self._set_rect_borders(row, bottom_row, col, right_col, cell.style) self._set_inner_borders(row, bottom_row, col, right_col, self.noborder) def _set_inner_borders(self, top_row, bottom_row, left_col, right_col, border_style): """ Set to the inner borders of the rectangle 1: for col in range(left_col, right_col): for row in range(top_row+1, bottom_row): self.set_hborder(row, col, border_style) if right_col - left_col > 1: for row in range(top_row, bottom_row): for col in range(left_col+1, right_col): self.set_vborder(row, col, border_style) def _set_rect_borders(self, top_row, bottom_row, left_col, right_col, style): """ Set border