mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
DOCX Output: Conversion of table borders
This commit is contained in:
parent
fb70f61b70
commit
999cfd81bd
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user