diff --git a/src/calibre/ebooks/docx/writer/from_html.py b/src/calibre/ebooks/docx/writer/from_html.py index 51d18e624a..f636e4dcc4 100644 --- a/src/calibre/ebooks/docx/writer/from_html.py +++ b/src/calibre/ebooks/docx/writer/from_html.py @@ -94,10 +94,10 @@ class TextRun(object): class Block(object): - def __init__(self, styles_manager, html_block, style): + def __init__(self, styles_manager, html_block, style, is_table_cell=False): self.html_block = html_block self.html_style = style - self.style = styles_manager.create_block_style(style, html_block) + self.style = styles_manager.create_block_style(style, html_block, is_table_cell=is_table_cell) self.styles_manager = styles_manager self.keep_next = False self.page_break_before = False @@ -181,9 +181,9 @@ class Blocks(object): self.items.append(self.current_block) self.current_block = None - def start_new_block(self, html_block, style): + def start_new_block(self, html_block, style, is_table_cell=False): self.end_current_block() - self.current_block = Block(self.styles_manager, html_block, style) + self.current_block = Block(self.styles_manager, html_block, style, is_table_cell=is_table_cell) self.open_html_blocks.add(html_block) return self.current_block @@ -289,7 +289,7 @@ class Convert(object): elif display.startswith('table') or display == 'inline-table': if display == 'table-cell': self.blocks.start_new_cell(html_tag, tag_style) - self.add_block_tag(tagname, html_tag, tag_style, stylizer) + self.add_block_tag(tagname, html_tag, tag_style, stylizer, is_table_cell=True) elif display == 'table-row': self.blocks.start_new_row(html_tag, tag_style) elif display in {'table', 'inline-table'}: @@ -318,8 +318,8 @@ class Convert(object): block = self.blocks.current_or_new_block(html_tag.getparent(), stylizer.style(html_tag.getparent())) block.add_text(html_tag.tail, stylizer.style(html_tag.getparent()), is_parent_style=True) - def add_block_tag(self, tagname, html_tag, tag_style, stylizer): - block = self.blocks.start_new_block(html_tag, tag_style) + def add_block_tag(self, tagname, html_tag, tag_style, stylizer, is_table_cell=False): + block = self.blocks.start_new_block(html_tag, tag_style, is_table_cell=is_table_cell) if tagname == 'img': self.images_manager.add_image(html_tag, block, stylizer) else: diff --git a/src/calibre/ebooks/docx/writer/styles.py b/src/calibre/ebooks/docx/writer/styles.py index 790cdcddaa..998a7a4a09 100644 --- a/src/calibre/ebooks/docx/writer/styles.py +++ b/src/calibre/ebooks/docx/writer/styles.py @@ -136,15 +136,21 @@ class TextStyle(DOCXStyle): # DOCX does not support individual borders/padding for inline content for edge in border_edges: # In DOCX padding can only be a positive integer - padding = max(0, int(css['padding-' + edge])) + try: + padding = max(0, int(css['padding-' + edge])) + except ValueError: + padding = 0 if self.padding is None: self.padding = padding elif self.padding != padding: self.padding = ignore - width = min(96, max(2, int({'thin':0.2, 'medium':1, 'thick':2}.get(css['border-%s-width' % edge], 0) * 8))) + val = css['border-%s-width' % edge] + if not isinstance(val, (float, int, long)): + val = {'thin':0.2, 'medium':1, 'thick':2}.get(val, 0) + val = min(96, max(2, int(val * 8))) if self.border_width is None: - self.border_width = width - elif self.border_width != width: + self.border_width = val + elif self.border_width != val: self.border_width = ignore color = convert_color(css['border-%s-color' % edge]) if self.border_color is None: @@ -225,6 +231,38 @@ class TextStyle(DOCXStyle): style_root.append(style) return style_root +def read_css_block_borders(self, css, store_css_style=False): + for edge in border_edges: + if css is None: + setattr(self, 'padding_' + edge, 0) + setattr(self, 'margin_' + edge, 0) + setattr(self, 'css_margin_' + edge, '') + setattr(self, 'border_%s_width' % edge, 2) + setattr(self, 'border_%s_color' % edge, None) + setattr(self, 'border_%s_style' % edge, 'none') + if store_css_style: + setattr(self, 'border_%s_css_style' % edge, 'none') + else: + # In DOCX padding can only be a positive integer + try: + setattr(self, 'padding_' + edge, max(0, int(css['padding-' + edge]))) + except ValueError: + setattr(self, 'padding_' + edge, 0) # invalid value for padding + # In DOCX margin must be a positive integer in twips (twentieth of a point) + try: + setattr(self, 'margin_' + edge, max(0, int(css['margin-' + edge] * 20))) + except ValueError: + setattr(self, 'margin_' + edge, 0) # for e.g.: margin: auto + setattr(self, 'css_margin_' + edge, css._style.get('margin-' + edge, '')) + val = css['border-%s-width' % edge] + if not isinstance(val, (float, int, long)): + val = {'thin':0.2, 'medium':1, 'thick':2}.get(val, 0) + val = min(96, max(2, int(val * 8))) + setattr(self, 'border_%s_width' % edge, val) + setattr(self, 'border_%s_color' % edge, convert_color(css['border-%s-color' % edge]) or 'auto') + setattr(self, 'border_%s_style' % edge, LINE_STYLES.get(css['border-%s-style' % edge].lower(), 'none')) + if store_css_style: + setattr(self, 'border_%s_css_style' % edge, css['border-%s-style' % edge].lower()) class BlockStyle(DOCXStyle): @@ -235,16 +273,14 @@ class BlockStyle(DOCXStyle): [x%edge for edge in border_edges for x in border_props] ) - def __init__(self, css, html_block): + def __init__(self, css, html_block, is_table_cell=False): + read_css_block_borders(self, css) + if is_table_cell: + for edge in border_edges: + setattr(self, 'border_%s_style' % edge, 'none') + setattr(self, 'border_%s_width' % edge, 0) if css is None: self.page_break_before = self.keep_lines = False - for edge in border_edges: - setattr(self, 'padding_' + edge, 0) - setattr(self, 'margin_' + edge, 0) - setattr(self, 'css_margin_' + edge, '') - setattr(self, 'border_%s_width' % edge, 2) - setattr(self, 'border_%s_color' % edge, None) - setattr(self, 'border_%s_style' % edge, 'none') self.text_indent = 0 self.css_text_indent = None self.line_height = 280 @@ -253,20 +289,10 @@ class BlockStyle(DOCXStyle): else: self.page_break_before = css['page-break-before'] == 'always' self.keep_lines = css['page-break-inside'] == 'avoid' - for edge in border_edges: - # In DOCX padding can only be a positive integer - setattr(self, 'padding_' + edge, max(0, int(css['padding-' + edge]))) - # In DOCX margin must be a positive integer in twips (twentieth of a point) - setattr(self, 'margin_' + edge, max(0, int(css['margin-' + edge] * 20))) - setattr(self, 'css_margin_' + edge, css._style.get('margin-' + edge, '')) - val = min(96, max(2, int({'thin':0.2, 'medium':1, 'thick':2}.get(css['border-%s-width' % edge], 0) * 8))) - setattr(self, 'border_%s_width' % edge, val) - setattr(self, 'border_%s_color' % edge, convert_color(css['border-%s-color' % edge])) - setattr(self, 'border_%s_style' % edge, LINE_STYLES.get(css['border-%s-style' % edge].lower(), 'none')) self.text_indent = max(0, int(css['text-indent'] * 20)) self.css_text_indent = css._get('text-indent') self.line_height = max(0, int(css.lineHeight * 20)) - self.background_color = convert_color(css['background-color']) + self.background_color = None if is_table_cell else convert_color(css['background-color']) self.text_align = {'start':'left', 'left':'left', 'end':'right', 'right':'right', 'center':'center', 'justify':'both', 'centre':'center'}.get( css['text-align'].lower(), 'left') @@ -377,8 +403,8 @@ class StylesManager(object): ans = existing return ans - def create_block_style(self, css_style, html_block): - ans = BlockStyle(css_style, html_block) + def create_block_style(self, css_style, html_block, is_table_cell=False): + ans = BlockStyle(css_style, html_block, is_table_cell=is_table_cell) existing = self.block_styles.get(ans, None) if existing is None: self.block_styles[ans] = ans diff --git a/src/calibre/ebooks/docx/writer/tables.py b/src/calibre/ebooks/docx/writer/tables.py index 02e6ac45b0..c339ea9b11 100644 --- a/src/calibre/ebooks/docx/writer/tables.py +++ b/src/calibre/ebooks/docx/writer/tables.py @@ -6,8 +6,30 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2015, Kovid Goyal ' +from collections import namedtuple + from calibre.ebooks.docx.names import makeelement from calibre.ebooks.docx.writer.utils import convert_color +from calibre.ebooks.docx.writer.styles import read_css_block_borders as rcbb, border_edges + +class Dummy(object): + pass + +Border = namedtuple('Border', 'css_style style width color level') +border_style_weight = { + x:100-i for i, x in enumerate(('double', 'solid', 'dashed', 'dotted', 'ridge', 'outset', 'groove', 'inset'))} + +def read_css_block_borders(self, css): + obj = Dummy() + rcbb(obj, css, store_css_style=True) + for edge in border_edges: + setattr(self, 'border_' + edge, Border( + getattr(obj, 'border_%s_css_style' % edge), + getattr(obj, 'border_%s_style' % edge), + getattr(obj, 'border_%s_width' % edge), + getattr(obj, 'border_%s_color' % edge), + self.BLEVEL + )) def as_percent(x): if x and x.endswith('%'): @@ -31,14 +53,26 @@ def convert_width(tag_style): pass return ('auto', 0) +def serialize_border_edge(self, bdr, edge): + width = getattr(self, 'border_%s_width' % edge) + bstyle = getattr(self, 'border_%s_style' % edge) + if width > 0 and bstyle != 'none': + makeelement(bdr, 'w:' + edge, w_val=bstyle, w_sz=str(width), w_color=getattr(self, 'border_%s_color' % edge)) + return True + return False + class Cell(object): + BLEVEL = 2 + def __init__(self, row, html_tag, tag_style): self.row = row + self.table = self.row.table self.html_tag = html_tag self.items = [] self.width = convert_width(tag_style) self.background_color = None if tag_style is None else convert_color(tag_style.backgroundColor) + read_css_block_borders(self, tag_style) def add_block(self, block): self.items.append(block) @@ -57,17 +91,90 @@ class Cell(object): bc = self.background_color or self.row.background_color or self.row.table.background_color if bc: makeelement(tcPr, 'w:shd', w_val="clear", w_color="auto", w_fill=bc) + b = makeelement(tcPr, 'w:tcBorders', append=False) + for edge, border in self.borders.iteritems(): + if border.width > 0 and border.style != 'none': + makeelement(b, 'w:' + edge, w_val=border.style, w_sz=str(border.width), w_color=border.color) + if len(b) > 0: + tcPr.append(b) + for item in self.items: item.serialize(tc) + def applicable_borders(self, edge): + if edge == 'left': + items = {self.table, self.row, self} if self.row.first_cell is self else {self} + elif edge == 'top': + items = ({self.table} if self.table.first_row is self.row else set()) | {self, self.row} + elif edge == 'right': + items = {self.table, self, self.row} if self.row.last_cell is self else {self} + elif edge == 'bottom': + items = ({self.table} if self.table.last_row is self.row else set()) | {self, self.row} + return {getattr(x, 'border_' + edge) for x in items} + + def resolve_border(self, edge): + # In Word cell borders override table borders, and Word ignores row + # borders, so we consolidate all borders as cell borders + # In HTML the priority is as described here: + # http://www.w3.org/TR/CSS21/tables.html#border-conflict-resolution + neighbor = self.neighbor(edge) + borders = self.applicable_borders(edge) + if neighbor is not None: + nedge = {'left':'right', 'top':'bottom', 'right':'left', 'bottom':'top'}[edge] + borders |= neighbor.applicable_borders(nedge) + + for b in borders: + if b.css_style == 'hidden': + return None + + def weight(border): + return ( + 0 if border.css_style == 'none' else 1, + border.width, + border_style_weight.get(border.css_style, 0), + border.level) + border = sorted(borders, key=weight)[-1] + return border + + def resolve_borders(self): + self.borders = {edge:self.resolve_border(edge) for edge in border_edges} + + def neighbor(self, edge): + idx = self.row.cells.index(self) + ans = None + if edge == 'left': + ans = self.row.cells[idx-1] if idx > 0 else None + elif edge == 'right': + ans = self.row.cells[idx+1] if (idx + 1) < len(self.row.cells) else None + elif edge == 'top': + ridx = self.table.rows.index(self.row) + if ridx > 0 and idx < len(self.table.rows[ridx-1].cells): + ans = self.table.rows[ridx-1].cells[idx] + elif edge == 'bottom': + ridx = self.table.rows.index(self.row) + if ridx + 1 < len(self.table.rows) and idx < len(self.table.rows[ridx+1].cells): + ans = self.table.rows[ridx+1].cells[idx] + return getattr(ans, 'spanning_cell', ans) + class Row(object): + BLEVEL = 1 + def __init__(self, table, html_tag, tag_style=None): self.table = table self.html_tag = html_tag self.cells = [] self.current_cell = None self.background_color = None if tag_style is None else convert_color(tag_style.backgroundColor) + read_css_block_borders(self, tag_style) + + @property + def first_cell(self): + return self.cells[0] if self.cells else None + + @property + def last_cell(self): + return self.cells[-1] if self.cells else None def start_new_cell(self, html_tag, tag_style): self.current_cell = Cell(self, html_tag, tag_style) @@ -86,14 +193,13 @@ class Row(object): def serialize(self, parent): tr = makeelement(parent, 'w:tr') - tblPrEx = makeelement(tr, 'w:tblPrEx') - if len(tblPrEx) == 0: - tr.remove(tblPrEx) for cell in self.cells: cell.serialize(tr) class Table(object): + BLEVEL = 0 + def __init__(self, html_tag, tag_style=None): self.html_tag = html_tag self.rows = [] @@ -105,6 +211,15 @@ class Table(object): ml, mr = tag_style._get('margin-left'), tag_style.get('margin-right') if ml == 'auto': self.jc = 'center' if mr == 'auto' else 'right' + read_css_block_borders(self, tag_style) + + @property + def first_row(self): + return self.rows[0] if self.rows else None + + @property + def last_row(self): + return self.rows[-1] if self.rows else None def finish_tag(self, html_tag): if self.current_row is not None: @@ -113,6 +228,10 @@ class Table(object): self.rows.append(self.current_row) self.current_row = None table_ended = self.html_tag is html_tag + if table_ended: + for row in self.rows: + for cell in row.cells: + cell.resolve_borders() return table_ended def start_new_row(self, html_tag, html_style):