mirror of
https://github.com/AllSpiceIO/generate-orcad-schematic-pdf.git
synced 2025-11-09 16:47:58 +00:00
311 lines
11 KiB
Python
Executable File
311 lines
11 KiB
Python
Executable File
#! /usr/bin/env python3
|
|
|
|
import os
|
|
import time
|
|
import uuid
|
|
import pymupdf
|
|
import zipfile
|
|
import cairosvg
|
|
import subprocess
|
|
from allspice import AllSpice
|
|
from argparse import ArgumentParser
|
|
from xml.etree import ElementTree as ET
|
|
|
|
###############################################################################
|
|
GET_LAST_COMMIT_ON_TARGET_BRANCH_ENDPOINT = (
|
|
"""/repos/{owner}/{repo}/commits?sha={ref}&limit=1"""
|
|
)
|
|
|
|
|
|
###############################################################################
|
|
def get_dsn_files_from_previous_commit(commit_files):
|
|
design_doc_paths = []
|
|
for file in commit_files:
|
|
if file["filename"].lower().endswith(".dsn"):
|
|
if file["status"] != "removed":
|
|
design_doc_paths.append(file["filename"])
|
|
return design_doc_paths
|
|
|
|
|
|
###############################################################################
|
|
def push_changes_on_target_branch(ref, sha):
|
|
# Pull first before pushing
|
|
subprocess.check_output(["git pull"], shell=True, encoding="utf-8")
|
|
# Add files for commit
|
|
subprocess.check_output(["git add ."], shell=True, encoding="utf-8")
|
|
try:
|
|
# Add a commit message
|
|
subprocess.check_output(
|
|
['git commit -m "PDF generated from design update in ' + sha[0:10] + '"'],
|
|
shell=True,
|
|
encoding="utf-8",
|
|
)
|
|
# Push to remote
|
|
print("- Pushing extracted files to the " + "'" + ref + "' branch")
|
|
subprocess.run("git push", capture_output=True, text=True, shell=True)
|
|
except subprocess.CalledProcessError as e:
|
|
print(e.returncode)
|
|
print(e.output)
|
|
|
|
|
|
###############################################################################
|
|
def set_git_config(name, email):
|
|
subprocess.check_output(
|
|
["git config --global user.name " + '"' + name + '"'],
|
|
shell=True,
|
|
encoding="utf-8",
|
|
)
|
|
subprocess.check_output(
|
|
["git config --global user.email " + '"' + email + '"'],
|
|
shell=True,
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
###############################################################################
|
|
def get_previous_commit_on_target_branch(client, repository, branch, auth_token):
|
|
time.sleep(0.1)
|
|
commits_json = client.requests_get(
|
|
GET_LAST_COMMIT_ON_TARGET_BRANCH_ENDPOINT.format(
|
|
owner=repository.owner.username,
|
|
repo=repository.name,
|
|
ref=branch,
|
|
token=auth_token,
|
|
)
|
|
)
|
|
# Set author as the same person who made the previous commit
|
|
name = commits_json[0]["commit"]["committer"]["name"]
|
|
email = commits_json[0]["commit"]["committer"]["email"]
|
|
sha = commits_json[0]["sha"]
|
|
files = commits_json[0]["files"]
|
|
return name, email, sha, files
|
|
|
|
|
|
###############################################################################
|
|
def split_multipage_svg(svg_text: str) -> list[str]:
|
|
"""
|
|
Split a multi-page SVG into individual SVG files, one for each page.
|
|
Uses ElementTree for proper XML parsing.
|
|
|
|
Args:
|
|
svg_text (str): The content of the multi-page SVG file.
|
|
Returns:
|
|
list: List of the SVG contents for each page.
|
|
"""
|
|
|
|
ET.register_namespace("", "http://www.w3.org/2000/svg")
|
|
parser = ET.XMLParser(encoding="utf-8")
|
|
|
|
root = ET.fromstring(svg_text, parser=parser)
|
|
|
|
children = list(root)
|
|
|
|
# Each pair of <style> and <g> is one page.
|
|
page_pairs = []
|
|
|
|
for i in range(len(children) - 1):
|
|
current = children[i]
|
|
next_elem = children[i + 1]
|
|
|
|
if current.tag.endswith("}style") and next_elem.tag.endswith("}g"):
|
|
page_pairs.append((current, next_elem))
|
|
|
|
output_files = []
|
|
|
|
for i, (style_elem, g_elem) in enumerate(page_pairs):
|
|
new_svg = ET.Element("svg")
|
|
|
|
for attr, value in root.attrib.items():
|
|
new_svg.set(attr, value)
|
|
|
|
original_id = root.get("id", "")
|
|
new_svg.set("id", f"{original_id}" if original_id else f"page-{i + 1}")
|
|
|
|
width = g_elem.get("data-width")
|
|
height = g_elem.get("data-height")
|
|
view_box: str = g_elem.get("data-view-box")
|
|
|
|
if width:
|
|
new_svg.set("width", width)
|
|
if height:
|
|
new_svg.set("height", height)
|
|
|
|
new_svg.set("viewBox", view_box)
|
|
|
|
new_svg.append(style_elem)
|
|
del g_elem.attrib["transform"]
|
|
new_svg.append(g_elem)
|
|
|
|
svg_str = ET.tostring(new_svg, encoding="unicode")
|
|
|
|
output_files.append(svg_str)
|
|
|
|
return output_files
|
|
|
|
|
|
###############################################################################
|
|
def generate_orcad_pdfs(
|
|
design_doc_paths, repository, commit_to_branch, title_block_field, sha, name, branch
|
|
):
|
|
# Initialize an XML parser
|
|
parser = ET.XMLParser(encoding="utf-8")
|
|
# Declare commit link
|
|
commit_link = repository.url + "/commit/" + sha
|
|
# For all design files, generate PDFs
|
|
for design_doc in design_doc_paths:
|
|
print("- Processing " + design_doc)
|
|
# Get SVG of schematic
|
|
retries = 1
|
|
fetched = False
|
|
while not fetched and retries < 6:
|
|
try:
|
|
time.sleep(0.1)
|
|
svg = repository.get_generated_svg(design_doc, ref=branch)
|
|
fetched = True
|
|
except Exception:
|
|
print("- Generateing svg in progress. Trying again in ", end="")
|
|
for t in range(0, 5, -1):
|
|
print(str(t) + "...")
|
|
print(" " + str(5 - retries) + " retries left", end="", flush=True)
|
|
time.sleep(1)
|
|
retries += 1
|
|
# If a title block field is specified, try to populate sha link in title block
|
|
if title_block_field is not None:
|
|
# Initialize an ElementTree for the svg
|
|
tree = ET.fromstring(svg, parser=parser)
|
|
# Find all title block fields with the matching field name
|
|
title_block_field_matches = tree.findall(
|
|
'.//{http://www.w3.org/2000/svg}text[@data-id="'
|
|
+ title_block_field
|
|
+ '"]'
|
|
)
|
|
for field_match in title_block_field_matches:
|
|
for field_value in field_match.iter(
|
|
"{http://www.w3.org/2000/svg}tspan"
|
|
):
|
|
field_value.text += " | AllSpice Commit: "
|
|
linktag = ET.SubElement(
|
|
field_value, "{http://www.w3.org/2000/svg}a"
|
|
)
|
|
linktag.set("href", commit_link)
|
|
linktag.set("class", "color")
|
|
linktag.set("style", "fill:blue")
|
|
linktag.text = sha[0:10]
|
|
linktag.tail = " by " + name
|
|
xmlstr = ET.tostring(tree, encoding="utf8")
|
|
else:
|
|
xmlstr = svg
|
|
# Split the schematic pages into individual svgs
|
|
schematic_pages_svgs = split_multipage_svg(xmlstr)
|
|
# Create a directory to store the PDFs
|
|
dir_name = str(uuid.uuid4())
|
|
os.mkdir("/tmp/" + dir_name)
|
|
# Create a PDF writer to write merged PDFs
|
|
doc = pymupdf.open()
|
|
pdf_filename = os.path.splitext(os.path.basename(design_doc))[0] + ".pdf"
|
|
# Loop through pages and generate/merge PDFs for each sheet
|
|
for page_num, page in enumerate(schematic_pages_svgs):
|
|
# Create PDF from each SVG
|
|
pdf_filepath = "/tmp/" + dir_name + "/" + str(page_num) + "_" + pdf_filename
|
|
cairosvg.svg2pdf(bytestring=page, write_to=pdf_filepath)
|
|
print("- Generating PDF for " + pdf_filepath)
|
|
# Append each PDF sheet to the writer
|
|
doc.insert_pdf(pymupdf.open(pdf_filepath))
|
|
# Add sha links to the PDF
|
|
for pagenum, page in enumerate(doc): # iterate the document pages
|
|
matches = page.search_for(sha[0:10], quads=True)
|
|
for match in matches:
|
|
linkdict = {
|
|
"kind": 2,
|
|
"from": match.rect,
|
|
"page": pagenum,
|
|
"to": None,
|
|
"file": None,
|
|
"uri": repository.html_url + "/commit/" + sha,
|
|
"xref": pagenum,
|
|
}
|
|
page.insert_link(linkdict)
|
|
# Write merged PDF file
|
|
print("- Saving merged PDF to artifacts folder " + "/pdfs/" + pdf_filename)
|
|
doc.save("/pdfs/" + pdf_filename)
|
|
# Write file back to branch if specified
|
|
if commit_to_branch:
|
|
repo_pdfpath = os.path.splitext(design_doc)[0] + ".pdf"
|
|
print("- Saving PDF to repo path " + repo_pdfpath)
|
|
doc.save(repo_pdfpath)
|
|
|
|
|
|
###############################################################################
|
|
if __name__ == "__main__":
|
|
# Initialize argument parser
|
|
parser = ArgumentParser()
|
|
parser.add_argument("repository", help="Repository object for the target repo")
|
|
parser.add_argument("ref", help="Target branch")
|
|
parser.add_argument("sha", help="Commit hash that triggered this action")
|
|
parser.add_argument(
|
|
"--allspice_hub_url",
|
|
help="The URL of your AllSpice Hub instance. Defaults to https://hub.allspice.io.",
|
|
)
|
|
parser.add_argument(
|
|
"--title_block_field",
|
|
help="Title block field name for the commit hash to be populated",
|
|
)
|
|
parser.add_argument(
|
|
"--commit_to_branch",
|
|
help="Commit PDF back to branch? ",
|
|
)
|
|
args = parser.parse_args()
|
|
# Process args
|
|
commit_to_branch = True if args.commit_to_branch.lower() == "true" else False
|
|
# Get auth token and hub url
|
|
auth_token = os.environ.get("ALLSPICE_AUTH_TOKEN")
|
|
if auth_token is None:
|
|
print("Please set the environment variable ALLSPICE_AUTH_TOKEN")
|
|
exit(1)
|
|
if args.allspice_hub_url is None:
|
|
allspice = AllSpice(token_text=auth_token)
|
|
else:
|
|
allspice = AllSpice(
|
|
token_text=auth_token,
|
|
allspice_hub_url=args.allspice_hub_url,
|
|
)
|
|
# Get the repository
|
|
repo_owner, repo_name = args.repository.split("/")
|
|
repository = allspice.get_repository(repo_owner, repo_name)
|
|
# Get the previous commit
|
|
name, email, sha, files = get_previous_commit_on_target_branch(
|
|
allspice, repository, args.sha, auth_token
|
|
)
|
|
# Get filepaths to any new or modified design files in the previous commmit
|
|
design_doc_paths = get_dsn_files_from_previous_commit(files)
|
|
# Process extracting
|
|
if design_doc_paths:
|
|
# Generate a PDF for every schematic in the list
|
|
try:
|
|
os.makedirs("/pdfs")
|
|
except FileExistsError:
|
|
pass
|
|
# Generate PDF
|
|
generate_orcad_pdfs(
|
|
design_doc_paths,
|
|
repository,
|
|
commit_to_branch,
|
|
args.title_block_field,
|
|
sha,
|
|
name,
|
|
args.ref,
|
|
)
|
|
# Commit back to the repo if specified
|
|
if args.commit_to_branch:
|
|
# Set git config user and email
|
|
set_git_config(name, email)
|
|
# Push changes to the target branch
|
|
push_changes_on_target_branch(args.ref, sha)
|
|
# Zip all the generated PDFs
|
|
with zipfile.ZipFile("pdfs.zip", "w", zipfile.ZIP_DEFLATED) as zipper:
|
|
for root, dirs, files in os.walk("/pdfs"):
|
|
for file in files:
|
|
zipper.write(os.path.join(root, file))
|
|
else:
|
|
print("- No design files were added or modified in the previous commit.")
|