diff --git a/KiBOM/__init__.py b/KiBOM/__init__.py index ba0fc5e2..fb79574d 100644 --- a/KiBOM/__init__.py +++ b/KiBOM/__init__.py @@ -2,4 +2,6 @@ import columns import netlist_reader import units import component -import sort \ No newline at end of file +import sort +import preferences +import bom_writer \ No newline at end of file diff --git a/KiBOM/columns.py b/KiBOM/columns.py index 8cceefd3..d6d803f6 100644 --- a/KiBOM/columns.py +++ b/KiBOM/columns.py @@ -12,6 +12,14 @@ class ColumnList: #default columns for groups COL_GRP_QUANTITY = 'Quantity' + COL_GRP_TOTAL_COST = 'Total Cost' #Total cost based on quantity + COL_GRP_BUILD_QUANTITY = 'Build Quantity' + + #generated columns + _COLUMNS_GEN = [ + COL_GRP_QUANTITY, + COL_GRP_BUILD_QUANTITY, + ] #default columns _COLUMNS_DEFAULT = [ @@ -23,6 +31,7 @@ class ColumnList: COL_FP, COL_FP_LIB, COL_GRP_QUANTITY, + COL_GRP_BUILD_QUANTITY, COL_DATASHEET ] @@ -71,7 +80,7 @@ class ColumnList: def RemoveColumnByName(self, name): #first check if this is in an immutable colum - if name in self._COLUMNS_DEFAULT: + if name in self._COLUMNS_PROTECTED: return #column does not exist, return diff --git a/KiBOM/component.py b/KiBOM/component.py index d01f462a..2a7526b6 100644 --- a/KiBOM/component.py +++ b/KiBOM/component.py @@ -182,7 +182,7 @@ class ComponentGroup(): def getField(self, field): if not field in self.fields.keys(): return "" if not self.fields[field]: return "" - return str(self.fields[field]) + return u''.join((self.fields[field])) def getCount(self): return len(self.components) @@ -255,9 +255,12 @@ class ComponentGroup(): #update 'global' fields self.fields[ColumnList.COL_REFERENCE] = self.getRefs() + + q = self.getCount() self.fields[ColumnList.COL_GRP_QUANTITY] = "{n}{dnf}".format( - n = self.getCount(), + n=q, dnf = " (DNF)" if not self.isFitted() else "") + self.fields[ColumnList.COL_GRP_BUILD_QUANTITY] = str(q * self.prefs.buildNumber) if self.isFitted() else "0" self.fields[ColumnList.COL_VALUE] = self.components[0].getValue() self.fields[ColumnList.COL_PART] = self.components[0].getPartName() self.fields[ColumnList.COL_PART_LIB] = self.components[0].getLibName() @@ -303,7 +306,18 @@ class ComponentGroup(): #return a dict of the KiCAD data based on the supplied columns + #NOW WITH UNICODE SUPPORT! def getRow(self, columns): - row = [self.getField(key) for key in columns] - #print(row) + row = [] + for key in columns: + val = self.getField(key) + + if val is None: + val = "" + else: + val = u'' + val + val = val.encode('utf-8') + + row.append(val) + return row \ No newline at end of file diff --git a/KiBOM/csv_writer.py b/KiBOM/csv_writer.py index edad802b..5dfc581d 100644 --- a/KiBOM/csv_writer.py +++ b/KiBOM/csv_writer.py @@ -1,3 +1,5 @@ +# _*_ coding:latin-1 _*_ + import csv import columns from component import * @@ -25,6 +27,11 @@ def WriteCSV(filename, groups, net, headings, prefs): else: return False + nGroups = len(groups) + nTotal = sum([g.getCount() for g in groups]) + nFitted = sum([g.getCount() for g in groups if g.isFitted()]) + nBuild = nFitted * prefs.buildNumber + with open(filename, "w") as f: writer = csv.writer(f, delimiter=delimiter, lineterminator="\n") @@ -43,8 +50,10 @@ def WriteCSV(filename, groups, net, headings, prefs): row = group.getRow(headings) if prefs.numberRows: - row = [rowCount] + row + row = [str(rowCount)] + row + #deal with unicode characters + #row = [el.decode('latin-1') for el in row] writer.writerow(row) try: @@ -58,8 +67,12 @@ def WriteCSV(filename, groups, net, headings, prefs): for i in range(5): writer.writerow([]) - writer.writerow(["Component Count:",sum([g.getCount() for g in groups])]) - writer.writerow(["Component Groups:",len(groups)]) + writer.writerow(["Component Groups:",nGroups]) + writer.writerow(["Component Count:",nTotal]) + writer.writerow(["Fitted Components:", nFitted]) + if prefs.buildNumber > 0: + writer.writerow(["Number of PCBs:",prefs.buildNumber]) + writer.writerow(["Total components:", nBuild]) writer.writerow(["Schematic Version:",net.getVersion()]) writer.writerow(["Schematic Date:",net.getSheetDate()]) writer.writerow(["BoM Date:",net.getDate()]) diff --git a/KiBOM/html_writer.py b/KiBOM/html_writer.py index 367e7c34..4c2a561a 100644 --- a/KiBOM/html_writer.py +++ b/KiBOM/html_writer.py @@ -1,3 +1,4 @@ + import columns from component import * import os @@ -9,7 +10,7 @@ BG_USER = "#E6F9FF" #return a background color for a given column title def bgColor(col): #auto-generated columns - if col == ColumnList.COL_GRP_QUANTITY: + if col in ColumnList._COLUMNS_GEN: return BG_GEN #kicad protected columns elif col in ColumnList._COLUMNS_PROTECTED: @@ -19,7 +20,7 @@ def bgColor(col): return BG_USER def link(text): - text = str(text) + for t in ["http","https","ftp","www"]: if text.startswith(t): return '{t}'.format(t=text) @@ -41,12 +42,21 @@ def WriteHTML(filename, groups, net, headings, prefs): print("{fn} is not a valid html file".format(fn=filename)) return False + nGroups = len(groups) + nTotal = sum([g.getCount() for g in groups]) + nFitted = sum([g.getCount() for g in groups if g.isFitted()]) + nBuild = nFitted * prefs.buildNumber + with open(filename,"w") as html: #header html.write("\n") + html.write("\n") + html.write('\t\n') #UTF-8 encoding for unicode support + html.write("\n") html.write("\n") + #PCB info html.write("

KiBoM PCB Bill of Materials

\n") html.write('\n') @@ -55,8 +65,12 @@ def WriteHTML(filename, groups, net, headings, prefs): html.write("\n".format(version=net.getVersion())) html.write("\n".format(date=net.getSheetDate())) html.write("\n".format(version=net.getTool())) - html.write("\n".format(n = sum([g.getCount() for g in groups]))) - html.write("\n".format(n=len(groups))) + html.write("\n".format(n=nGroups)) + html.write("\n".format(n=nTotal)) + html.write("\n".format(n=nFitted)) + if prefs.buildNumber > 0: + html.write("\n".format(n=prefs.buildNumber)) + html.write("\n".format(n=prefs.buildNumber, t=nBuild)) html.write("
Schematic Version{version}
Schematic Date{date}
KiCad Version{version}
Total Components{n}
Component Groups{n}
Component Groups{n}
Component Count (per PCB){n}
Fitted Components (per PCB){n}
Number of PCBs{n}
Total Component Count
(for {n} PCBs)
{t}
\n") html.write("
\n") html.write("

Component Groups

\n") @@ -93,7 +107,7 @@ def WriteHTML(filename, groups, net, headings, prefs): html.write("\n") if prefs.numberRows: - html.write('\t{n}'.format(n=rowCount)) + html.write('\t{n}\n'.format(n=rowCount)) for n, r in enumerate(row): bg = bgColor(headings[n]) diff --git a/KiBOM/preferences.py b/KiBOM/preferences.py index d356e32e..9350a781 100644 --- a/KiBOM/preferences.py +++ b/KiBOM/preferences.py @@ -18,12 +18,15 @@ class BomPref: SECTION_EXCLUDE_PART = "EXCLUDE_COMPONENT_PART" SECTION_EXCLUDE_DESC = "EXCLUDE_COMPONENT_DESC" SECTION_ALIASES = "COMPONENT_ALIASES" + SECTION_CONFIGURATIONS = "PCB_CONFIGURATIONS" OPT_IGNORE_DNF = "ignore_dnf" OPT_NUMBER_ROWS = "number_rows" OPT_GROUP_CONN = "group_connectors" OPT_USE_REGEX = "test_regex" OPT_COMP_FP = "compare_footprints" + OPT_INC_PRICE = "calculate_price" + OPT_BUILD_NUMBER = 'build_quantity' #list of columns which we can use regex on COL_REG_EX = [ @@ -46,6 +49,9 @@ class BomPref: self.groupConnectors = True #group connectors and ignore component value self.useRegex = True #Test various columns with regex self.compareFootprints = True #test footprints when comparing components + self.buildNumber = 0 + self.verbose = False #by default, is not verbose + self.configurations = [] #list of various configurations #default reference exclusions self.excluded_references = [ @@ -71,7 +77,9 @@ class BomPref: ["c", "c_small", "cap", "capacitor"], ["r", "r_small", "res", "resistor"], ["sw", "switch"], - ["l", "l_small", "inductor"] + ["l", "l_small", "inductor"], + ["zener","zenersmall"], + ["d","diode","d_small"] ] #dictionary of possible regex expressions for ignoring component row(s) @@ -97,6 +105,13 @@ class BomPref: def columnToGroup(self, col): return "REGEXCLUDE_" + col.upper().replace(" ","_") + #check an option within the SECTION_GENERAL group + def checkOption(self, parser, opt, default=False): + if parser.has_option(self.SECTION_GENERAL, opt): + return parser.get(self.SECTION_GENERAL, opt).lower() in ["1","true","yes"] + else: + return default + #read KiBOM preferences from file def Read(self, file, verbose=False): file = os.path.abspath(file) @@ -111,16 +126,23 @@ class BomPref: #read general options if self.SECTION_GENERAL in cf.sections(): - if cf.has_option(self.SECTION_GENERAL, self.OPT_IGNORE_DNF): - self.ignoreDNF = cf.get(self.SECTION_GENERAL, self.OPT_IGNORE_DNF) == "1" - if cf.has_option(self.SECTION_GENERAL, self.OPT_NUMBER_ROWS): - self.numberRows = cf.get(self.SECTION_GENERAL, self.OPT_NUMBER_ROWS) == "1" - if cf.has_option(self.SECTION_GENERAL, self.OPT_GROUP_CONN): - self.groupConnectors = cf.get(self.SECTION_GENERAL, self.OPT_GROUP_CONN) == "1" - if cf.has_option(self.SECTION_GENERAL, self.OPT_USE_REGEX): - self.useRegex = cf.get(self.SECTION_GENERAL, self.OPT_USE_REGEX) == "1" - if cf.has_option(self.SECTION_GENERAL, self.OPT_COMP_FP): - self.compareFootprints = cf.get(self.SECTION_GENERAL, self.OPT_COMP_FP) == "1" + self.ignoreDNF = self.checkOption(cf, self.OPT_IGNORE_DNF, default=True) + self.numberRows = self.checkOption(cf, self.OPT_NUMBER_ROWS, default=True) + self.groupConnectors = self.checkOption(cf, self.OPT_GROUP_CONN, default=True) + self.useRegex = self.checkOption(cf, self.OPT_USE_REGEX, default=True) + self.compareFootprints = self.checkOption(cf, self.OPT_COMP_FP, default=True) + + if cf.has_option(self.SECTION_GENERAL, self.OPT_BUILD_NUMBER): + try: + self.buildNumber = int(cf.get(self.SECTION_GENERAL, self.OPT_BUILD_NUMBER)) + if self.buildNumber < 1: + self.buildNumber = 0 + except: + pass + + #read out configurations + if self.SECTION_CONFIGURATIONS in cf.sections(): + self.configurations = [i for i in cf.options(self.SECTION_CONFIGURATIONS)] #read out ignored-rows if self.SECTION_IGNORE in cf.sections(): @@ -136,7 +158,14 @@ class BomPref: if section in cf.sections(): self.regex[key] = [r for r in cf.options(section)] - + + #add an option to the SECTION_GENRAL group + def addOption(self, parser, opt, value, comment=None): + if comment: + if not comment.startswith(";"): + comment = "; " + comment + parser.set(self.SECTION_GENERAL, comment) + parser.set(self.SECTION_GENERAL, opt, "1" if value else "0") #write KiBOM preferences to file def Write(self, file): @@ -146,16 +175,12 @@ class BomPref: cf.add_section(self.SECTION_GENERAL) cf.set(self.SECTION_GENERAL, "; General BoM options here") - cf.set(self.SECTION_GENERAL, "; If '{opt}' option is set to 1, rows that are not to be fitted on the PCB will not be written to the BoM file".format(opt=self.OPT_IGNORE_DNF)) - cf.set(self.SECTION_GENERAL, self.OPT_IGNORE_DNF, 1 if self.ignoreDNF else 0) - cf.set(self.SECTION_GENERAL, "; If '{opt}' option is set to 1, each row in the BoM will be prepended with an incrementing row number".format(opt=self.OPT_NUMBER_ROWS)) - cf.set(self.SECTION_GENERAL, self.OPT_NUMBER_ROWS, 1 if self.numberRows else 0) - cf.set(self.SECTION_GENERAL, "; If '{opt}' option is set to 1, connectors with the same footprints will be grouped together, independent of the name of the connector".format(opt=self.OPT_GROUP_CONN)) - cf.set(self.SECTION_GENERAL, self.OPT_GROUP_CONN, 1 if self.groupConnectors else 0) - cf.set(self.SECTION_GENERAL, "; If '{opt}' option is set to 1, each component group will be tested against a number of regular-expressions (specified, per column, below). If any matches are found, the row is ignored in the output file".format(opt=self.OPT_USE_REGEX)) - cf.set(self.SECTION_GENERAL, self.OPT_USE_REGEX, 1 if self.useRegex else 0) - cf.set(self.SECTION_GENERAL, "; If '{opt}' option is set to 1, two components must have the same footprint to be grouped together. If '{opt}' is not set, then footprint comparison is ignored.".format(opt=self.OPT_COMP_FP)) - cf.set(self.SECTION_GENERAL, self.OPT_COMP_FP, 1 if self.compareFootprints else 0) + self.addOption(cf, self.OPT_IGNORE_DNF, self.ignoreDNF, comment="If '{opt}' option is set to 1, rows that are not to be fitted on the PCB will not be written to the BoM file".format(opt=self.OPT_IGNORE_DNF)) + self.addOption(cf, self.OPT_NUMBER_ROWS, self.numberRows, comment="If '{opt}' option is set to 1, each row in the BoM will be prepended with an incrementing row number".format(opt=self.OPT_NUMBER_ROWS)) + self.addOption(cf, self.OPT_GROUP_CONN, self.groupConnectors, comment="If '{opt}' option is set to 1, connectors with the same footprints will be grouped together, independent of the name of the connector".format(opt=self.OPT_GROUP_CONN)) + self.addOption(cf, self.OPT_USE_REGEX, self.useRegex, comment="If '{opt}' option is set to 1, each component group will be tested against a number of regular-expressions (specified, per column, below). If any matches are found, the row is ignored in the output file".format(opt=self.OPT_USE_REGEX)) + self.addOption(cf, self.OPT_COMP_FP, self.compareFootprints, comment="If '{opt}' option is set to 1, two components must have the same footprint to be grouped together. If '{opt}' is not set, then footprint comparison is ignored.".format(opt=self.OPT_COMP_FP)) + self.addOption(cf, self.OPT_BUILD_NUMBER, self.buildNumber, comment="; '{opt}' is the number of boards to build, which is used to calculate total parts quantity. If this is set to zero (0) then it is ignored".format(opt=self.OPT_BUILD_NUMBER)) cf.add_section(self.SECTION_IGNORE) cf.set(self.SECTION_IGNORE, "; Any column heading that appears here will be excluded from the Generated BoM") @@ -164,6 +189,11 @@ class BomPref: for i in self.ignore: cf.set(self.SECTION_IGNORE, i) + cf.add_section(self.SECTION_CONFIGURATIONS) + cf.set(self.SECTION_CONFIGURATION, '; List of PCB configuration parameters') + for i in self.configurations: + cf.set(self.SECTION_CONFIGURATIONS, i) + cf.add_section(self.SECTION_ALIASES) cf.set(self.SECTION_ALIASES, "; A series of values which are considered to be equivalent for the part name") cf.set(self.SECTION_ALIASES, "; Each line represents a space-separated list of equivalent component name values") diff --git a/KiBOM/units.py b/KiBOM/units.py index b8f63c45..8ad4f1c8 100644 --- a/KiBOM/units.py +++ b/KiBOM/units.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# _*_ coding:utf-8 _*_ """ diff --git a/KiBOM/xml_writer.py b/KiBOM/xml_writer.py index 58f1c1db..5ffe418c 100644 --- a/KiBOM/xml_writer.py +++ b/KiBOM/xml_writer.py @@ -18,19 +18,32 @@ def WriteXML(filename, groups, net, headings, prefs): if not filename.endswith(".xml"): return False - xml = ElementTree.Element('KiCAD_BOM', attrib = { - 'Schematic_Source' : net.getSource(), - 'Schematic_Version' : net.getVersion(), - 'Schematic_Date' : net.getSheetDate(), - 'BOM_Date' : net.getDate(), - 'KiCad_Version' : net.getTool(), - 'groups' : str(len(groups)), - 'components' : str(sum([group.getCount() for group in groups])) - }) + nGroups = len(groups) + nTotal = sum([g.getCount() for g in groups]) + nFitted = sum([g.getCount() for g in groups if g.isFitted()]) + nBuild = nFitted * prefs.buildNumber + + attrib = {} + + attrib['Schematic_Source'] = net.getSource() + attrib['Schematic_Version'] = net.getVersion() + attrib['Schematic_Date'] = net.getSheetDate() + attrib['BOM_Date'] = net.getDate() + attrib['KiCad_Version'] = net.getTool() + attrib['Component_Groups'] = str(nGroups) + attrib['Component_Count'] = str(nTotal) + attrib['Fitted_Components'] = str(nFitted) + + if prefs.buildNumber > 0: + attrib['Number_of_PCBs'] = str(prefs.buildNumber) + attrib['Total_Components'] = str(nBuild) + + xml = ElementTree.Element('KiCAD_BOM', attrib = attrib, encoding='utf-8') for group in groups: if prefs.ignoreDNF and not group.isFitted(): continue + row = group.getRow(headings) attrib = {} @@ -40,13 +53,12 @@ def WriteXML(filename, groups, net, headings, prefs): h = h.replace('"','') h = h.replace("'",'') - attrib[h] = row[i] + attrib[h] = str(row[i]).decode('ascii',errors='ignore') sub = ElementTree.SubElement(xml, "group", attrib=attrib) with open(filename,"w") as output: - out = ElementTree.tostring(xml, 'utf-8') - + out = ElementTree.tostring(xml, encoding="utf-8") output.write(minidom.parseString(out).toprettyxml(indent="\t")) return True diff --git a/KiBOM_CLI.py b/KiBOM_CLI.py index fcc0eb3f..9d2fabac 100644 --- a/KiBOM_CLI.py +++ b/KiBOM_CLI.py @@ -6,67 +6,91 @@ import sys import os import shutil +import argparse + here = os.path.abspath(os.path.dirname(sys.argv[0])) sys.path.append(here) +sys.path.append(os.path.join(here,"KiBOM")) from KiBOM.columns import ColumnList from KiBOM.netlist_reader import * from KiBOM.bom_writer import * from KiBOM.preferences import BomPref +verbose = False + def close(*arg): print(*arg) sys.exit(0) -if len(sys.argv) < 2: - close("No input file supplied") +def say(*arg): + if verbose: + print(*arg) -input_file = sys.argv[1].replace("\\",os.path.sep).replace("/",os.path.sep) +parser = argparse.ArgumentParser(description="KiBOM Bill of Materials generator script") -input_file = os.path.abspath(input_file) +parser.add_argument("netlist", help='xml netlist file. Use "%%I" when running from within KiCad') +parser.add_argument("--output", "-o", help='BoM output file name.\nUse "%%O" when running from within KiCad to use the default output name (csv file).\nFor e.g. HTML output, use "%%O.html"\r\nIf output file is unspecified, default output filename (csv format) will be used', default=None) + +parser.add_argument("-v", "--verbose", help="Enable verbose output", action='count') +parser.add_argument("--cfg", help="BoM config file (script will try to use 'bom.ini' if not specified here)") + +args = parser.parse_args() +input_file = args.netlist + if not input_file.endswith(".xml"): - close("Supplied file is not .xml") - -#work out an output file -ext = ".csv" - -if len(sys.argv) < 3: - #no output file supplied, assume .csv - output_file = input_file.replace(".xml",".csv") -else: - output_file = sys.argv[2].replace("\\",os.path.sep).replace("/",os.path.sep) + close("{i} is not a valid xml file".format(i=input_file)) - valid = False +verbose = args.verbose is not None - for e in [".xml",".csv",".txt",".tsv",".html"]: - if output_file.endswith(e): - valid = True - ext = e - break - if not valid: - output_file += ext - - output_file = os.path.abspath(output_file) - -print("Input File: " + input_file) -print("Output File: " + output_file) +input_file = os.path.abspath(input_file) -#preferences -ignore = [] -ignoreDNF = False -numberRows = True +say("Input:",input_file) -#Look for a '.bom' preference file -pref_file = os.path.join(os.path.dirname(input_file) , "bom.ini") +output_file = args.output + +if output_file is None: + output_file = input_file.replace(".xml","_bom.csv") + +#enfore a proper extension +valid = False +extensions = [".xml",".csv",".txt",".tsv",".html"] +for e in extensions: + if output_file.endswith(e): + valid = True + break +if not valid: + close("Extension must be one of",extensions) + +output_file = os.path.abspath(output_file) + +say("Output:",output_file) + +#look for a config file! +#bom.ini by default +ini = os.path.abspath(os.path.join(os.path.dirname(input_file), "bom.ini")) + +config_file = ini #default value +#user can overwrite with a specific config file +if args.cfg: + config_file = args.cfg #read preferences from file. If file does not exists, default preferences will be used pref = BomPref() -pref.Read(pref_file) + +#verbosity options +pref.verbose = verbose + +if os.path.exists(config_file): + pref.Read(config_file) + say("Config:",config_file) #write preference file back out (first run will generate a file with default preferences) -pref.Write(pref_file) +if not os.path.exists(ini): + pref.Write(ini) + say("Writing preferences file bom.ini") #individual components components = [] @@ -90,6 +114,10 @@ for g in groups: for f in g.fields: columns.AddColumn(f) +if pref.buildNumber < 1: + columns.RemoveColumn(ColumnList.COL_GRP_BUILD_QUANTITY) + say("Removing:",ColumnList.COL_GRP_BUILD_QUANTITY) + #Finally, write the BoM out to file result = WriteBoM(output_file, groups, net, columns.columns, pref) diff --git a/README.md b/README.md index 8696f0d6..2db7e51c 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,26 @@ KiBoM intelligently groups components based on multiple factors, and can generat BoM options are user-configurable in a per-project configuration file. +## Usage + +The *KiBOM_CLI* script can be run directly from KiCad or from the command line. For command help, run the script with the *-h* flag e.g. + + python KiBOM_CLI.py -h + +![alt tag](example/help.png?raw=True "Command Line") + +**netlist** The netlist must be provided to the script. When running from KiCAD use "%I" + +**--output / -o** If provided, this is the path to the BoM output. If not provided, the script will use the same name as the input file, with the suffix "_bom.csv" + +**--cfg** If provided, this is the BOM config file that will be used. If not provided, options will be loaded from "bom.ini" + +**--verbose / -v** Enable extra debugging information + +To run from KiCad, simply add the same command line in the *Bill of Materials* script window. e.g. to generate a HTML output: + +![alt tag](example/html_eg.png?raw=True "HTML Example") + ## Features ### Intelligent Component Grouping @@ -107,7 +127,9 @@ Hit the "Generate" button, and the output window should show that the BoM genera ### HTML Output The output HTML file is generated as follows: -![alt tag](example/html.png?raw=True "HTML") +![alt tag](example/html_eg.png?raw=True "HTML Gen") + +![alt tag](example/html.png?raw=True "HTML Output") Here the components are correctly grouped, with links to datasheets where appropriate, and fields color-coded. diff --git a/__init__.py b/__init__.py index 90b1d514..f234336b 100644 --- a/__init__.py +++ b/__init__.py @@ -1,3 +1,2 @@ - -import sys -sys.path.append(".") \ No newline at end of file +import KiBOM +from KiBOM import * \ No newline at end of file diff --git a/example/help.png b/example/help.png new file mode 100644 index 00000000..8dbb526f Binary files /dev/null and b/example/help.png differ diff --git a/example/html_eg.png b/example/html_eg.png new file mode 100644 index 00000000..15bd8e03 Binary files /dev/null and b/example/html_eg.png differ