KiBot/kibot/PcbDraw/present.py

199 lines
7.2 KiB
Python

# Author: Jan Mrázek
# License: MIT
from pathlib import Path
import sys
import os
import json
import glob
import shutil
import subprocess
import tempfile
import markdown2
import pybars
from datetime import datetime
from kikit import export
def resolveTemplatePath(path):
"""
Return a correct template path:
- if the path matches a directory relative to working directory and the
directory contains template.json, return that
- otherwise treat the path as a name into the default template library.
If none of those are template directories, raise exception.
"""
if os.path.exists(os.path.join(path, "template.json")):
return path
PKG_BASE = os.path.dirname(__file__)
TEMPLATES = os.path.join(PKG_BASE, "resources/present/templates")
if os.path.exists(os.path.join(TEMPLATES, path, "template.json")):
return os.path.join(TEMPLATES, path)
raise RuntimeError("'{}' is not a name or a path for existing template. Perhaps you miss template.json in the template?")
def readTemplate(path):
"""
Resolve template path, read the property file and return a subclass of
Template which can render the template.
"""
templateClasses = {
"HtmlTemplate": HtmlTemplate
}
path = resolveTemplatePath(path)
with open(os.path.join(path, "template.json")) as jsonFile:
parameters = json.load(jsonFile)
try:
tType = parameters["type"]
except KeyError:
raise RuntimeError("Invalid template.json - missing 'type'")
try:
return templateClasses[tType](path)
except KeyError:
raise RuntimeError("Unknown template type '{}'".format(tType))
def copyRelativeTo(sourceTree, sourceFile, outputDir):
sourceTree = os.path.abspath(sourceTree)
sourceFile = os.path.abspath(sourceFile)
relPath = os.path.relpath(sourceFile, sourceTree)
outputDir = os.path.join(outputDir, os.path.dirname(relPath))
Path(outputDir).mkdir(parents=True, exist_ok=True)
shutil.copy(sourceFile, outputDir)
class Template:
def __init__(self, directory):
self.directory = directory
with open(os.path.join(directory, "template.json")) as jsonFile:
self.parameters = json.load(jsonFile)
self.extraResources = []
self.boards = []
self.name = None
self.repository = None
def _copyResources(self, outputDirectory):
"""
Copy all resource files specified by template.json and further specified
by addResource to the output directory.
"""
for pattern in self.parameters["resources"]:
for path in glob.glob(os.path.join(self.directory, pattern), recursive=True):
copyRelativeTo(self.directory, path, outputDirectory)
for pattern in self.extraResources:
for path in glob.glob(pattern, recursive=True):
copyRelativeTo(".", path, outputDirectory)
def addResource(self, resource):
"""
Add a resources. Resource can be specified by a glob pattern. The files
are treated relative to current working directory.
"""
self.extraResources.append(resource)
def addBoard(self, name, comment, boardfile):
"""
Add board
"""
self.boards.append({
"name": name,
"comment": comment,
"source": boardfile
})
def _renderBoards(self, outputDirectory):
"""
Convert all boards to images and gerber exports. Enrich self.boards
with paths of generated files
"""
pcbdraw = shutil.which("pcbdraw")
if not pcbdraw:
raise RuntimeError("PcbDraw needs to be installed in order to render boards")
dirPrefix = "boards"
boardDir = os.path.join(outputDirectory, dirPrefix)
Path(boardDir).mkdir(parents=True, exist_ok=True)
for boardDesc in self.boards:
boardName = os.path.basename(boardDesc["source"]).replace(".kicad_pcb", "")
boardDesc["front"] = os.path.join(dirPrefix, boardName + "-front.png")
boardDesc["back"] = os.path.join(dirPrefix, boardName + "-back.png")
boardDesc["gerbers"] = os.path.join(dirPrefix, boardName + "-gerbers.zip")
boardDesc["file"] = os.path.join(dirPrefix, boardName + ".kicad_pcb")
subprocess.check_call([pcbdraw, "plot", "--vcuts=Cmts.User", "--silent", "--side=front", boardDesc["source"],
os.path.join(outputDirectory, boardDesc["front"])])
subprocess.check_call([pcbdraw, "plot", "--vcuts=Cmts.User", "--silent", "--side=back", boardDesc["source"],
os.path.join(outputDirectory, boardDesc["back"])])
tmp = tempfile.mkdtemp()
export.gerberImpl(boardDesc["source"], tmp)
shutil.make_archive(os.path.join(outputDirectory, boardDesc["gerbers"])[:-4], "zip", tmp)
shutil.rmtree(tmp)
shutil.copy(boardDesc["source"], os.path.join(outputDirectory, boardDesc["file"]))
def render(self, outputDirectory):
self._copyResources(outputDirectory)
self._renderBoards(outputDirectory)
self._renderPage(outputDirectory)
def gitRevision(self):
"""
Return a git revision string if in git repo, None otherwise
"""
proc = subprocess.run(["git", "rev-parse", "HEAD"], capture_output=True)
if proc.returncode:
return None
return proc.stdout.decode("utf-8")
def currentDateTime(self):
return datetime.now().strftime("%d. %m. %Y %H:%M")
def setName(self, name):
self.name = name
def setRepository(self, rep):
self.repository = rep
class HtmlTemplate(Template):
def __init__(self, path):
super().__init__(path)
def addDescriptionFile(self, description):
if not description.endswith(".md"):
raise RuntimeError("Only markdown descriptions are supported for now")
self.description = markdown2.markdown_path(description, extras=["fenced-code-blocks"])
def _renderPage(self, outputDirectory):
with open(os.path.join(self.directory, "index.html")) as templateFile:
template = pybars.Compiler().compile(templateFile.read())
gitRev = self.gitRevision()
content = template({
"repo": self.repository,
"gitRev": gitRev,
"gitRevShort": gitRev[:7] if gitRev else None,
"datetime": self.currentDateTime(),
"name": self.name,
"boards": self.boards,
"description": self.description
})
with open(os.path.join(outputDirectory, "index.html"),"w") as outFile:
outFile.write(content)
def boardpage(outdir, description, board, resource, template, repository, name):
try:
Path(outdir).mkdir(parents=True, exist_ok=True)
template = readTemplate(template)
template.addDescriptionFile(description)
template.setRepository(repository)
template.setName(name)
for r in resource:
template.addResource(r)
for name, comment, file in board:
template.addBoard(name, comment, file)
template.render(outdir)
except Exception as e:
sys.stderr.write("An error occurred: " + str(e) + "\n")
sys.exit(1)