# -*- coding: utf-8 -*- # Copyright (c) 2020 Salvador E. Tropea # Copyright (c) 2020 Instituto Nacional de TecnologĂ­a Industrial # Copyright (c) 2016-2020 Oliver Henry Walters (@SchrodingersGat) # License: MIT # Project: KiBot (formerly KiPlot) # Adapted from: https://github.com/SchrodingersGat/KiBoM """ HTML Writer: Generates a HTML BoM file. """ import os from base64 import b64encode from struct import unpack from .columnlist import ColumnList, BoMError from .kibot_logo import KIBOT_LOGO, KIBOT_LOGO_W, KIBOT_LOGO_H BG_GEN = "#E6FFEE" BG_KICAD = "#FFE6B3" BG_USER = "#E6F9FF" BG_EMPTY = "#FF8080" BG_GEN_L = "#F0FFF4" BG_KICAD_L = "#FFF0BD" BG_USER_L = "#F0FFFF" BG_EMPTY_L = "#FF8A8A" HEAD_COLOR_R = "#982020" HEAD_COLOR_G = "#009879" HEAD_COLOR_B = "#0e4e8e" STYLE_COMMON = (" .cell-title { vertical-align: bottom; }\n" " .cell-info { vertical-align: top; padding: 1em;}\n" " .cell-stats { vertical-align: top; padding: 1em;}\n" " .title { font-size:2.5em; font-weight: bold; }\n" " .h2 { font-size:1.5em; font-weight: bold; }\n" " .td-empty0 { text-align: center; background-color: "+BG_EMPTY+";}\n" " .td-gen0 { text-align: center; background-color: "+BG_GEN+";}\n" " .td-kicad0 { text-align: center; background-color: "+BG_KICAD+";}\n" " .td-user0 { text-align: center; background-color: "+BG_USER+";}\n" " .td-empty1 { text-align: center; background-color: "+BG_EMPTY_L+";}\n" " .td-gen1 { text-align: center; background-color: "+BG_GEN_L+";}\n" " .td-kicad1 { text-align: center; background-color: "+BG_KICAD_L+";}\n" " .td-user1 { text-align: center; background-color: "+BG_USER_L+";}\n" " .color-ref { margin: 25px 0; }\n" " .color-ref th { text-align: left }\n" " .color-ref td { padding: 5px 20px; }\n" " .head-table { margin-bottom: 2em; }\n") TABLE_MODERN = """ .content-table { border-collapse: collapse; margin-top: 5px; margin-bottom: 4em; font-size: 0.9em; font-family: sans-serif; min-width: 400px; border-radius: 5px 5px 0 0; overflow: hidden; box-shadow: 0 0 20px rgba(0, 0, 0, 0.15); } .content-table thead tr { background-color: @bg@; color: #ffffff; text-align: left; } .content-table th, .content-table td { padding: 12px 15px; } .content-table tbody tr { border-bottom: 1px solid #dddddd; } .content-table tbody tr:nth-of-type(even) { background-color: #f3f3f3; } .content-table tbody tr:last-of-type { border-bottom: 2px solid @bg@; } """ TABLE_CLASSIC = " .content-table, .content-table th, .content-table td { border: 1px solid black; }\n" def cell_class(col): """ Return a background color for a given column title """ col = col.lower() # Auto-generated columns if col in ColumnList.COLUMNS_GEN_L: return 'gen' # BG_GEN # KiCad protected columns elif col in ColumnList.COLUMNS_PROTECTED_L: return 'kicad' # BG_KICAD # Additional user columns return 'user' # BG_USER def link(text): for t in ["http", "https", "ftp", "www"]: if text.startswith(t): return '{t}'.format(t=text) return text def content_table(html, groups, headings, head_names, cfg, link_datasheet, link_digikey, col_colors, dnf=False): cl = '' # Table start html.write('\n') # Row titles: html.write(" \n") html.write(" \n") for i, h in enumerate(head_names): if col_colors: # Cell background color cl = ' class="th-'+cell_class(headings[i])+'"' html.write(' {}\n'.format(cl, h)) html.write(" \n") html.write(" \n") html.write(" \n") rc = 0 hl_empty = cfg.html.highlight_empty for i, group in enumerate(groups): if (cfg.ignore_dnf and not group.is_fitted()) != dnf: continue row = group.get_row(headings) if link_datasheet != -1: datasheet = group.get_field(ColumnList.COL_DATASHEET_L) html.write(" \n") for n, r in enumerate(row): # A link to Digi-Key? if link_digikey and headings[n] in link_digikey: r = '' + r + '' # Link this column to the datasheet? if link_datasheet == n and datasheet.startswith('http'): r = '' + r + '' if col_colors: # Empty cell? if hl_empty and (len(r) == 0 or r.strip() == "~"): cl = 'empty' else: cl = cell_class(headings[n]) cl = ' class="td-{}{}"'.format(cl, rc % 2) html.write(' {}\n'.format(cl, link(r))) html.write(" \n") rc += 1 html.write(" \n") html.write("
\n") return def embed_image(file): with open(file, 'rb') as f: s = f.read() if not (s[:8] == b'\x89PNG\r\n\x1a\n' and (s[12:16] == b'IHDR')): raise BoMError('Only PNG images are supported for the logo') w, h = unpack('>LL', s[16:24]) return int(w), int(h), 'data:image/png;base64,'+b64encode(s).decode('ascii') def write_html(filename, groups, headings, head_names, cfg): """ Write BoM out to a HTML file filename = path to output file (must be a .csv, .txt or .tsv file) groups = [list of ComponentGroup groups] headings = [list of headings to search for data in the BoM file] head_names = [list of headings to display in the BoM file] cfg = BoMOptions object with all the configuration """ link_datasheet = -1 if cfg.html.datasheet_as_link and cfg.html.datasheet_as_link in headings: link_datasheet = headings.index(cfg.html.datasheet_as_link) link_digikey = cfg.html.digikey_link col_colors = cfg.html.col_colors # Compute the CSS style_name = cfg.html.style if os.path.isfile(style_name): with open(style_name, 'rt') as f: style = f.read() else: # Common stuff style = STYLE_COMMON if style_name.startswith('modern-'): # content-table details if style_name.endswith('green'): head_color = HEAD_COLOR_G elif style_name.endswith('blue'): head_color = HEAD_COLOR_B else: head_color = HEAD_COLOR_R style += TABLE_MODERN.replace('@bg@', head_color) else: style += TABLE_CLASSIC with open(filename, "w") as html: # HTML Head html.write("\n") html.write("\n") html.write(' \n') # UTF-8 encoding for unicode support if cfg.html.title: html.write(' '+cfg.html.title+'\n') # CSS html.write("\n") html.write("\n") html.write("\n") # Page Header img = None if cfg.html.logo is not None: if cfg.html.logo: img_w, img_h, img = embed_image(cfg.html.logo) else: img = 'data:image/png;base64,'+KIBOT_LOGO img_w = KIBOT_LOGO_W img_h = KIBOT_LOGO_H if img or not cfg.html.hide_pcb_info or not cfg.html.hide_stats_info or cfg.html.title: html.write('\n') html.write('\n') html.write(' \n') html.write(' \n') html.write('\n') html.write('\n') html.write(' \n') html.write(' \n') html.write('\n') html.write('
\n') if img: html.write(' Logo\n') html.write(' \n') if cfg.html.title: html.write('
'+cfg.html.title+'
\n') html.write('
\n') if not cfg.html.hide_pcb_info: html.write(" Schematic: {}
\n".format(cfg.source)) html.write(" Variant: {}
\n".format(cfg.variant.name)) html.write(" Revision: {}
\n".format(cfg.revision)) html.write(" Date: {}
\n".format(cfg.date)) html.write(" KiCad Version: {}
\n".format(cfg.kicad_version)) html.write('
\n') if not cfg.html.hide_stats_info: html.write(" Component Groups: {}
\n".format(cfg.n_groups)) html.write(" Component Count: {} (per PCB)
\n\n".format(cfg.n_total)) html.write(" Fitted Components: {} (per PCB)
\n".format(cfg.n_fitted)) html.write(" Number of PCBs: {}
\n".format(cfg.number)) html.write(" Total Components: {t} (for {n} PCBs)
\n".format(n=cfg.number, t=cfg.n_build)) html.write('
\n') # Fitted groups html.write("

Component Groups

\n") content_table(html, groups, headings, head_names, cfg, link_datasheet, link_digikey, col_colors) # DNF component groups if cfg.html.generate_dnf and cfg.n_total != cfg.n_fitted: html.write("

Optional components (DNF=Do Not Fit)

\n") content_table(html, groups, headings, head_names, cfg, link_datasheet, link_digikey, col_colors, True) # Color reference if col_colors: html.write('\n') html.write('\n') html.write('\n') html.write('\n') html.write('\n') if cfg.html.highlight_empty: html.write('\n') html.write('
Color reference:
KiCad Fields (default)
Generated Fields
User Fields
Empty Fields
\n') html.write("") return True