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()

      

+3


source to share


1 answer


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)

      

0


source







All Articles