Scheduler builder enhancements #3154

Now it supports
1) print ranges - in svg you'll get only the part that's included in print-range.
2) children styles - sometimes cells inherit center alignment from column styles and before that commit text wouldn't be centered in svg in that case
3) repeating rows support
This commit is contained in:
Andrej730
2023-05-15 18:02:41 +05:00
parent 38cf88d5d2
commit bfb59dd387
@@ -19,14 +19,36 @@
from blenderbim.bim.module.drawing.svgwriter import SvgWriter
import svgwrite
from odf.opendocument import load
from odf.opendocument import load as load_ods
from odf.table import Table, TableRow, TableColumn, TableCell
from odf.text import P
from odf.style import Style
from textwrap import wrap
from pathlib import Path
import string
FONT_SIZE = 4.13
FONT_WIDTH = FONT_SIZE * 0.45
DEBUG = False
def col2num(col):
"""convert letter column index to number:
`"A" -> 1`, `"AA" -> 27``
"""
num = 0
for c in col:
if c in string.ascii_letters:
num = num * 26 + (ord(c.upper()) - ord("A")) + 1
return num
def a1_to_rc(cell):
"""convert cell index from A1 format to RC: `"A1" -> (0,0)`"""
column_letter = cell.strip(string.digits)
col_number = col2num(column_letter) - 1
row_number = int(cell[len(column_letter) :]) - 1
return row_number, col_number
class Scheduler:
@@ -38,21 +60,33 @@ class Scheduler:
)
self.padding = 1
self.margin = 1
doc = load(infile)
doc = load_ods(infile)
# useful for debugging ods
if DEBUG:
import xml.dom.minidom
path = Path(infile)
dom = xml.dom.minidom.parseString(doc.xml())
pretty_xml = dom.toprettyxml()
with open(path.with_suffix(".xml"), "w") as fo:
fo.write(pretty_xml)
styles = {}
for style in doc.getElementsByType(Style):
name = style.getAttribute("name")
for cell_style in doc.getElementsByType(Style):
name = cell_style.getAttribute("name")
styles[name] = {}
# NOTE: there are also styles that inherit from parent styles that we do not process atm
if not style.firstChild:
if not cell_style.firstChild:
continue
if style.firstChild.tagName in ["style:table-column-properties", "style:table-row-properties"]:
style_children = [style.firstChild]
if cell_style.firstChild.tagName in ["style:table-column-properties", "style:table-row-properties"]:
style_children = [cell_style.firstChild]
else:
# for style:table-cell-properties we need to collect also text and paragraph properties
style_children = style.childNodes
style_children = cell_style.childNodes
for child in style_children:
child_params = {key[1]: value for key, value in child.attributes.items()}
@@ -60,116 +94,210 @@ class Scheduler:
table = doc.getElementsByType(Table)[0]
# related styles stored as a list of tuples:
# [(child, parent), ...]
related_styles = []
# collect columns width
column_widths = []
column_styles = []
for col in table.getElementsByType(TableColumn):
style_name = col.getAttribute("stylename")
repeat = col.getAttribute("numbercolumnsrepeated")
repeat = int(repeat) if repeat else 1
for i in range(repeat):
col_repeat = col.getAttribute("numbercolumnsrepeated")
col_repeat = int(col_repeat) if col_repeat else 1
for i in range(col_repeat):
if not style_name or "column-width" not in styles[style_name]:
column_width = 50
else:
column_width = self.convert_to_mm(styles[style_name]["column-width"])
column_styles.append(style_name)
column_widths.append(column_width)
cell_style = col.getAttribute("defaultcellstylename")
if cell_style:
related_styles.append((style_name, cell_style))
# collect rows height
row_heights = []
# TODO: never used yet because unsure about priority for row styles
# over column styles or vice versa
row_styles = []
for col in table.getElementsByType(TableRow):
style_name = col.getAttribute("stylename")
repeat = col.getAttribute("numberrowsrepeated")
repeat = int(repeat) if repeat else 1
for i in range(repeat):
row_repeat = col.getAttribute("numberrowsrepeated")
row_repeat = int(row_repeat) if row_repeat else 1
for i in range(row_repeat):
if not style_name or "row-height" not in styles[style_name]:
row_height = 6
else:
row_height = self.convert_to_mm(styles[style_name]["row-height"])
row_styles.append(style_name)
row_heights.append(row_height)
cell_style = col.getAttribute("defaultcellstylename")
if cell_style:
related_styles.append((style_name, cell_style))
while len(related_styles) > 0:
# unzip related styles to children and parents
children, parents = zip(*related_styles)
independent_styles = set(parents) - set(children)
for relation in related_styles[:]:
child, parent = relation
if parent in independent_styles:
child_style = styles[child]
styles[child] = styles[parent] | child_style
related_styles.remove(relation)
# TODO: multiple print ranges? 😔
print_range = table.getAttribute("printranges")
if print_range:
min_rc, max_rc = [a1_to_rc(cell.rsplit(".", 1)[1]) for cell in print_range.split(":")]
else:
# fallback if print range is not defined
min_rc, max_rc = (0,0), (1048576, 16384)
min_row, min_col = min_rc
max_row, max_col = max_rc
# draw table
y = self.margin
for tri, tr in enumerate(table.getElementsByType(TableRow)):
x = self.margin
height = row_heights[tri]
tdi = 0
tri = 0
stop_iterating_over_rows = False
# TODO: row spans support?
for tr in table.getElementsByType(TableRow):
if stop_iterating_over_rows:
break
for td in tr.getElementsByType(TableCell):
column_span = td.getAttribute("numbercolumnsspanned")
column_span = int(column_span) if column_span else 1
row_repeat = tr.getAttribute("numberrowsrepeated")
row_repeat = int(row_repeat) if row_repeat else 1
repeat = td.getAttribute("numbercolumnsrepeated")
repeat = int(repeat) if repeat else 1
for i_row_repeat in range(row_repeat):
if tri < min_row:
tri += 1
continue
elif tri > max_row:
stop_iterating_over_rows = True
break
# figuring text alignment
style_name = td.getAttribute("stylename")
style = styles[style_name] if style_name else {}
if style_name and "vertical-align" in style and style["vertical-align"] != "automatic":
vertical_align = style["vertical-align"]
else:
vertical_align = "bottom"
x = self.margin
height = row_heights[tri]
tdi = 0
stop_iterating_over_columns = False
alignment_translation = {
"center": "middle",
"end": "right",
"start": "left",
}
for td in tr.getElementsByType(TableCell):
if stop_iterating_over_columns:
break
if style_name and "text-align" in style and style["text-align"] != "automatic":
horizontal_align = style["text-align"]
horizontal_align = alignment_translation.get(horizontal_align, horizontal_align)
else:
horizontal_align = "left"
column_span = td.getAttribute("numbercolumnsspanned")
column_span = int(column_span) if column_span else 1
if vertical_align == "middle" and horizontal_align == "middle":
box_alignment = "center"
else:
box_alignment = f"{vertical_align}-{horizontal_align}"
col_repeat = td.getAttribute("numbercolumnsrepeated")
col_repeat = int(col_repeat) if col_repeat else 1
# for future use
wrap_text = style.get("wrap-option", None) == "wrap"
# figuring text alignment
cell_style = self.get_style(td.getAttribute("stylename"), styles)
# drawing cells and text
for i in range(0, repeat):
width = sum(column_widths[tdi : tdi + int(column_span)])
self.svg.add(
self.svg.rect(
insert=(x, y),
size=(width, height),
style="fill: #ffffff; stroke-width:.125; stroke: #000000;",
# drawing cells and text
for i_col_repeat in range(col_repeat):
start_tdi = tdi
end_tdi = tdi + int(column_span) - 1
# if the entire span is beyond print range => continue
# if only part then keeping that part
if start_tdi < min_col:
if end_tdi < min_col:
tdi += column_span
continue
else:
start_tdi = min_col
# stop if start column is beyond print range
if start_tdi > max_col:
stop_iterating_over_columns = True
break
# making sure last column won't go beyond the print range
if end_tdi > max_col:
end_tdi = max_col
width = sum(column_widths[start_tdi : end_tdi + 1])
self.svg.add(
self.svg.rect(
insert=(x, y),
size=(width, height),
style="fill: #ffffff; stroke-width:.125; stroke: #000000;",
)
)
)
p_tags = td.getElementsByType(P)
p_tags = td.getElementsByType(P)
if p_tags:
# figuring text position based on alignment
text_position = [0.0, 0.0]
if box_alignment.endswith("left"):
text_position[0] = x + self.padding
elif box_alignment.endswith("middle") or box_alignment == "center":
text_position[0] = x + width / 2
elif box_alignment.endswith("right"):
text_position[0] = x + width - self.padding
col_style = self.get_style(column_styles[tdi], styles)
final_cell_style = col_style | cell_style
box_alignment = self.get_box_alignment(final_cell_style)
wrap_text = final_cell_style.get("wrap-option", None) == "wrap"
if p_tags:
# figuring text position based on alignment
text_position = [0.0, 0.0]
if box_alignment.endswith("left"):
text_position[0] = x + self.padding
elif box_alignment.endswith("middle") or box_alignment == "center":
text_position[0] = x + width / 2
elif box_alignment.endswith("right"):
text_position[0] = x + width - self.padding
if box_alignment.startswith("top"):
text_position[1] = y + self.padding
elif box_alignment.startswith("middle") or box_alignment == "center":
text_position[1] = y + height / 2
elif box_alignment.startswith("bottom"):
text_position[1] = y + height - self.padding
if box_alignment.startswith("top"):
text_position[1] = y + self.padding
elif box_alignment.startswith("middle") or box_alignment == "center":
text_position[1] = y + height / 2
elif box_alignment.startswith("bottom"):
text_position[1] = y + height - self.padding
self.add_text(
p_tags, *text_position, box_alignment=box_alignment, wrap_text=wrap_text, cell_width=width
)
x += width
tdi += column_span
self.add_text(
p_tags,
*text_position,
box_alignment=box_alignment,
wrap_text=wrap_text,
cell_width=width,
)
x += width
tdi += column_span
tri += 1
y += height
y += height
total_width = sum(column_widths) + (self.margin * 2)
self.svg["width"] = "{}mm".format(total_width)
self.svg["height"] = "{}mm".format(y)
self.svg["viewBox"] = "0 0 {} {}".format(total_width, y)
self.svg.save(pretty=True)
def get_style(self, style_name, styles):
style = styles[style_name] if style_name else {}
return style
def get_box_alignment(self, style):
if style and "vertical-align" in style and style["vertical-align"] != "automatic":
vertical_align = style["vertical-align"]
else:
vertical_align = "bottom"
alignment_translation = {
"center": "middle",
"end": "right",
"start": "left",
}
if style and "text-align" in style and style["text-align"] != "automatic":
horizontal_align = style["text-align"]
horizontal_align = alignment_translation.get(horizontal_align, horizontal_align)
else:
horizontal_align = "left"
if vertical_align == "middle" and horizontal_align == "middle":
box_alignment = "center"
else:
box_alignment = f"{vertical_align}-{horizontal_align}"
return box_alignment
def add_text(self, p_tags, x, y, box_alignment="bottom-left", wrap_text=False, cell_width=100):
"""
Adds text to svg.