DOCX Output: Conversion of table borders

This commit is contained in:
Kovid Goyal 2015-04-06 16:55:12 +05:30
parent fb70f61b70
commit 999cfd81bd
3 changed files with 180 additions and 35 deletions

View File

@ -94,10 +94,10 @@ class TextRun(object):
class Block(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_block = html_block
self.html_style = style 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.styles_manager = styles_manager
self.keep_next = False self.keep_next = False
self.page_break_before = False self.page_break_before = False
@ -181,9 +181,9 @@ class Blocks(object):
self.items.append(self.current_block) self.items.append(self.current_block)
self.current_block = None 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.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) self.open_html_blocks.add(html_block)
return self.current_block return self.current_block
@ -289,7 +289,7 @@ class Convert(object):
elif display.startswith('table') or display == 'inline-table': elif display.startswith('table') or display == 'inline-table':
if display == 'table-cell': if display == 'table-cell':
self.blocks.start_new_cell(html_tag, tag_style) 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': elif display == 'table-row':
self.blocks.start_new_row(html_tag, tag_style) self.blocks.start_new_row(html_tag, tag_style)
elif display in {'table', 'inline-table'}: 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 = 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) 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): 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) block = self.blocks.start_new_block(html_tag, tag_style, is_table_cell=is_table_cell)
if tagname == 'img': if tagname == 'img':
self.images_manager.add_image(html_tag, block, stylizer) self.images_manager.add_image(html_tag, block, stylizer)
else: else:

View File

@ -136,15 +136,21 @@ class TextStyle(DOCXStyle):
# DOCX does not support individual borders/padding for inline content # DOCX does not support individual borders/padding for inline content
for edge in border_edges: for edge in border_edges:
# In DOCX padding can only be a positive integer # 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: if self.padding is None:
self.padding = padding self.padding = padding
elif self.padding != padding: elif self.padding != padding:
self.padding = ignore 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: if self.border_width is None:
self.border_width = width self.border_width = val
elif self.border_width != width: elif self.border_width != val:
self.border_width = ignore self.border_width = ignore
color = convert_color(css['border-%s-color' % edge]) color = convert_color(css['border-%s-color' % edge])
if self.border_color is None: if self.border_color is None:
@ -225,6 +231,38 @@ class TextStyle(DOCXStyle):
style_root.append(style) style_root.append(style)
return style_root 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): class BlockStyle(DOCXStyle):
@ -235,16 +273,14 @@ class BlockStyle(DOCXStyle):
[x%edge for edge in border_edges for x in border_props] [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: if css is None:
self.page_break_before = self.keep_lines = False 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.text_indent = 0
self.css_text_indent = None self.css_text_indent = None
self.line_height = 280 self.line_height = 280
@ -253,20 +289,10 @@ class BlockStyle(DOCXStyle):
else: else:
self.page_break_before = css['page-break-before'] == 'always' self.page_break_before = css['page-break-before'] == 'always'
self.keep_lines = css['page-break-inside'] == 'avoid' 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.text_indent = max(0, int(css['text-indent'] * 20))
self.css_text_indent = css._get('text-indent') self.css_text_indent = css._get('text-indent')
self.line_height = max(0, int(css.lineHeight * 20)) 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( self.text_align = {'start':'left', 'left':'left', 'end':'right', 'right':'right', 'center':'center', 'justify':'both', 'centre':'center'}.get(
css['text-align'].lower(), 'left') css['text-align'].lower(), 'left')
@ -377,8 +403,8 @@ class StylesManager(object):
ans = existing ans = existing
return ans return ans
def create_block_style(self, css_style, html_block): def create_block_style(self, css_style, html_block, is_table_cell=False):
ans = BlockStyle(css_style, html_block) ans = BlockStyle(css_style, html_block, is_table_cell=is_table_cell)
existing = self.block_styles.get(ans, None) existing = self.block_styles.get(ans, None)
if existing is None: if existing is None:
self.block_styles[ans] = ans self.block_styles[ans] = ans

View File

@ -6,8 +6,30 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
from collections import namedtuple
from calibre.ebooks.docx.names import makeelement from calibre.ebooks.docx.names import makeelement
from calibre.ebooks.docx.writer.utils import convert_color 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): def as_percent(x):
if x and x.endswith('%'): if x and x.endswith('%'):
@ -31,14 +53,26 @@ def convert_width(tag_style):
pass pass
return ('auto', 0) 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): class Cell(object):
BLEVEL = 2
def __init__(self, row, html_tag, tag_style): def __init__(self, row, html_tag, tag_style):
self.row = row self.row = row
self.table = self.row.table
self.html_tag = html_tag self.html_tag = html_tag
self.items = [] self.items = []
self.width = convert_width(tag_style) self.width = convert_width(tag_style)
self.background_color = None if tag_style is None else convert_color(tag_style.backgroundColor) 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): def add_block(self, block):
self.items.append(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 bc = self.background_color or self.row.background_color or self.row.table.background_color
if bc: if bc:
makeelement(tcPr, 'w:shd', w_val="clear", w_color="auto", w_fill=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: for item in self.items:
item.serialize(tc) 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): class Row(object):
BLEVEL = 1
def __init__(self, table, html_tag, tag_style=None): def __init__(self, table, html_tag, tag_style=None):
self.table = table self.table = table
self.html_tag = html_tag self.html_tag = html_tag
self.cells = [] self.cells = []
self.current_cell = None self.current_cell = None
self.background_color = None if tag_style is None else convert_color(tag_style.backgroundColor) 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): def start_new_cell(self, html_tag, tag_style):
self.current_cell = 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): def serialize(self, parent):
tr = makeelement(parent, 'w:tr') tr = makeelement(parent, 'w:tr')
tblPrEx = makeelement(tr, 'w:tblPrEx')
if len(tblPrEx) == 0:
tr.remove(tblPrEx)
for cell in self.cells: for cell in self.cells:
cell.serialize(tr) cell.serialize(tr)
class Table(object): class Table(object):
BLEVEL = 0
def __init__(self, html_tag, tag_style=None): def __init__(self, html_tag, tag_style=None):
self.html_tag = html_tag self.html_tag = html_tag
self.rows = [] self.rows = []
@ -105,6 +211,15 @@ class Table(object):
ml, mr = tag_style._get('margin-left'), tag_style.get('margin-right') ml, mr = tag_style._get('margin-left'), tag_style.get('margin-right')
if ml == 'auto': if ml == 'auto':
self.jc = 'center' if mr == 'auto' else 'right' 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): def finish_tag(self, html_tag):
if self.current_row is not None: if self.current_row is not None:
@ -113,6 +228,10 @@ class Table(object):
self.rows.append(self.current_row) self.rows.append(self.current_row)
self.current_row = None self.current_row = None
table_ended = self.html_tag is html_tag 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 return table_ended
def start_new_row(self, html_tag, html_style): def start_new_row(self, html_tag, html_style):