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 -*-
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 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
@ -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
MultilineHypertext = str # Hypertext possibly also including newlines to break lines in diagram output
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
ConnectorMultiplier = PlainText # = Literal['pincount', 'populated']
@ -32,26 +33,66 @@ class Metadata(dict):
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
class Options:
fontname: PlainText = 'arial'
bgcolor: Color = 'WH'
bgcolor_node: Optional[Color] = 'WH'
bgcolor_connector: Optional[Color] = None
bgcolor_cable: Optional[Color] = None
bgcolor_bundle: Optional[Color] = None
base: Look = field(default_factory=dict)
node: Look = field(default_factory=dict)
connector: Look = field(default_factory=dict)
cable: Look = field(default_factory=dict)
bundle: Look = field(default_factory=dict)
color_mode: ColorMode = 'SHORT'
mini_bom_mode: bool = True
def __post_init__(self):
if not self.bgcolor_node:
self.bgcolor_node = self.bgcolor
if not self.bgcolor_connector:
self.bgcolor_connector = self.bgcolor_node
if not self.bgcolor_cable:
self.bgcolor_cable = self.bgcolor_node
if not self.bgcolor_bundle:
self.bgcolor_bundle = self.bgcolor_cable
# Build initialization dicts with default values followed by dict entries from YAML input.
self.base = Look(**{**asdict(DEFAULT_LOOK), **self.base})
self.node = Look(**{**asdict(self.base), **self.node})
self.connector = Look(**{**asdict(self.node), **self.connector})
self.cable = Look(**{**asdict(self.node), **self.cable})
self.bundle = Look(**{**asdict(self.cable), **self.bundle})
@dataclass
@ -67,8 +108,8 @@ class Image:
src: str
scale: Optional[ImageScale] = None
# Attributes of the image cell <td> containing the image:
width: Optional[int] = None
height: Optional[int] = None
width: Optional[Points] = None
height: Optional[Points] = None
fixedsize: Optional[bool] = None
bgcolor: Optional[Color] = None
# 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.attr('graph', rankdir='LR',
ranksep='2',
bgcolor=wv_colors.translate_color(self.options.bgcolor, "HEX"),
nodesep='0.33',
fontname=self.options.fontname)
dot.attr('node',
shape='none',
width='0', height='0', margin='0', # Actual size of the node is entirely determined by the label.
**self.options.base.graph_args())
dot.attr('node', shape='none',
style='filled',
fillcolor=wv_colors.translate_color(self.options.bgcolor_node, "HEX"),
fontname=self.options.fontname)
width='0', height='0', margin='0', # Actual size of the node is entirely determined by the label.
**self.options.node.node_args())
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
for _, cable in self.cables.items():
@ -175,10 +174,11 @@ class Harness:
html = '\n'.join(html)
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:
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:
loop_side = 'l'
loop_dir = 'w'
@ -258,10 +258,11 @@ class Harness:
wirehtml.append(f' <td><!-- {i}_out --></td>')
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' <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">')
# 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
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>')
@ -301,10 +302,10 @@ class Harness:
if isinstance(cable.shield, str):
# shield is shown with specified color and black borders
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:
# 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(' <tr><td>&nbsp;</td></tr>')
@ -315,10 +316,10 @@ class Harness:
# connections
for connection in cable.connections:
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
# 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
from_connector = self.connectors[connection.from_name]
from_port = f':p{connection.from_port+1}r' if from_connector.style != 'simple' else ''
@ -352,11 +353,10 @@ class Harness:
to_string = ''
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 \
('filled', self.options.bgcolor_cable)
style, options = ('filled,dashed', self.options.bundle) if cable.category == 'bundle' else \
('filled', self.options.cable)
html = '\n'.join(html)
dot.node(cable.name, label=f'<\n{html}\n>', shape='box',
style=style, fillcolor=translate_color(bgcolor, "HEX"))
dot.node(cable.name, label=f'<\n{html}\n>', shape='box', style=style, **options.node_args())
def typecheck(name: str, value: Any, expect: type) -> None:
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(f' <meta name="generator" content="{APP_NAME} {__version__} - {APP_URL}">\n')
file.write(f' <title>{metadata["title"]}</title>\n')
file.write(f'</head><body style="font-family:{options.fontname};background-color:'
f'{wv_colors.translate_color(options.bgcolor, "HEX")}">\n')
file.write(f'</head><body style="{options.base.html_style()}">\n')
file.write(f'<h1>{metadata["title"]}</h1>\n')
description = metadata.get('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')
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')
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')
for row in listy[1:]:
file.write(' <tr>\n')
for i, item in enumerate(row):
item_str = item.replace('\u00b2', '&sup2;')
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('</table>\n')