Move color and font options into new Look dataclass

This solves the basic part of #225 - supporting options to specify
- Foreground/border color and text color in addition to bgcolor
- Font size is not requested in #225, but included as well
This commit is contained in:
KV 2021-04-25 07:37:01 +02:00
parent 40aed9e23a
commit 251aab08ff
3 changed files with 83 additions and 43 deletions

View File

@ -1,11 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Dict, List, Optional, Tuple, Union from typing import Dict, List, Optional, Tuple, Union
from dataclasses import dataclass, field, InitVar from dataclasses import asdict, dataclass, field, InitVar
from pathlib import Path from pathlib import Path
from wireviz.wv_helper import int2tuple, aspect_ratio from wireviz.wv_helper import int2tuple, aspect_ratio
from wireviz.wv_colors import Color, Colors, ColorMode, ColorScheme, COLOR_CODES from wireviz.wv_colors import Color, Colors, ColorMode, ColorScheme, COLOR_CODES, translate_color
# Each type alias have their legal values described in comments - validation might be implemented in the future # Each type alias have their legal values described in comments - validation might be implemented in the future
@ -13,6 +13,7 @@ PlainText = str # Text not containing HTML tags nor newlines
Hypertext = str # Text possibly including HTML hyperlinks that are removed in all outputs except HTML output Hypertext = str # Text possibly including HTML hyperlinks that are removed in all outputs except HTML output
MultilineHypertext = str # Hypertext possibly also including newlines to break lines in diagram output MultilineHypertext = str # Hypertext possibly also including newlines to break lines in diagram output
Designator = PlainText # Case insensitive unique name of connector or cable Designator = PlainText # Case insensitive unique name of connector or cable
Points = float # Size in points = 1/72 inch
# Literal type aliases below are commented to avoid requiring python 3.8 # Literal type aliases below are commented to avoid requiring python 3.8
ConnectorMultiplier = PlainText # = Literal['pincount', 'populated'] ConnectorMultiplier = PlainText # = Literal['pincount', 'populated']
@ -32,26 +33,66 @@ class Metadata(dict):
pass pass
@dataclass
class Look:
"""Colors and font that defines how an element should look like."""
color: Optional[Color] = None
bgcolor: Optional[Color] = None
fontcolor: Optional[Color] = None
fontname: Optional[PlainText] = None
fontsize: Optional[Points] = None
def _2dict(self) -> dict:
"""Return dict of strings with color values translated to hex."""
return {
k:translate_color(v, "hex") if 'color' in k else str(v) for k,v in asdict(self).items()
}
def graph_args(self) -> dict:
"""Return dict with arguments to a dot graph."""
return {k:v for k,v in self._2dict().items() if k != 'color'}
def node_args(self) -> dict:
"""Return dict with arguments to a dot node with filled style."""
return {k.replace('bg', 'fill'):v for k,v in self._2dict().items()}
def html_style(self, color_prefix: Optional[str] = None, include_all: bool = True) -> str:
"""Return HTML style value containing all non-empty option values."""
translated = Look(**self._2dict())
return ' '.join(value for value in (
f'{color_prefix} {translated.color};' if self.color and color_prefix else None,
f'background-color: {translated.bgcolor};' if self.bgcolor and include_all else None,
f'color: {translated.fontcolor};' if self.fontcolor and include_all else None,
f'font-family: {self.fontname};' if self.fontname and include_all else None,
f'font-size: {self.fontsize}pt;' if self.fontsize and include_all else None,
) if value)
DEFAULT_LOOK = Look(
color = 'BK',
bgcolor = 'WH',
fontcolor = 'BK',
fontname = 'arial',
fontsize = 14,
)
@dataclass @dataclass
class Options: class Options:
fontname: PlainText = 'arial' base: Look = field(default_factory=dict)
bgcolor: Color = 'WH' node: Look = field(default_factory=dict)
bgcolor_node: Optional[Color] = 'WH' connector: Look = field(default_factory=dict)
bgcolor_connector: Optional[Color] = None cable: Look = field(default_factory=dict)
bgcolor_cable: Optional[Color] = None bundle: Look = field(default_factory=dict)
bgcolor_bundle: Optional[Color] = None
color_mode: ColorMode = 'SHORT' color_mode: ColorMode = 'SHORT'
mini_bom_mode: bool = True mini_bom_mode: bool = True
def __post_init__(self): def __post_init__(self):
if not self.bgcolor_node: # Build initialization dicts with default values followed by dict entries from YAML input.
self.bgcolor_node = self.bgcolor self.base = Look(**{**asdict(DEFAULT_LOOK), **self.base})
if not self.bgcolor_connector: self.node = Look(**{**asdict(self.base), **self.node})
self.bgcolor_connector = self.bgcolor_node self.connector = Look(**{**asdict(self.node), **self.connector})
if not self.bgcolor_cable: self.cable = Look(**{**asdict(self.node), **self.cable})
self.bgcolor_cable = self.bgcolor_node self.bundle = Look(**{**asdict(self.cable), **self.bundle})
if not self.bgcolor_bundle:
self.bgcolor_bundle = self.bgcolor_cable
@dataclass @dataclass
@ -67,8 +108,8 @@ class Image:
src: str src: str
scale: Optional[ImageScale] = None scale: Optional[ImageScale] = None
# Attributes of the image cell <td> containing the image: # Attributes of the image cell <td> containing the image:
width: Optional[int] = None width: Optional[Points] = None
height: Optional[int] = None height: Optional[Points] = None
fixedsize: Optional[bool] = None fixedsize: Optional[bool] = None
bgcolor: Optional[Color] = None bgcolor: Optional[Color] = None
# Contents of the text cell <td> just below the image cell: # Contents of the text cell <td> just below the image cell:

View File

@ -97,17 +97,16 @@ class Harness:
dot.body.append(f'// {APP_URL}') dot.body.append(f'// {APP_URL}')
dot.attr('graph', rankdir='LR', dot.attr('graph', rankdir='LR',
ranksep='2', ranksep='2',
bgcolor=wv_colors.translate_color(self.options.bgcolor, "HEX"),
nodesep='0.33', nodesep='0.33',
fontname=self.options.fontname) **self.options.base.graph_args())
dot.attr('node', dot.attr('node', shape='none',
shape='none',
width='0', height='0', margin='0', # Actual size of the node is entirely determined by the label.
style='filled', style='filled',
fillcolor=wv_colors.translate_color(self.options.bgcolor_node, "HEX"), width='0', height='0', margin='0', # Actual size of the node is entirely determined by the label.
fontname=self.options.fontname) **self.options.node.node_args())
dot.attr('edge', style='bold', dot.attr('edge', style='bold',
fontname=self.options.fontname) **self.options.base.node_args())
wire_border_hex = wv_colors.get_color_hex(self.options.base.color)[0]
# prepare ports on connectors depending on which side they will connect # prepare ports on connectors depending on which side they will connect
for _, cable in self.cables.items(): for _, cable in self.cables.items():
@ -175,10 +174,11 @@ class Harness:
html = '\n'.join(html) html = '\n'.join(html)
dot.node(connector.name, label=f'<\n{html}\n>', shape='box', style='filled', dot.node(connector.name, label=f'<\n{html}\n>', shape='box', style='filled',
fillcolor=translate_color(self.options.bgcolor_connector, "HEX")) **self.options.connector.node_args())
if len(connector.loops) > 0: if len(connector.loops) > 0:
dot.attr('edge', color='#000000:#ffffff:#000000') # TODO: Use self.options.wire.color and self.options.wire.bgcolor here?
dot.attr('edge', color=f'{wire_border_hex}:#ffffff:{wire_border_hex}')
if connector.ports_left: if connector.ports_left:
loop_side = 'l' loop_side = 'l'
loop_dir = 'w' loop_dir = 'w'
@ -258,10 +258,11 @@ class Harness:
wirehtml.append(f' <td><!-- {i}_out --></td>') wirehtml.append(f' <td><!-- {i}_out --></td>')
wirehtml.append(' </tr>') wirehtml.append(' </tr>')
bgcolors = ['#000000'] + get_color_hex(connection_color, pad=pad) + ['#000000'] bgcolors = [wire_border_hex] + get_color_hex(connection_color, pad=pad) + [wire_border_hex]
wirehtml.append(f' <tr>') wirehtml.append(f' <tr>')
wirehtml.append(f' <td colspan="3" border="0" cellspacing="0" cellpadding="0" port="w{i}" height="{(2 * len(bgcolors))}">') wirehtml.append(f' <td colspan="3" border="0" cellspacing="0" cellpadding="0" port="w{i}" height="{(2 * len(bgcolors))}">')
wirehtml.append(' <table cellspacing="0" cellborder="0" border="0">') wirehtml.append(' <table cellspacing="0" cellborder="0" border="0">')
# TODO: Reverse curved wire colors instead? Test also with empty wire colors! wv_colors.default_color ??
for j, bgcolor in enumerate(bgcolors[::-1]): # Reverse to match the curved wires when more than 2 colors for j, bgcolor in enumerate(bgcolors[::-1]): # Reverse to match the curved wires when more than 2 colors
wirehtml.append(f' <tr><td colspan="3" cellpadding="0" height="2" bgcolor="{bgcolor if bgcolor != "" else wv_colors.default_color}" border="0"></td></tr>') wirehtml.append(f' <tr><td colspan="3" cellpadding="0" height="2" bgcolor="{bgcolor if bgcolor != "" else wv_colors.default_color}" border="0"></td></tr>')
wirehtml.append(' </table>') wirehtml.append(' </table>')
@ -301,10 +302,10 @@ class Harness:
if isinstance(cable.shield, str): if isinstance(cable.shield, str):
# shield is shown with specified color and black borders # shield is shown with specified color and black borders
shield_color_hex = wv_colors.get_color_hex(cable.shield)[0] shield_color_hex = wv_colors.get_color_hex(cable.shield)[0]
attributes = f'height="6" bgcolor="{shield_color_hex}" border="2" sides="tb"' attributes = f'height="6" bgcolor="{shield_color_hex}" color="{wire_border_hex}" border="2" sides="tb"'
else: else:
# shield is shown as a thin black wire # shield is shown as a thin black wire
attributes = f'height="2" bgcolor="#000000" border="0"' attributes = f'height="2" bgcolor="{wire_border_hex}" border="0"'
wirehtml.append(f' <tr><td colspan="3" cellpadding="0" {attributes} port="ws"></td></tr>') wirehtml.append(f' <tr><td colspan="3" cellpadding="0" {attributes} port="ws"></td></tr>')
wirehtml.append(' <tr><td>&nbsp;</td></tr>') wirehtml.append(' <tr><td>&nbsp;</td></tr>')
@ -315,10 +316,10 @@ class Harness:
# connections # connections
for connection in cable.connections: for connection in cable.connections:
if isinstance(connection.via_port, int): # check if it's an actual wire and not a shield if isinstance(connection.via_port, int): # check if it's an actual wire and not a shield
dot.attr('edge', color=':'.join(['#000000'] + wv_colors.get_color_hex(cable.colors[connection.via_port - 1], pad=pad) + ['#000000'])) dot.attr('edge', color=':'.join([wire_border_hex] + wv_colors.get_color_hex(cable.colors[connection.via_port - 1], pad=pad) + [wire_border_hex]))
else: # it's a shield connection else: # it's a shield connection
# shield is shown with specified color and black borders, or as a thin black wire otherwise # shield is shown with specified color and black borders, or as a thin black wire otherwise
dot.attr('edge', color=':'.join(['#000000', shield_color_hex, '#000000']) if isinstance(cable.shield, str) else '#000000') dot.attr('edge', color=':'.join([wire_border_hex, shield_color_hex, wire_border_hex]) if isinstance(cable.shield, str) else wire_border_hex)
if connection.from_port is not None: # connect to left if connection.from_port is not None: # connect to left
from_connector = self.connectors[connection.from_name] from_connector = self.connectors[connection.from_name]
from_port = f':p{connection.from_port+1}r' if from_connector.style != 'simple' else '' from_port = f':p{connection.from_port+1}r' if from_connector.style != 'simple' else ''
@ -352,11 +353,10 @@ class Harness:
to_string = '' to_string = ''
html = [row.replace(f'<!-- {connection.via_port}_out -->', to_string) for row in html] html = [row.replace(f'<!-- {connection.via_port}_out -->', to_string) for row in html]
style, bgcolor = ('filled,dashed', self.options.bgcolor_bundle) if cable.category == 'bundle' else \ style, options = ('filled,dashed', self.options.bundle) if cable.category == 'bundle' else \
('filled', self.options.bgcolor_cable) ('filled', self.options.cable)
html = '\n'.join(html) html = '\n'.join(html)
dot.node(cable.name, label=f'<\n{html}\n>', shape='box', dot.node(cable.name, label=f'<\n{html}\n>', shape='box', style=style, **options.node_args())
style=style, fillcolor=translate_color(bgcolor, "HEX"))
def typecheck(name: str, value: Any, expect: type) -> None: def typecheck(name: str, value: Any, expect: type) -> None:
if not isinstance(value, expect): if not isinstance(value, expect):

View File

@ -15,9 +15,7 @@ def generate_html_output(filename: Union[str, Path], bom_list: List[List[str]],
file.write(' <meta charset="UTF-8">\n') file.write(' <meta charset="UTF-8">\n')
file.write(f' <meta name="generator" content="{APP_NAME} {__version__} - {APP_URL}">\n') file.write(f' <meta name="generator" content="{APP_NAME} {__version__} - {APP_URL}">\n')
file.write(f' <title>{metadata["title"]}</title>\n') file.write(f' <title>{metadata["title"]}</title>\n')
file.write(f'</head><body style="font-family:{options.fontname};background-color:' file.write(f'</head><body style="{options.base.html_style()}">\n')
f'{wv_colors.translate_color(options.bgcolor, "HEX")}">\n')
file.write(f'<h1>{metadata["title"]}</h1>\n') file.write(f'<h1>{metadata["title"]}</h1>\n')
description = metadata.get('description') description = metadata.get('description')
if description: if description:
@ -33,17 +31,18 @@ def generate_html_output(filename: Union[str, Path], bom_list: List[List[str]],
file.write('<h2>Bill of Materials</h2>\n') file.write('<h2>Bill of Materials</h2>\n')
listy = flatten2d(bom_list) listy = flatten2d(bom_list)
file.write('<table style="border:1px solid #000000; font-size: 14pt; border-spacing: 0px">\n') border = options.base.html_style(color_prefix="border: 1px solid", include_all=False)
file.write(f'<table style="{border} border-spacing: 0px;">\n')
file.write(' <tr>\n') file.write(' <tr>\n')
for item in listy[0]: for item in listy[0]:
file.write(f' <th style="text-align:left; border:1px solid #000000; padding: 8px">{item}</th>\n') file.write(f' <th style="{border} padding: 8px; text-align:left;">{item}</th>\n')
file.write(' </tr>\n') file.write(' </tr>\n')
for row in listy[1:]: for row in listy[1:]:
file.write(' <tr>\n') file.write(' <tr>\n')
for i, item in enumerate(row): for i, item in enumerate(row):
item_str = item.replace('\u00b2', '&sup2;') item_str = item.replace('\u00b2', '&sup2;')
align = '; text-align:right' if listy[0][i] == 'Qty' else '' align = '; text-align:right' if listy[0][i] == 'Qty' else ''
file.write(f' <td style="border:1px solid #000000; padding: 4px{align}">{item_str}</td>\n') file.write(f' <td style="{border} padding: 4px{align};">{item_str}</td>\n')
file.write(' </tr>\n') file.write(' </tr>\n')
file.write('</table>\n') file.write('</table>\n')