mirror of
https://github.com/gryf/ebook-converter.git
synced 2026-01-31 10:55:44 +01:00
368 lines
14 KiB
Python
368 lines
14 KiB
Python
from collections import namedtuple
|
|
|
|
from ebook_converter.ebooks.docx.writer.utils import convert_color
|
|
from ebook_converter.ebooks.docx.writer.styles import read_css_block_borders as rcbb, border_edges
|
|
|
|
|
|
__license__ = 'GPL v3'
|
|
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
|
|
|
|
|
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'))}
|
|
|
|
|
|
class SpannedCell(object):
|
|
|
|
def __init__(self, spanning_cell, horizontal=True):
|
|
self.spanning_cell = spanning_cell
|
|
self.horizontal = horizontal
|
|
self.row_span = self.col_span = 1
|
|
|
|
def resolve_borders(self):
|
|
pass
|
|
|
|
def serialize(self, tr, makeelement):
|
|
tc = makeelement(tr, 'w:tc')
|
|
tcPr = makeelement(tc, 'w:tcPr')
|
|
makeelement(tcPr, 'w:%sMerge' % ('h' if self.horizontal else 'v'), w_val='continue')
|
|
makeelement(tc, 'w:p')
|
|
|
|
def applicable_borders(self, edge):
|
|
return self.spanning_cell.applicable_borders(edge)
|
|
|
|
|
|
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
|
|
))
|
|
setattr(self, 'padding_' + edge, getattr(obj, 'padding_' + edge))
|
|
|
|
|
|
def as_percent(x):
|
|
if x and x.endswith('%'):
|
|
try:
|
|
return float(x.rstrip('%'))
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def convert_width(tag_style):
|
|
if tag_style is not None:
|
|
w = tag_style._get('width')
|
|
wp = as_percent(w)
|
|
if w == 'auto':
|
|
return ('auto', 0)
|
|
elif wp is not None:
|
|
return ('pct', int(wp * 50))
|
|
else:
|
|
try:
|
|
return ('dxa', int(float(tag_style['width']) * 20))
|
|
except Exception:
|
|
pass
|
|
return ('auto', 0)
|
|
|
|
|
|
class Cell(object):
|
|
|
|
BLEVEL = 2
|
|
|
|
def __init__(self, row, html_tag, tag_style=None):
|
|
self.row = row
|
|
self.table = self.row.table
|
|
self.html_tag = html_tag
|
|
try:
|
|
self.row_span = max(0, int(html_tag.get('rowspan', 1)))
|
|
except Exception:
|
|
self.row_span = 1
|
|
try:
|
|
self.col_span = max(0, int(html_tag.get('colspan', 1)))
|
|
except Exception:
|
|
self.col_span = 1
|
|
if tag_style is None:
|
|
self.valign = 'center'
|
|
else:
|
|
self.valign = {'top':'top', 'bottom':'bottom', 'middle':'center'}.get(tag_style._get('vertical-align'))
|
|
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)
|
|
block.parent_items = self.items
|
|
|
|
def add_table(self, table):
|
|
self.items.append(table)
|
|
return table
|
|
|
|
def serialize(self, parent, makeelement):
|
|
tc = makeelement(parent, 'w:tc')
|
|
tcPr = makeelement(tc, 'w:tcPr')
|
|
makeelement(tcPr, 'w:tcW', w_type=self.width[0], w_w=str(self.width[1]))
|
|
# For some reason, Word 2007 refuses to honor <w:shd> at the table or row
|
|
# level, despite what the specs say, so we inherit and apply at the
|
|
# cell level
|
|
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.items():
|
|
if border is not None and 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)
|
|
|
|
m = makeelement(tcPr, 'w:tcMar', append=False)
|
|
for edge in border_edges:
|
|
padding = getattr(self, 'padding_' + edge)
|
|
if edge in {'top', 'bottom'} or (edge == 'left' and self is self.row.first_cell) or (edge == 'right' and self is self.row.last_cell):
|
|
padding += getattr(self.row, 'padding_' + edge)
|
|
if padding > 0:
|
|
makeelement(m, 'w:' + edge, w_type='dxa', w_w=str(int(padding * 20)))
|
|
if len(m) > 0:
|
|
tcPr.append(m)
|
|
|
|
if self.valign is not None:
|
|
makeelement(tcPr, 'w:vAlign', w_val=self.valign)
|
|
|
|
if self.row_span > 1:
|
|
makeelement(tcPr, 'w:vMerge', w_val='restart')
|
|
if self.col_span > 1:
|
|
makeelement(tcPr, 'w:hMerge', w_val='restart')
|
|
|
|
item = None
|
|
for item in self.items:
|
|
item.serialize(tc)
|
|
if item is None or isinstance(item, Table):
|
|
# Word 2007 requires the last element in a table cell to be a paragraph
|
|
makeelement(tc, 'w:p')
|
|
|
|
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.orig_tag_style = tag_style
|
|
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)
|
|
|
|
def finish_tag(self, html_tag):
|
|
if self.current_cell is not None:
|
|
if html_tag is self.current_cell.html_tag:
|
|
self.cells.append(self.current_cell)
|
|
self.current_cell = None
|
|
|
|
def add_block(self, block):
|
|
if self.current_cell is None:
|
|
self.start_new_cell(self.html_tag, self.orig_tag_style)
|
|
self.current_cell.add_block(block)
|
|
|
|
def add_table(self, table):
|
|
if self.current_cell is None:
|
|
self.current_cell = Cell(self, self.html_tag, self.orig_tag_style)
|
|
return self.current_cell.add_table(table)
|
|
|
|
def serialize(self, parent, makeelement):
|
|
tr = makeelement(parent, 'w:tr')
|
|
for cell in self.cells:
|
|
cell.serialize(tr, makeelement)
|
|
|
|
|
|
class Table(object):
|
|
|
|
BLEVEL = 0
|
|
|
|
def __init__(self, namespace, html_tag, tag_style=None):
|
|
self.namespace = namespace
|
|
self.html_tag = html_tag
|
|
self.orig_tag_style = tag_style
|
|
self.rows = []
|
|
self.current_row = None
|
|
self.width = convert_width(tag_style)
|
|
self.background_color = None if tag_style is None else convert_color(tag_style.backgroundColor)
|
|
self.jc = None
|
|
self.float = None
|
|
self.margin_left = self.margin_right = self.margin_top = self.margin_bottom = None
|
|
if tag_style is not None:
|
|
ml, mr = tag_style._get('margin-left'), tag_style.get('margin-right')
|
|
if ml == 'auto':
|
|
self.jc = 'center' if mr == 'auto' else 'right'
|
|
self.float = tag_style['float']
|
|
for edge in border_edges:
|
|
setattr(self, 'margin_' + edge, tag_style['margin-' + edge])
|
|
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:
|
|
self.current_row.finish_tag(html_tag)
|
|
if self.current_row.html_tag is html_tag:
|
|
self.rows.append(self.current_row)
|
|
self.current_row = None
|
|
table_ended = self.html_tag is html_tag
|
|
if table_ended:
|
|
self.expand_spanned_cells()
|
|
for row in self.rows:
|
|
for cell in row.cells:
|
|
cell.resolve_borders()
|
|
return table_ended
|
|
|
|
def expand_spanned_cells(self):
|
|
# Expand horizontally
|
|
for row in self.rows:
|
|
for cell in tuple(row.cells):
|
|
idx = row.cells.index(cell)
|
|
if cell.col_span > 1 and (cell is row.cells[-1] or not isinstance(row.cells[idx+1], SpannedCell)):
|
|
row.cells[idx:idx+1] = [cell] + [SpannedCell(cell, horizontal=True) for i in range(1, cell.col_span)]
|
|
|
|
# Expand vertically
|
|
for r, row in enumerate(self.rows):
|
|
for idx, cell in enumerate(row.cells):
|
|
if cell.row_span > 1:
|
|
for nrow in self.rows[r+1:]:
|
|
sc = SpannedCell(cell, horizontal=False)
|
|
try:
|
|
tcell = nrow.cells[idx]
|
|
except Exception:
|
|
tcell = None
|
|
if tcell is None:
|
|
nrow.cells.extend([SpannedCell(nrow.cells[-1], horizontal=True) for i in range(idx - len(nrow.cells))])
|
|
nrow.cells.append(sc)
|
|
else:
|
|
if isinstance(tcell, SpannedCell):
|
|
# Conflict between rowspan and colspan
|
|
break
|
|
else:
|
|
nrow.cells.insert(idx, sc)
|
|
|
|
def start_new_row(self, html_tag, html_style):
|
|
if self.current_row is not None:
|
|
self.rows.append(self.current_row)
|
|
self.current_row = Row(self, html_tag, html_style)
|
|
|
|
def start_new_cell(self, html_tag, html_style):
|
|
if self.current_row is None:
|
|
self.start_new_row(html_tag, None)
|
|
self.current_row.start_new_cell(html_tag, html_style)
|
|
|
|
def add_block(self, block):
|
|
self.current_row.add_block(block)
|
|
|
|
def add_table(self, table):
|
|
if self.current_row is None:
|
|
self.current_row = Row(self, self.html_tag, self.orig_tag_style)
|
|
return self.current_row.add_table(table)
|
|
|
|
def serialize(self, parent):
|
|
makeelement = self.namespace.makeelement
|
|
rows = [r for r in self.rows if r.cells]
|
|
if not rows:
|
|
return
|
|
tbl = makeelement(parent, 'w:tbl')
|
|
tblPr = makeelement(tbl, 'w:tblPr')
|
|
makeelement(tblPr, 'w:tblW', w_type=self.width[0], w_w=str(self.width[1]))
|
|
if self.float in {'left', 'right'}:
|
|
kw = {'w_vertAnchor':'text', 'w_horzAnchor':'text', 'w_tblpXSpec':self.float}
|
|
for edge in border_edges:
|
|
val = getattr(self, 'margin_' + edge) or 0
|
|
if {self.float, edge} == {'left', 'right'}:
|
|
val = max(val, 2)
|
|
kw['w_' + edge + 'FromText'] = str(max(0, int(val *20)))
|
|
makeelement(tblPr, 'w:tblpPr', **kw)
|
|
if self.jc is not None:
|
|
makeelement(tblPr, 'w:jc', w_val=self.jc)
|
|
for row in rows:
|
|
row.serialize(tbl, makeelement)
|