3

I have been trying to create multiple pdf files using DispatchEx but when I try to test my code, it creates only first pdf file while all other requests fail with strange errors. What am I doing wrong and/or how can I effectively handle multiple clients calls simultaneously to generate their respective pdf files they request?

Here is that part of my code:

            rundate = "Quote_{:%d%m%Y_%H%M%S%f}".format(datetime.now())
            pythoncom.CoInitialize()
            FILENAME = "D:/packages/abc.pptx"
            APPLICATION = win32com.client.DispatchEx("PowerPoint.Application")
            APPLICATION.Visible = True
            path_ppt = shutil.copy(FILENAME, "D:/{0}.pptx".format(rundate))
            PRESENTATION = APPLICATION.Presentations.Open(path_ppt)
            Slide1 = PRESENTATION.Slides(1)
            Shape1 = Slide1.Shapes(1)
            print(Shape1.AlternativeText)
            for shape in Slide1.Shapes:
                if shape.AlternativeText:
                    print(shape.AlternativeText)
                if shape.HasTextFrame:
                    shape.TextFrame.TextRange.Replace(FindWhat="#abc",ReplaceWhat="THAILAND", WholeWords=False)
                if shape.AlternativeText == "1":
                    shape.Fill.UserPicture("D:/1.jpg")
                if shape.AlternativeText == "2":
                    shape.Fill.UserPicture("D:/2.jpg")
                if shape.AlternativeText == "3":
                    shape.Fill.UserPicture("D:/3.jpg")
            PATH_TO_PDF = "{0}{1}{2}".format(r'd:/',rundate,'.pdf')
            PRESENTATION.SaveAs(PATH_TO_PDF, 32)
            APPLICATION.Quit()
            PRESENTATION.Close()
            PRESENTATION =  None
            APPLICATION = None
            os.remove(path_ppt)

PS - The code successfully creates as many ppt copies (using shutil) as requests sent to it, but the win32com gives error when multiple requests are made in short interval of time, like shape.AlternativeText not found, object does not exist etc.

J J
  • 43
  • 7
  • 1
    Maybe its `APPLICATION.quit` overlapping another application dispatch. Perhaps you could unilaterally open the application and just do the `PRESENTATION = APPLICATION.Presentations.Open(path_ppt)` part in this worker. – tdelaney Feb 19 '20 at 21:50
  • Could you please shed some more light on this about how to do it? I am new to using win32com and trying my best. – J J Feb 19 '20 at 21:56
  • Even if you could help me out with how to know if one call already exists and then ask my client to try later, because DispatchEx doesnt seem to work the way I understand it – J J Feb 19 '20 at 21:57
  • Its been a long time since I've done `win32com` so just speculating. I'm taking a wild guess that creating multiple presentation in one instance of ppt is better than creating multiple ppts. I find the docs on `IDispatchEx` more than a little confusing. Suppose `APPLICATION` was a global variable and you only did `APPLICATION = win32com.client.DispatchEx("PowerPoint.Application")` once. Then presentations would come and go as tabs in that application. – tdelaney Feb 19 '20 at 22:14
  • If you have a client hot under the collar, you could protect this code with a mutex to serialize it... at the expense of potentially messing up whatever framework you are in. – tdelaney Feb 19 '20 at 22:15
  • Thanks. I tried declaring APPLICATION as global variable in my Django view but it gives "referenced before assignment error", which is strange as I have another global variable (company_name) which works fine for all views. Just a thought - is there a way I can refer to individual powerpoint processes generated by DispatchEx? I also suspect that error is occurring since APPLICATION.QUIT() denotes to all instances and closes them all while I should serially close them. I have tried 'for a in APPLICATION: but in vain – J J Feb 19 '20 at 22:25
  • Add `global APPLICATION` to your method above first use to get rid of that error message. I haven't figured out the APPLICATION thing either. Perhaps this code could be put into a different .py file and then use `subprocess` to call it. – tdelaney Feb 19 '20 at 22:33
  • Perhaps use `makepy` and `win32com.client.gencache.EnsureDispatch ("Powerpoint.Application")` instead of `IDispatch` for early binding of interfaces. Otherwise, with late binding you get method/variable names dynamically with `GetIDsOfNames` with lots of caveats in the Windows IDispatchEx docs. I'm not sure whether this will bring up new version of the ppt app or not, but worth a try. http://timgolden.me.uk/python/win32_how_do_i/generate-a-static-com-proxy.html and https://stackoverflow.com/questions/50127959/win32-dispatch-vs-win32-gencache-in-python-what-are-the-pros-and-cons – tdelaney Feb 19 '20 at 23:02
  • Finished tried them but in vain. I cant create a subprocess using py file as I am a newb. However, I do feel like it will help to handle code separately for separate instances since APPLICATION.Quit() is quitting all open instances of DispatchEx even before some of them have finished, and that gives out strange errors. I dont know why I cant access each instance using for loop, it would have been nice to do so – J J Feb 19 '20 at 23:16

2 Answers2

1

You may be able to solve the APPLICATION exit problem by running this code in a separate process. (Ignoring the risk of using a hard coded file name when simultaneous calls).

Create another module in your project package

powerpointer.py

import shutil
import sys
import subprocess as subp

def do_powerpoint(filename):
   """run external copy of self to do powerpoint stuff"""
   # sys.executable is the python.exe you are using, __file__ is the
   # path to this module's source and filename you pass in
   return subp.call([sys.executable, __file__, filename])

def _do_powerpoint(filename):
    rundate = "Quote_{:%d%m%Y_%H%M%S%f}".format(datetime.now())
    pythoncom.CoInitialize()
    APPLICATION = win32com.client.DispatchEx("PowerPoint.Application")
    APPLICATION.Visible = True # I think False is better so you dont see it pop up
    path_ppt = shutil.copy(filename, "D:/{0}.pptx".format(rundate))
    PRESENTATION = APPLICATION.Presentations.Open(path_ppt)
    Slide1 = PRESENTATION.Slides(1)
    Shape1 = Slide1.Shapes(1)
    print(Shape1.AlternativeText)
    for shape in Slide1.Shapes:
        if shape.AlternativeText:
            print(shape.AlternativeText)
        if shape.HasTextFrame:
            shape.TextFrame.TextRange.Replace(FindWhat="#abc",ReplaceWhat="THAILAND", WholeWords=False)
        if shape.AlternativeText == "1":
            shape.Fill.UserPicture("D:/1.jpg")
        if shape.AlternativeText == "2":
            shape.Fill.UserPicture("D:/2.jpg")
        if shape.AlternativeText == "3":
            shape.Fill.UserPicture("D:/3.jpg")
    PATH_TO_PDF = "{0}{1}{2}".format(r'd:/',rundate,'.pdf')
    PRESENTATION.SaveAs(PATH_TO_PDF, 32)
    PRESENTATION.Close()
    APPLICATION.Quit()
    os.remove(path_ppt)

if __name__ == "__main__":
    _do_powerpoint(sys.argv[1]) # will raise error if no parameters

Now, in your main code, import powerpointer and call powerpointer.do_powerpoint(filename) as needed. It will run its own module as a script and you will only have 1 application object in that instance.

tdelaney
  • 73,364
  • 6
  • 83
  • 116
  • Thank you, tdlaney for the code. I tried it today but the app gives me "call was rejected by callee" error. Seems like it cannot open multiple applications in simultaneous calls? Errors received when making 5 silmultaneous calls: pywintypes.com_error: (-2147418111, 'Call was rejected by callee.', None, None) self._oleobj_.InvokeTypes(dispid, 0, wFlags, retType, argTypes, *args), self._oleobj_.InvokeTypes(dispid, 0, wFlags, retType, argTypes, *args), self._oleobj_.InvokeTypes(dispid, 0, wFlags, retType, argTypes, *args), – J J Feb 20 '20 at 05:09
  • After the error, I also tried to check if file is readable or not, only then code should work, but it still gives issue. It prints True for all calls which should mean the file is available? def _do_powerpoint(filename): print(os.access(filename, os.R_OK)) if os.access(filename, os.R_OK): runPpt(filename) else: t = Timer(30.0, runPpt) t.start() where runPpt contains the remaining _do_powerpoint code – J J Feb 20 '20 at 05:25
  • I also tried this code by removing the following lines and it worked fine: PRESENTATION.Close() APPLICATION.Quit() os.remove(path_ppt) Although, this should mean that even though we made subprocesses, the APPLICATION is still the same. I tried using Dispatch too instead of DispatchEx before trying to remove last lines, and it still gave call rejected by callee error – J J Feb 20 '20 at 06:37
0

After hours of working, I was able to solve this problem by introducing following changes:

1) Remove

APPLICATION.Visible = True 

2) Change

APPLICATION.Presentations.Open(path_ppt) 

to

PRESENTATION = APPLICATION.Presentations.Open(path_ppt, WithWindow=False, ReadOnly=False) 

3) Remove

APPLICATION.Quit()
PRESENTATION.Close()
PRESENTATION =  None
APPLICATION = None
os.remove(path_ppt) 

The problem was that if I used

APPLICATION.Quit()
PRESENTATION.Close()
PRESENTATION =  None
APPLICATION = None
os.remove(path_ppt) 

in the code and made simultaneous calls, it gave "call was rejected by callee" error. When I removed those lines of code, it was able to publish as many PDFs as powerpoints generated, but the PowerPoint instances were still there and copies of files too, eating-up system resources. Using WithWindow = False helps to automatically close PowerPoint instance after PDF publishing. The only problem left is the copies of PowerPoint files left, which can be removed at end of the day when system is idle at your client's side.

PS - I am using ExportAsFixedFormat instead of SaveAs for PDF PRESENTATION.ExportAsFixedFormat(PATH_TO_PDF, 32, PrintRange=None)

J J
  • 43
  • 7