KiBot/kibot/PcbDraw/present.py

189 lines
7.0 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
from . import pybars
from datetime import datetime
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):
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)