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):
|
||||
|
||||
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:
|
||||
|
@ -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
|
||||
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
|
||||
|
@ -6,8 +6,30 @@ from __future__ import (unicode_literals, division, absolute_import,
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user