简体   繁体   中英

Python Build Script for VBA Add-In file

I have written a python script that will serve as a "build script" for a macro-enabled PowerPoint file that I am supporting.

The script creates a new, empty PowerPoint presentation, imports all of the VBA modules, saves the file and converts it to a ZIP archive in order to insert th e the RibbonUI configurations ( ribbon_xml.xml file and the mylogo.jpg file).

All of this is working 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 exits cleanly but the output archive (copy.zip) when converted to a PPTM file does not open cleanly.

I receive the warning that something is wrong with the configuration and PowerPoint will attempt to repair the file.

PowerPoint, true to its nature of course does not indicate what the problem is, only that it has found "unreadable content" and that such content has "been removed"...The only thing I can see after comparing some files which I created manually, is that the XML attribute for the CustomUI appears to use some sort of GUID as part of its id attribute

Current workaround: the function build_ribbon could be done manually using the CustomUI Editor tool and would take about 3 minutes to reliably produce the PPTM output.

So this is not particularly a "Python" question, since it's a question about the implementation of the CustomUI XML / ribbon XML interface.

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

Some references are missing in the output, which causes PowerPoint to be mad. Resolved this like so:

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 the build_ribbon and fixed that, but still not quite 100% because PowerPoint still has to "repair" the file when it first opens (once), but after that, it seems to be working as expected.

I notice that the custom logo is not appearing in the ribbon and I find that the "unreadable content" is probably related to a JPG image file that is loaded on one of the ribbon controls. From this forum on OpenXMLDeveloper :

This kind of problem occurs when there is an issue in one of the following areas.

  1. Relationship Id does not match with parts
  2. Error in content_types.xml file
  3. Error in parts (document.xml or any other parts)
  4. Mismatched link between slide master-slide layout/slide layout-slide

I double-check the [Content_Types].xml file does not include the element for .jpg file extension.

I add the import statement for ElementTree:

import xml.etree.ElementTree as ET

And then modify the build_ribbon_zip as follows:

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)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM