Python Build Script for VBA add-in file
I wrote a python script that will be used as a "build script" for a macro-enabled PowerPoint file that I maintain.
The script creates a new, blank PowerPoint presentation, imports all VBA modules, saves the file, and converts it to a ZIP archive to insert the RibbonUI configurations (ribbon_xml.xml and mylogo.jpg file).
This all works more or less as expected - until I try to use the output file (manually rename from .zip to .pptm and open it in PowerPoint).
Error . The code crashes, but the output archive (copy.zip) does not open open when converted to PPTM file.
I get a warning that there is something wrong with the configuration and PowerPoint will try to repair the file.
PowerPoint, true to its nature, of course does not indicate that the problem is, only that it found "unreadable content" and that such content was "deleted" ... The only thing I can see after comparing some of the files that I created manually is that the XML attribute for the CustomUI seems to use some sort of GUID as part of the id attribute
Current workaround: The build_ribbon function can be done manually using the CustomUI Editor tool and will take about 3 minutes to build the PPTM output reliably.
So this is not a "Python" question, as it is a question about the implementation of the XML / XML Customize feed XML interface.
Complete code:
import win32com.client
import os
import zipfile
import uuid
#### PARAMETERS
vba_source_control_path = r"C:\Repos\MyAddIn\VBA\ChartBuilder_PPT\Modules"
output_path = r"C:\debug\output.pptm"
ribbon_xml_path = r"C:\Repos\MyAddIn\Ribbon XML\ribbon_xml.xml"
ribbon_logo_path = r"C:\Repos\MyAddIn\Ribbon XML\mylogo.jpg"
def build_addin(pres, path):
"""
This procedure does the following:
1. adds all of the VBComponents to the working PPTM file
The .PPTM file is used for local development & debugging and
is only usually packaged as a PPAM for Testing and Distribution
"""
for fn in [fn for fn in os.listdir(path) if not(fn.endswith(".frx"))]:
pres.VBProject.VBComponents.Import(path + "\\" + fn)
# Clean up old files, if any
if os.path.isfile(output_path):
os.remove(output_path)
if os.path.isfile(output_path.replace(".pptm", ".zip")):
os.remove(output_path.replace(".pptm", ".zip"))
# Save the new file with VBProject components
pres.SaveAs(output_path)
pres.Close()
def build_ribbon_zip():
"""
build_ribbon_zip handles manipulation of the .ZIP contents and places the
necessary components within the PPTM ZIP archive structure
2. converts the PPTM to a .ZIP
3. Adds the CustomUI XML and logo.jpg to the .ZIP directory
4. converts the .ZIP to a PPTM
"""
id = '<Relationship Id='
schema = 'http://schemas.openxmlformats.org/officeDocument/2006/'
_path = output_path.replace(".pptm", ".zip")
copy_path = r"C:\debug\copy.zip"
# Convert to ZIP archive
os.rename(output_path, _path)
zip = zipfile.ZipFile(_path, 'a')
copy = zipfile.ZipFile(copy_path, 'w')
guid = str(uuid.uuid4()).replace('-', '')[:16]
for itm in [itm for itm in zip.infolist() if itm.filename != r'_rels/.rels']:
buffer = zip.read(itm.filename)
copy.writestr(itm, buffer)
# Append the Logo file to the .zip and create the archive
copy.write(ribbon_logo_path, r'\CustomUI\images\jdplogo.jpg')
# append the CustomUI xml part to the .zip and create the archive
copy.write(ribbon_xml_path, r'\CustomUI\customUI14.xml')
# append the .rels file to CustomUI\_rels
rels_xml = r'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
rels_xml += r'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
rels_xml += r'<Relationship Id="jdplogo" Type="'+schema+'relationships/image" Target="images/jdplogo.jpg"/>'
rels_xml += r'</Relationships>'
copy.writestr(r'CustomUI\_rels\customUI14.xml.rels', rels_xml.encode('utf-8'))
# get the existing _rels/.rels XML content and append the UI:
rels_xml = zip.read(r'_rels/.rels').rstrip()[:-16]
rels_xml += id + r'"R'+guid+'" Type="http://schemas.microsoft.com/office/2007/relationships/ui/extensibility"'
rels_xml += r'Target="customUI/customUI14.xml"/></Relationships>'
rels_xml = rels_xml.replace(os.linesep, '')
# this file-like object is read-only, and the writestr method will create another .rels file...
copy.writestr(r'_rels/.rels', rels_xml.encode('utf-8'))
zip.close()
copy.close()
if __name__ == "__main__":
"""
Procedure to create a new PowerPoint Presentation and insert the Code Modules from source control
"""
ppApp = win32com.client.Dispatch("PowerPoint.Application")
pres = ppApp.Presentations.Add(False)
pres.Slides.AddSlide(1, pres.SlideMaster.CustomLayouts(1))
build_addin(pres, vba_source_control_path)
ppApp.Quit()
build_ribbon_zip()
source to share
The release is missing some links that make PowerPoint go crazy. Solved it like this:
def build_addin(pres, path):
"""
This procedure does the following:
1. adds all of the VBComponents to the working PPTM file
2. adds required project references
The .PPTM file is used for local development & debugging and
is only usually packaged as a PPAM for Testing and Distribution
"""
version = str(int(float(pres.Application.version)))
# import the VB Components
for fn in [fn for fn in os.listdir(path) if not(fn.endswith(".frx"))]:
pres.VBProject.VBComponents.Import(path + "\\" + fn)
# add the required project references
pres.VBProject.References.AddFromFile(r'C:\Program Files (x86)\Microsoft Office\Office'+version+'\EXCEL.EXE')
# MSForms TreeView Control
pres.VBProject.References.AddFromFile(r'C:\Windows\SysWOW64\MSCOMCTL.OCX')
# MSXML2
pres.VBProject.References.AddFromFile(r'C:\Windows\System32\msxml6.dll')
# ADODB
pres.VBProject.References.AddFromFile(r'C:\Program Files (x86)\Common Files\System\ado\msado15.dll')
# VBE Extensibility
I also found some possibly malformed XML in build_ribbon
and fixed it, but still not quite 100% because PowerPoint still has to "repair" the file the first time it is opened (once), but after that it seems to work as expected ...
I noticed that the custom logo does not appear on the ribbon, and I found that the "unreadable content" is probably related to the JPG image file that is being loaded on one of the ribbon controls. From this forum at OpenXMLDeveloper :
This problem occurs when there is a problem in one of the following areas.
- Relationship id does not match parts
- Error in content_types.xml file
- Error in parts (document.xml or any other parts)
- Unrelated relationship between slide slide slide / slide slide
I double check the [Content_Types] .xml file does not include the .jpg extension element.
I add an import statement for the ElementTree:
import xml.etree.ElementTree as ET
And then change build_ribbon_zip
like this:
def build_ribbon_zip():
"""
build_ribbon_zip handles manipulation of the .ZIP contents and places the
necessary components within the PPTM ZIP archive structure
3. converts the PPTM to a .ZIP
4. Adds the CustomUI XML to the .ZIP directory
5. converts the .ZIP to a PPTM
"""
bom = u'\ufeff'
_path=output_path.replace('.pptm', '.zip')
copy_path=r'C:\debug\copy.zip'
# Convert to ZIP archive
os.rename(output_path, _path)
z=zipfile.ZipFile(_path, 'a', zipfile.ZIP_DEFLATED)
copy=zipfile.ZipFile(copy_path, 'w', zipfile.ZIP_DEFLATED)
guid=str(uuid.uuid4()).replace('-', '')[:16]
"""
the .rels files are written directly from XML string built in procedure
the [Content_Types].xml file needs to include additional parameter for the 'jpg' extension
"""
for itm in [itm for itm in z.infolist() if itm.filename != r'_rels/.rels']:
buffer = z.read(itm.filename)
if itm.filename == "[Content_Types].xml":
# Modify the [Content_Types].xml file to include the jpg reference
# <Default Extension="jpg" ContentType="image/.jpg" />
# copy the XML from the original zip archive, this file has not been copied in the above loop
root = ET.fromstring(buffer)
ET.SubElement(root, '{http://schemas.openxmlformats.org/package/2006/content-types}Default', {'Extension': 'jpg', 'ContentType': 'image/.jpg'})
copy.writestr(itm, ET.tostring(root).encode('utf-8'))
# Append the Logo file to the .zip and create the archive
copy.write(ribbon_logo_path, r'\customUI\images\jdplogo.jpg')
else:
copy.writestr(itm, buffer)
# append the CustomUI xml part to the .zip and create the archive
copy.write(ribbon_xml_path, r'\customUI\customUI14.xml')
# create the string & append the .rels to CustomUI\_rels
rels_xml = """<?xml version="1.0" encoding="utf-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="images/jdplogo.jpg" Id="jdplogo" />
</Relationships>"""
copy.writestr(r'customUI\_rels\customUI14.xml.rels', rels_xml.encode('utf-8'))
# get the existing _rels/.rels XML content and copy to the copied archiveI:
rels_xml = r'<?xml version="1.0" encoding="utf-8" ?>'
rels_xml += r'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
rels_xml += r'<Relationship Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/'
rels_xml += r'core-properties" '
rels_xml += r'Target="docProps/core.xml" Id="rId3" />'
rels_xml += r'<Relationship Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail" '
rels_xml += r'Target="docProps/thumbnail.jpeg" Id="rId2" />'
rels_xml += r'<Relationship Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" '
rels_xml += r'Target="ppt/presentation.xml" Id="rId1" />'
rels_xml += r'<Relationship Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" '
rels_xml += r'Target="docProps/app.xml" Id="rId4" /><Relationship '
rels_xml += r'Type="http://schemas.microsoft.com/office/2007/relationships/ui/extensibility" '
rels_xml += r'Target="/customUI/customUI14.xml" Id="R'+guid+'" /></Relationships>'
copy.writestr(r'_rels\.rels', rels_xml.encode('utf-8'))
z.close()
copy.close()
os.remove(_path)
os.rename(copy_path, output_path)
source to share