Create separate modules for BOM and HTML functions

This commit is contained in:
Daniel Rojas 2020-11-01 12:30:31 +01:00
parent eebf932c8d
commit 96bd121403
6 changed files with 308 additions and 270 deletions

View File

@ -4,6 +4,7 @@
from typing import Optional, List, Tuple, Union from typing import Optional, List, Tuple, Union
from dataclasses import dataclass, field, InitVar from dataclasses import 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 import wv_colors from wireviz import wv_colors

View File

@ -1,20 +1,24 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from wireviz.DataClasses import Connector, Cable
from graphviz import Graph from graphviz import Graph
from wireviz import wv_colors, wv_helper, __version__, APP_NAME, APP_URL
from wireviz.wv_colors import get_color_hex
from wireviz.wv_helper import awg_equiv, mm2_equiv, tuplelist2tsv, \
nested_html_table, flatten2d, index_if_list, html_line_breaks, \
clean_whitespace, open_file_read, open_file_write, html_colorbar, \
html_image, html_caption, manufacturer_info_field, component_table_entry, remove_links
from collections import Counter from collections import Counter
from typing import List, Union from typing import List, Union
from pathlib import Path from pathlib import Path
from itertools import zip_longest from itertools import zip_longest
import re import re
from wireviz import wv_colors, __version__, APP_NAME, APP_URL
from wireviz.DataClasses import Connector, Cable
from wireviz.wv_colors import get_color_hex
from wireviz.wv_gv_html import nested_html_table, html_colorbar, html_image, \
html_caption, remove_links, html_line_breaks
from wireviz.wv_bom import manufacturer_info_field, component_table_entry, \
get_additional_component_table, bom_list, generate_bom
from wireviz.wv_html import generate_html_output
from wireviz.wv_helper import awg_equiv, mm2_equiv, tuplelist2tsv, flatten2d, \
open_file_read, open_file_write
class Harness: class Harness:
@ -119,7 +123,7 @@ class Harness:
'<!-- connector table -->' if connector.style != 'simple' else None, '<!-- connector table -->' if connector.style != 'simple' else None,
[html_image(connector.image)], [html_image(connector.image)],
[html_caption(connector.image)]] [html_caption(connector.image)]]
rows.extend(self.get_additional_component_table(connector)) rows.extend(get_additional_component_table(self, connector))
rows.append([html_line_breaks(connector.notes)]) rows.append([html_line_breaks(connector.notes)])
html.extend(nested_html_table(rows)) html.extend(nested_html_table(rows))
@ -205,7 +209,7 @@ class Harness:
[html_image(cable.image)], [html_image(cable.image)],
[html_caption(cable.image)]] [html_caption(cable.image)]]
rows.extend(self.get_additional_component_table(cable)) rows.extend(get_additional_component_table(self, cable))
rows.append([html_line_breaks(cable.notes)]) rows.append([html_line_breaks(cable.notes)])
html.extend(nested_html_table(rows)) html.extend(nested_html_table(rows))
@ -353,181 +357,13 @@ class Harness:
graph.render(filename=filename, view=view, cleanup=cleanup) graph.render(filename=filename, view=view, cleanup=cleanup)
graph.save(filename=f'{filename}.gv') graph.save(filename=f'{filename}.gv')
# bom output # bom output
bom_list = self.bom_list() bomlist = bom_list(self.bom())
with open_file_write(f'{filename}.bom.tsv') as file: with open_file_write(f'{filename}.bom.tsv') as file:
file.write(tuplelist2tsv(bom_list)) file.write(tuplelist2tsv(bomlist))
# HTML output # HTML output
with open_file_write(f'{filename}.html') as file: generate_html_output(filename, bomlist)
file.write('<!DOCTYPE html>\n')
file.write('<html lang="en"><head>\n')
file.write(' <meta charset="UTF-8">\n')
file.write(f' <meta name="generator" content="{APP_NAME} {__version__} - {APP_URL}">\n')
file.write(f' <title>{APP_NAME} Diagram and BOM</title>\n')
file.write('</head><body style="font-family:Arial">\n')
file.write('<h1>Diagram</h1>')
with open_file_read(f'{filename}.svg') as svg:
file.write(re.sub(
'^<[?]xml [^?>]*[?]>[^<]*<!DOCTYPE [^>]*>',
'<!-- XML and DOCTYPE declarations from SVG file removed -->',
svg.read(1024), 1))
for svgdata in svg:
file.write(svgdata)
file.write('<h1>Bill of Materials</h1>')
listy = flatten2d(bom_list)
file.write('<table style="border:1px solid #000000; font-size: 14pt; border-spacing: 0px">')
file.write('<tr>')
for item in listy[0]:
file.write(f'<th style="text-align:left; border:1px solid #000000; padding: 8px">{item}</th>')
file.write('</tr>')
for row in listy[1:]:
file.write('<tr>')
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="{align}border:1px solid #000000; padding: 4px">{item_str}</td>')
file.write('</tr>')
file.write('</table>')
file.write('</body></html>')
def get_additional_component_table(self, component: Union[Connector, Cable]) -> List[str]:
rows = []
if component.additional_components:
rows.append(["Additional components"])
for extra in component.additional_components:
qty = extra.qty * component.get_qty_multiplier(extra.qty_multiplier)
if self.mini_bom_mode:
id = self.get_bom_index(extra.description, extra.unit, extra.manufacturer, extra.mpn, extra.pn)
rows.append(component_table_entry(f'#{id} ({extra.type.rstrip()})', qty, extra.unit))
else:
rows.append(component_table_entry(extra.description, qty, extra.unit, extra.pn, extra.manufacturer, extra.mpn))
return(rows)
def get_additional_component_bom(self, component: Union[Connector, Cable]) -> List[dict]:
bom_entries = []
for part in component.additional_components:
qty = part.qty * component.get_qty_multiplier(part.qty_multiplier)
bom_entries.append({
'item': part.description,
'qty': qty,
'unit': part.unit,
'manufacturer': part.manufacturer,
'mpn': part.mpn,
'pn': part.pn,
'designators': component.name if component.show_name else None
})
return(bom_entries)
def bom(self): def bom(self):
# if the bom has previously been generated then return the generated bom if not self._bom:
if self._bom: self._bom = generate_bom(self)
return self._bom
bom_entries = []
# connectors
for connector in self.connectors.values():
if not connector.ignore_in_bom:
description = ('Connector'
+ (f', {connector.type}' if connector.type else '')
+ (f', {connector.subtype}' if connector.subtype else '')
+ (f', {connector.pincount} pins' if connector.show_pincount else '')
+ (f', {connector.color}' if connector.color else ''))
bom_entries.append({
'item': description, 'qty': 1, 'unit': None, 'designators': connector.name if connector.show_name else None,
'manufacturer': connector.manufacturer, 'mpn': connector.mpn, 'pn': connector.pn
})
# add connectors aditional components to bom
bom_entries.extend(self.get_additional_component_bom(connector))
# cables
# TODO: If category can have other non-empty values than 'bundle', maybe it should be part of item name?
for cable in self.cables.values():
if not cable.ignore_in_bom:
if cable.category != 'bundle':
# process cable as a single entity
description = ('Cable'
+ (f', {cable.type}' if cable.type else '')
+ (f', {cable.wirecount}')
+ (f' x {cable.gauge} {cable.gauge_unit}' if cable.gauge else ' wires')
+ (' shielded' if cable.shield else ''))
bom_entries.append({
'item': description, 'qty': cable.length, 'unit': 'm', 'designators': cable.name if cable.show_name else None,
'manufacturer': cable.manufacturer, 'mpn': cable.mpn, 'pn': cable.pn
})
else:
# add each wire from the bundle to the bom
for index, color in enumerate(cable.colors):
description = ('Wire'
+ (f', {cable.type}' if cable.type else '')
+ (f', {cable.gauge} {cable.gauge_unit}' if cable.gauge else '')
+ (f', {color}' if color else ''))
bom_entries.append({
'item': description, 'qty': cable.length, 'unit': 'm', 'designators': cable.name if cable.show_name else None,
'manufacturer': index_if_list(cable.manufacturer, index),
'mpn': index_if_list(cable.mpn, index), 'pn': index_if_list(cable.pn, index)
})
# add cable/bundles aditional components to bom
bom_entries.extend(self.get_additional_component_bom(cable))
for item in self.additional_bom_items:
bom_entries.append({
'item': item.get('description', ''), 'qty': item.get('qty', 1), 'unit': item.get('unit'), 'designators': item.get('designators'),
'manufacturer': item.get('manufacturer'), 'mpn': item.get('mpn'), 'pn': item.get('pn')
})
# remove line breaks if present and cleanup any resulting whitespace issues
bom_entries = [{k: clean_whitespace(v) for k, v in entry.items()} for entry in bom_entries]
# deduplicate bom
bom_types_group = lambda bt: (bt['item'], bt['unit'], bt['manufacturer'], bt['mpn'], bt['pn'])
for group in Counter([bom_types_group(v) for v in bom_entries]):
group_entries = [v for v in bom_entries if bom_types_group(v) == group]
designators = []
for group_entry in group_entries:
if group_entry.get('designators'):
if isinstance(group_entry['designators'], List):
designators.extend(group_entry['designators'])
else:
designators.append(group_entry['designators'])
designators = list(dict.fromkeys(designators)) # remove duplicates
designators.sort()
total_qty = sum(entry['qty'] for entry in group_entries)
self._bom.append({**group_entries[0], 'qty': round(total_qty, 3), 'designators': designators})
self._bom = sorted(self._bom, key=lambda k: k['item']) # sort list of dicts by their values (https://stackoverflow.com/a/73050)
# add an incrementing id to each bom item
self._bom = [{**entry, 'id': index} for index, entry in enumerate(self._bom, 1)]
return self._bom return self._bom
def get_bom_index(self, item, unit, manufacturer, mpn, pn):
# Remove linebreaks and clean whitespace of values in search
target = tuple(clean_whitespace(v) for v in (item, unit, manufacturer, mpn, pn))
for entry in self.bom():
if (entry['item'], entry['unit'], entry['manufacturer'], entry['mpn'], entry['pn']) == target:
return entry['id']
return None
def bom_list(self):
bom = self.bom()
keys = ['id', 'item', 'qty', 'unit', 'designators'] # these BOM columns will always be included
for fieldname in ['pn', 'manufacturer', 'mpn']: # these optional BOM columns will only be included if at least one BOM item actually uses them
if any(entry.get(fieldname) for entry in bom):
keys.append(fieldname)
bom_list = []
# list of staic bom header names, headers not specified here are generated by capitilising the internal name
bom_headings = {
"pn": "P/N",
"mpn": "MPN"
}
bom_list.append([(bom_headings[k] if k in bom_headings else k.capitalize()) for k in keys]) # create header row with keys
for item in bom:
item_list = [item.get(key, '') for key in keys] # fill missing values with blanks
item_list = [', '.join(subitem) if isinstance(subitem, List) else subitem for subitem in item_list] # convert any lists into comma separated strings
item_list = ['' if subitem is None else subitem for subitem in item_list] # if a field is missing for some (but not all) BOM items
bom_list.append(item_list)
return bom_list

178
src/wireviz/wv_bom.py Normal file
View File

@ -0,0 +1,178 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from typing import List, Union
from collections import Counter
from wireviz.DataClasses import Connector, Cable
from wireviz.wv_gv_html import html_line_breaks
from wireviz.wv_helper import clean_whitespace
def get_additional_component_table(harness, component: Union[Connector, Cable]) -> List[str]:
rows = []
if component.additional_components:
rows.append(["Additional components"])
for extra in component.additional_components:
qty = extra.qty * component.get_qty_multiplier(extra.qty_multiplier)
if harness.mini_bom_mode:
id = get_bom_index(harness, extra.description, extra.unit, extra.manufacturer, extra.mpn, extra.pn)
rows.append(component_table_entry(f'#{id} ({extra.type.rstrip()})', qty, extra.unit))
else:
rows.append(component_table_entry(extra.description, qty, extra.unit, extra.pn, extra.manufacturer, extra.mpn))
return(rows)
def get_additional_component_bom(component: Union[Connector, Cable]) -> List[dict]:
bom_entries = []
for part in component.additional_components:
qty = part.qty * component.get_qty_multiplier(part.qty_multiplier)
bom_entries.append({
'item': part.description,
'qty': qty,
'unit': part.unit,
'manufacturer': part.manufacturer,
'mpn': part.mpn,
'pn': part.pn,
'designators': component.name if component.show_name else None
})
return(bom_entries)
def generate_bom(harness):
from wireviz.Harness import Harness # Local import to avoid circular imports
bom_entries = []
# connectors
for connector in harness.connectors.values():
if not connector.ignore_in_bom:
description = ('Connector'
+ (f', {connector.type}' if connector.type else '')
+ (f', {connector.subtype}' if connector.subtype else '')
+ (f', {connector.pincount} pins' if connector.show_pincount else '')
+ (f', {connector.color}' if connector.color else ''))
bom_entries.append({
'item': description, 'qty': 1, 'unit': None, 'designators': connector.name if connector.show_name else None,
'manufacturer': connector.manufacturer, 'mpn': connector.mpn, 'pn': connector.pn
})
# add connectors aditional components to bom
bom_entries.extend(get_additional_component_bom(connector))
# cables
# TODO: If category can have other non-empty values than 'bundle', maybe it should be part of item name?
for cable in harness.cables.values():
if not cable.ignore_in_bom:
if cable.category != 'bundle':
# process cable as a single entity
description = ('Cable'
+ (f', {cable.type}' if cable.type else '')
+ (f', {cable.wirecount}')
+ (f' x {cable.gauge} {cable.gauge_unit}' if cable.gauge else ' wires')
+ (' shielded' if cable.shield else ''))
bom_entries.append({
'item': description, 'qty': cable.length, 'unit': 'm', 'designators': cable.name if cable.show_name else None,
'manufacturer': cable.manufacturer, 'mpn': cable.mpn, 'pn': cable.pn
})
else:
# add each wire from the bundle to the bom
for index, color in enumerate(cable.colors):
description = ('Wire'
+ (f', {cable.type}' if cable.type else '')
+ (f', {cable.gauge} {cable.gauge_unit}' if cable.gauge else '')
+ (f', {color}' if color else ''))
bom_entries.append({
'item': description, 'qty': cable.length, 'unit': 'm', 'designators': cable.name if cable.show_name else None,
'manufacturer': index_if_list(cable.manufacturer, index),
'mpn': index_if_list(cable.mpn, index), 'pn': index_if_list(cable.pn, index)
})
# add cable/bundles aditional components to bom
bom_entries.extend(get_additional_component_bom(cable))
for item in harness.additional_bom_items:
bom_entries.append({
'item': item.get('description', ''), 'qty': item.get('qty', 1), 'unit': item.get('unit'), 'designators': item.get('designators'),
'manufacturer': item.get('manufacturer'), 'mpn': item.get('mpn'), 'pn': item.get('pn')
})
# remove line breaks if present and cleanup any resulting whitespace issues
bom_entries = [{k: clean_whitespace(v) for k, v in entry.items()} for entry in bom_entries]
# deduplicate bom
bom = []
bom_types_group = lambda bt: (bt['item'], bt['unit'], bt['manufacturer'], bt['mpn'], bt['pn'])
for group in Counter([bom_types_group(v) for v in bom_entries]):
group_entries = [v for v in bom_entries if bom_types_group(v) == group]
designators = []
for group_entry in group_entries:
if group_entry.get('designators'):
if isinstance(group_entry['designators'], List):
designators.extend(group_entry['designators'])
else:
designators.append(group_entry['designators'])
designators = list(dict.fromkeys(designators)) # remove duplicates
designators.sort()
total_qty = sum(entry['qty'] for entry in group_entries)
bom.append({**group_entries[0], 'qty': round(total_qty, 3), 'designators': designators})
bom = sorted(bom, key=lambda k: k['item']) # sort list of dicts by their values (https://stackoverflow.com/a/73050)
# add an incrementing id to each bom item
bom = [{**entry, 'id': index} for index, entry in enumerate(bom, 1)]
return bom
def get_bom_index(harness, item, unit, manufacturer, mpn, pn):
# Remove linebreaks and clean whitespace of values in search
target = tuple(clean_whitespace(v) for v in (item, unit, manufacturer, mpn, pn))
for entry in harness.bom():
if (entry['item'], entry['unit'], entry['manufacturer'], entry['mpn'], entry['pn']) == target:
return entry['id']
return None
def bom_list(bom):
keys = ['id', 'item', 'qty', 'unit', 'designators'] # these BOM columns will always be included
for fieldname in ['pn', 'manufacturer', 'mpn']: # these optional BOM columns will only be included if at least one BOM item actually uses them
if any(entry.get(fieldname) for entry in bom):
keys.append(fieldname)
bom_list = []
# list of staic bom header names, headers not specified here are generated by capitilising the internal name
bom_headings = {
"pn": "P/N",
"mpn": "MPN"
}
bom_list.append([(bom_headings[k] if k in bom_headings else k.capitalize()) for k in keys]) # create header row with keys
for item in bom:
item_list = [item.get(key, '') for key in keys] # fill missing values with blanks
item_list = [', '.join(subitem) if isinstance(subitem, List) else subitem for subitem in item_list] # convert any lists into comma separated strings
item_list = ['' if subitem is None else subitem for subitem in item_list] # if a field is missing for some (but not all) BOM items
bom_list.append(item_list)
return bom_list
def component_table_entry(type, qty, unit=None, pn=None, manufacturer=None, mpn=None):
output = f'{qty}'
if unit:
output += f' {unit}'
output += f' x {type}'
# print an extra line with part and manufacturer information if provided
manufacturer_str = manufacturer_info_field(manufacturer, mpn)
if pn or manufacturer_str:
output += '<br/>'
if pn:
output += f'P/N: {pn}'
if manufacturer_str:
output += ', '
if manufacturer_str:
output += manufacturer_str
output = html_line_breaks(output)
# format the above output as left aligned text in a single visible cell
# indent is set to two to match the indent in the generated html table
return f'''<table border="0" cellspacing="0" cellpadding="3" cellborder="1"><tr>
<td align="left" balign="left">{output}</td>
</tr></table>'''
def manufacturer_info_field(manufacturer, mpn):
if manufacturer or mpn:
return f'{manufacturer if manufacturer else "MPN"}{": " + str(mpn) if mpn else ""}'
else:
return None
# Return the value indexed if it is a list, or simply the value otherwise.
def index_if_list(value, index):
return value[index] if isinstance(value, list) else value

66
src/wireviz/wv_gv_html.py Normal file
View File

@ -0,0 +1,66 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from typing import List, Union
import re
from wireviz.wv_colors import translate_color
from wireviz.wv_helper import remove_links
def nested_html_table(rows):
# input: list, each item may be scalar or list
# output: a parent table with one child table per parent item that is list, and one cell per parent item that is scalar
# purpose: create the appearance of one table, where cell widths are independent between rows
# attributes in any leading <tdX> inside a list are injected into to the preceeding <td> tag
html = []
html.append('<table border="0" cellspacing="0" cellpadding="0">')
for row in rows:
if isinstance(row, List):
if len(row) > 0 and any(row):
html.append(' <tr><td>')
html.append(' <table border="0" cellspacing="0" cellpadding="3" cellborder="1"><tr>')
for cell in row:
if cell is not None:
# Inject attributes to the preceeding <td> tag where needed
html.append(f' <td balign="left">{cell}</td>'.replace('><tdX', ''))
html.append(' </tr></table>')
html.append(' </td></tr>')
elif row is not None:
html.append(' <tr><td>')
html.append(f' {row}')
html.append(' </td></tr>')
html.append('</table>')
return html
def html_colorbar(color):
return f'<tdX bgcolor="{translate_color(color, "HEX")}" width="4">' if color else None
def html_image(image):
from wireviz.DataClasses import Image
if not image:
return None
# The leading attributes belong to the preceeding tag. See where used below.
html = f'{html_size_attr(image)}><img scale="{image.scale}" src="{image.src}"/>'
if image.fixedsize:
# Close the preceeding tag and enclose the image cell in a table without
# borders to avoid narrow borders when the fixed width < the node width.
html = f'''>
<table border="0" cellspacing="0" cellborder="0"><tr>
<td{html}</td>
</tr></table>
'''
return f'''<tdX{' sides="TLR"' if image.caption else ''}{html}'''
def html_caption(image):
from wireviz.DataClasses import Image
return f'<tdX sides="BLR">{html_line_breaks(image.caption)}' if image and image.caption else None
def html_size_attr(image):
from wireviz.DataClasses import Image
# Return Graphviz HTML attributes to specify minimum or fixed size of a TABLE or TD object
return ((f' width="{image.width}"' if image.width else '')
+ (f' height="{image.height}"' if image.height else '')
+ ( ' fixedsize="true"' if image.fixedsize else '')) if image else ''
def html_line_breaks(inp):
return remove_links(inp).replace('\n', '<br />') if isinstance(inp, str) else inp

View File

@ -1,7 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from wireviz import wv_colors
from typing import List from typing import List
import re import re
@ -32,58 +31,6 @@ def awg_equiv(mm2):
def mm2_equiv(awg): def mm2_equiv(awg):
return mm2_equiv_table.get(str(awg), 'Unknown') return mm2_equiv_table.get(str(awg), 'Unknown')
def nested_html_table(rows):
# input: list, each item may be scalar or list
# output: a parent table with one child table per parent item that is list, and one cell per parent item that is scalar
# purpose: create the appearance of one table, where cell widths are independent between rows
# attributes in any leading <tdX> inside a list are injected into to the preceeding <td> tag
html = []
html.append('<table border="0" cellspacing="0" cellpadding="0">')
for row in rows:
if isinstance(row, List):
if len(row) > 0 and any(row):
html.append(' <tr><td>')
html.append(' <table border="0" cellspacing="0" cellpadding="3" cellborder="1"><tr>')
for cell in row:
if cell is not None:
# Inject attributes to the preceeding <td> tag where needed
html.append(f' <td balign="left">{cell}</td>'.replace('><tdX', ''))
html.append(' </tr></table>')
html.append(' </td></tr>')
elif row is not None:
html.append(' <tr><td>')
html.append(f' {row}')
html.append(' </td></tr>')
html.append('</table>')
return html
def html_colorbar(color):
return f'<tdX bgcolor="{wv_colors.translate_color(color, "HEX")}" width="4">' if color else None
def html_image(image):
if not image:
return None
# The leading attributes belong to the preceeding tag. See where used below.
html = f'{html_size_attr(image)}><img scale="{image.scale}" src="{image.src}"/>'
if image.fixedsize:
# Close the preceeding tag and enclose the image cell in a table without
# borders to avoid narrow borders when the fixed width < the node width.
html = f'''>
<table border="0" cellspacing="0" cellborder="0"><tr>
<td{html}</td>
</tr></table>
'''
return f'''<tdX{' sides="TLR"' if image.caption else ''}{html}'''
def html_caption(image):
return f'<tdX sides="BLR">{html_line_breaks(image.caption)}' if image and image.caption else None
def html_size_attr(image):
# Return Graphviz HTML attributes to specify minimum or fixed size of a TABLE or TD object
return ((f' width="{image.width}"' if image.width else '')
+ (f' height="{image.height}"' if image.height else '')
+ ( ' fixedsize="true"' if image.fixedsize else '')) if image else ''
def expand(yaml_data): def expand(yaml_data):
# yaml_data can be: # yaml_data can be:
@ -140,19 +87,15 @@ def tuplelist2tsv(inp, header=None):
output = output + '\t'.join(str(remove_links(item)) for item in row) + '\n' output = output + '\t'.join(str(remove_links(item)) for item in row) + '\n'
return output return output
# Return the value indexed if it is a list, or simply the value otherwise.
def index_if_list(value, index):
return value[index] if isinstance(value, list) else value
def remove_links(inp): def remove_links(inp):
return re.sub(r'<[aA] [^>]*>([^<]*)</[aA]>', r'\1', inp) if isinstance(inp, str) else inp return re.sub(r'<[aA] [^>]*>([^<]*)</[aA]>', r'\1', inp) if isinstance(inp, str) else inp
def html_line_breaks(inp):
return remove_links(inp).replace('\n', '<br />') if isinstance(inp, str) else inp
def clean_whitespace(inp): def clean_whitespace(inp):
return ' '.join(inp.split()).replace(' ,', ',') if isinstance(inp, str) else inp return ' '.join(inp.split()).replace(' ,', ',') if isinstance(inp, str) else inp
def open_file_read(filename): def open_file_read(filename):
# TODO: Intelligently determine encoding # TODO: Intelligently determine encoding
return open(filename, 'r', encoding='UTF-8') return open(filename, 'r', encoding='UTF-8')
@ -163,7 +106,6 @@ def open_file_write(filename):
def open_file_append(filename): def open_file_append(filename):
return open(filename, 'a', encoding='UTF-8') return open(filename, 'a', encoding='UTF-8')
def aspect_ratio(image_src): def aspect_ratio(image_src):
try: try:
from PIL import Image from PIL import Image
@ -175,32 +117,3 @@ def aspect_ratio(image_src):
except Exception as error: except Exception as error:
print(f'aspect_ratio(): {type(error).__name__}: {error}') print(f'aspect_ratio(): {type(error).__name__}: {error}')
return 1 # Assume 1:1 when unable to read actual image size return 1 # Assume 1:1 when unable to read actual image size
def manufacturer_info_field(manufacturer, mpn):
if manufacturer or mpn:
return f'{manufacturer if manufacturer else "MPN"}{": " + str(mpn) if mpn else ""}'
else:
return None
def component_table_entry(type, qty, unit=None, pn=None, manufacturer=None, mpn=None):
output = f'{qty}'
if unit:
output += f' {unit}'
output += f' x {type}'
# print an extra line with part and manufacturer information if provided
manufacturer_str = manufacturer_info_field(manufacturer, mpn)
if pn or manufacturer_str:
output += '<br/>'
if pn:
output += f'P/N: {pn}'
if manufacturer_str:
output += ', '
if manufacturer_str:
output += manufacturer_str
output = html_line_breaks(output)
# format the above output as left aligned text in a single visible cell
# indent is set to two to match the indent in the generated html table
return f'''<table border="0" cellspacing="0" cellpadding="3" cellborder="1"><tr>
<td align="left" balign="left">{output}</td>
</tr></table>'''

44
src/wireviz/wv_html.py Normal file
View File

@ -0,0 +1,44 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pathlib import Path
import re
from wireviz import __version__, APP_NAME, APP_URL
from wireviz.wv_helper import flatten2d, open_file_read, open_file_write
def generate_html_output(filename: (str, Path), bom_list):
with open_file_write(f'{filename}.html') as file:
file.write('<!DOCTYPE html>\n')
file.write('<html lang="en"><head>\n')
file.write(' <meta charset="UTF-8">\n')
file.write(f' <meta name="generator" content="{APP_NAME} {__version__} - {APP_URL}">\n')
file.write(f' <title>{APP_NAME} Diagram and BOM</title>\n')
file.write('</head><body style="font-family:Arial">\n')
file.write('<h1>Diagram</h1>')
with open_file_read(f'{filename}.svg') as svg:
file.write(re.sub(
'^<[?]xml [^?>]*[?]>[^<]*<!DOCTYPE [^>]*>',
'<!-- XML and DOCTYPE declarations from SVG file removed -->',
svg.read(1024), 1))
for svgdata in svg:
file.write(svgdata)
file.write('<h1>Bill of Materials</h1>')
listy = flatten2d(bom_list)
file.write('<table style="border:1px solid #000000; font-size: 14pt; border-spacing: 0px">')
file.write('<tr>')
for item in listy[0]:
file.write(f'<th style="text-align:left; border:1px solid #000000; padding: 8px">{item}</th>')
file.write('</tr>')
for row in listy[1:]:
file.write('<tr>')
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="{align}border:1px solid #000000; padding: 4px">{item_str}</td>')
file.write('</tr>')
file.write('</table>')
file.write('</body></html>')