1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4"""
5This python script connects with OpenOffice.org using PyUNO and provides
6us the functionality to control OpenOffice.org.
7
8    # Execute the python-script with the ODT-file as argument
9    python `kde4-config --install data`/calligrawords/scripts/extensions/oouno.py /path/mydoc.odt
10    # Define the hostaddress and port the OpenOffice.org server is running on
11    python `kde4-config --install data`/calligrawords/scripts/extensions/oouno.py --host=192.168.0.1 --port=2002 /path/mydoc.odt
12
13We are using the PyUNO module to access OpenOffice.org. For this an optional hidden
14OpenOffice.org instance need to be started. Then the script connects as client to
15this OpenOffice.org server instance and controls it.
16If the script is run and there connecting to the server failed, then it will (optional)
17startup such a OpenOffice.org server instance and shuts it down again once the work is
18done. A faster way is, to startup and shutdown the server instance by yourself and then
19the script does not need to do it for you each time.
20
21    # Start OpenOffice.org with visible mainwindow
22    soffice -nologo "-accept=socket,host=localhost,port=2002;urp;"
23    # Start OpenOffice.org in background
24    soffice -nologo -norestore -invisible -headless "-accept=socket,host=localhost,port=2002;urp;"
25
26(C)2007 Sebastian Sauer <mail@dipe.org>
27
28http://kross.dipe.org
29http://www.calligra.org/words
30http://udk.openoffice.org/python/python-bridge.html
31
32Dual-licensed under LGPL v2+higher and the BSD license.
33"""
34
35import sys, os, getopt, time, traceback, popen2, subprocess, signal #, threading
36
37try:
38    import uno
39    from com.sun.star.connection import NoConnectException as UnoNoConnectException
40    from com.sun.star.task import ErrorCodeIOException as UnoErrorCodeIOException
41    #from com.sun.star import connection as UnoConnection
42    from unohelper import Base as UnoBase
43    #from unohelper import systemPathToFileUrl, absolutize
44    from com.sun.star.beans import PropertyValue as UnoPropertyValue
45    #from com.sun.star.uno import Exception as UnoException
46    #from com.sun.star.io import IOException as UnoIOException
47    from com.sun.star.io import XOutputStream as UnoXOutputStream
48except ImportError, e:
49    print >> sys.stderr, "Failed to import the OpenOffice.org PyUNO python module. This script requires the PyUNO python module to communicate with the OpenOffice.org server."
50    raise e
51
52class UnoConfig:
53    """ The configuration for to access the OpenOffice.org functionality. """
54
55    def __init__(self):
56        # The host the OpenOffice.org Server runs on.
57        self.host= "localhost"
58        # The port the OpenOffice.org Server runs on.
59        self.port = 2002
60        # Number of seconds we try to connect before aborting, set to 0 to try to
61        # connect only once and -1 to disable timeout and try to connect forever.
62        self.timeout = 45
63        # Startup OpenOffice.org instance if not running already.
64        self.startupServer = True
65        # Hide the client window.
66        self.hideClient = True
67        # Close new documents once not needed any longer.
68        self.autoCloseDocument = True
69        # The used logger we write debug-output to.
70        self._logger = sys.stdout
71
72        # The file to load.
73        self.loadfile = ""
74        # The file to save.
75        self.savefile = ""
76
77class UnoDocument:
78    """ Class that represents an OpenOffice.org UNO document within an UnoClient. """
79
80    class OutputStream( UnoBase, UnoXOutputStream ):
81        """ The OutputStream class offers the default implementation of an output-stream
82        the content of the document could be written to. """
83
84        def __init__(self):
85            self.filterName = "Text (encoded)"
86            #self.filterName = "HTML (StarWriter)"
87            #self.filterName = "writer_pdf_Export"
88            self.closed = 0
89        def closeOutput(self):
90            self.closed = 1
91        def writeBytes(self, seq):
92            sys.stdout.write(seq.value)
93        def flush(self):
94            pass
95
96    def __init__(self, unoConfig, desktop):
97        self.unoConfig = unoConfig
98        self.desktop = desktop
99        self.doc = None
100
101    def __del__(self):
102        if self.unoConfig.autoCloseDocument:
103            self.close()
104
105    def load(self, fileUrl):
106        if not fileUrl.startswith("file://"):
107            raise "Invalid file url \"%s\"" % fileUrl
108
109        fileName = fileUrl[7:]
110        if not os.path.isfile(fileName):
111            raise "There exist no such file \"%s\"" % fileName
112
113        self.close()
114
115        fileBaseName = os.path.basename(fileName)
116        self.unoConfig._logger.write("Loading document %s ...\n" % fileBaseName)
117
118        inProps = []
119        if self.unoConfig.hideClient:
120            inProps.append( UnoPropertyValue("Hidden" , 0 , True, 0) )
121
122        self.doc = self.desktop.loadComponentFromURL(fileUrl , "_blank", 0, tuple(inProps))
123        if not self.doc:
124            raise "Failed to load document %s" % fileName
125
126        self.unoConfig._logger.write("Done loading document %s\n" % fileBaseName)
127
128    def save(self, fileUrl):
129        if not self.doc:
130            raise "Failed to save cause there is no document"
131        if fileUrl.startswith("file://"):
132            fileUrl = fileUrl[7:]
133        if not fileUrl:
134            raise "Failed to save cause invalid file \"%s\" defined." % fileUrl
135
136        try:
137            import unohelper
138            outUrl = unohelper.systemPathToFileUrl(fileUrl)
139            outProps = []
140
141            fileExt = os.path.splitext(fileUrl)[1].lower()
142            if fileExt == '.txt' or fileExt == '.text':
143                outProps.append( UnoPropertyValue('FilterName', 0, 'Text (encoded)', 0) )
144            elif fileExt == '.htm' or fileExt == '.html':
145                outProps.append( UnoPropertyValue('FilterName', 0, 'HTML (StarWriter)', 0) )
146            elif fileExt == '.pdf':
147                outProps.append( UnoPropertyValue('FilterName', 0, 'writer_pdf_Export', 0) )
148            #else: opendocument...
149
150            print "Save to: %s" % outUrl
151            self.doc.storeToURL(outUrl, tuple(outProps))
152        except:
153            traceback.print_exc()
154
155    def close(self):
156        if self.doc:
157            self.doc.close(True)
158            self.doc = None
159
160    def read(self, outputstream = OutputStream()):
161        outProps = []
162        outProps.append( UnoPropertyValue("FilterName" , 0, outputstream.filterName, 0) )
163        outProps.append( UnoPropertyValue("Overwrite" , 0, True , 0) )
164        outProps.append( UnoPropertyValue("OutputStream", 0, outputstream, 0) )
165
166        try:
167            self.doc.storeToURL("private:stream", tuple(outProps))
168        except UnoErrorCodeIOException, e:
169            self.unoConfig._logger.write("ErrorCodeIOException: %s" % e.ErrCode)
170
171class UnoServer:
172    """ Class that provides functionality to deal with the OpenOffice.org server instance. """
173
174    def __init__(self, unoConfig):
175        self.unoConfig = unoConfig
176
177        self.unoConfig._logger.write("Starting OpenOffice.org at %s:%s ...\n" % (self.unoConfig.host,self.unoConfig.port))
178        try:
179            self.process = popen2.Popen3([ #subprocess.Popen([
180                'soffice',
181                '-nologo', #don't show startup screen.
182                '-minimized', #keep startup bitmap minimized.
183                '-norestore', #suppress restart/restore after fatal errors.
184                '-invisible', #no startup screen, no default document and no UI.
185                '-headless',
186                "-accept=socket,host=%s,port=%s;urp;" % (self.unoConfig.host,self.unoConfig.port)
187            ], )
188        except IOError:
189            traceback.print_exc()
190            raise
191
192    def __del__(self):
193        if hasattr(self,'process'):
194            os.kill(self.process, signal.SIGKILL)
195            #os.kill(self.process.pid, signal.SIGINT)
196            #killedpid, stat = os.waitpid(self.process, os.WNOHANG)
197            #if killedpid == 0:
198                #print >> sys.stderr, "Failed to kill OpenOffice.org Server process"
199
200class UnoClient:
201    """ Class that provides the client-functionality to deal with an OpenOffice.org
202    server instance. """
203
204    def __init__(self, unoConfig):
205        self.unoConfig = unoConfig
206        self.unoServer = None
207        self.document = None
208
209        # get the uno component context from the PyUNO runtime
210        localContext = uno.getComponentContext()
211        # create the UnoUrlResolver
212        resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext)
213
214        # connect to the running office
215        elapsed = 0
216        while True:
217            self.unoConfig._logger.write("Trying to connect with OpenOffice.org on %s:%s ...\n" % (self.unoConfig.host,self.unoConfig.port))
218            try:
219                # the UNO url we like to resolve
220                url = "uno:socket,host=%s,port=%s;urp;StarOffice.ComponentContext" % (self.unoConfig.host,self.unoConfig.port)
221                # fetch the ComponentContext
222                componentContext = resolver.resolve(url)
223                # fetch the service manager
224                self.servicemanager = componentContext.ServiceManager
225                # create the desktop
226                self.desktop = self.servicemanager.createInstanceWithContext("com.sun.star.frame.Desktop", componentContext)
227                # create the UnoDocument instance
228                self.unoDocument = UnoDocument(self.unoConfig, self.desktop)
229                # job is done
230                break
231            except UnoNoConnectException:
232                self.unoConfig._logger.write("Failed to connect with OpenOffice.org on %s:%s ...\n" % (self.unoConfig.host,self.unoConfig.port))
233                if self.unoConfig.startupServer:
234                    if not self.unoServer:
235                        self.unoServer = UnoServer(self.unoConfig)
236                if self.unoConfig.timeout >= 0:
237                    if elapsed >= self.unoConfig.timeout:
238                        raise "Failed to connect to OpenOffice.org on %s:%s" % (self.unoConfig.host,self.unoConfig.port)
239                    elapsed += 1
240                time.sleep(1)
241
242        self.unoConfig._logger.write("Connected with OpenOffice.org on %s:%s\n" % (self.unoConfig.host,self.unoConfig.port))
243
244    def __del__(self):
245        if self.unoServer:
246            self.desktop.terminate()
247            time.sleep(1)
248            self.unoServer = None
249
250class UnoController:
251    """ Class that offers high level access to control all aspects of OpenOffice.org
252    we may need. """
253
254    def __init__(self, unoConfig = UnoConfig()):
255        self.unoConfig = unoConfig
256        self.unoClient = None
257
258    def connect(self):
259        self.unoClient = UnoClient(self.unoConfig)
260
261    def disconnect(self):
262        self.unoClient = None
263
264    def loadDocument(self, fileUrl):
265        if not self.unoClient:
266            raise "The client is not connected"
267        self.unoClient.unoDocument.load(fileUrl)
268
269    def saveDocument(self, fileUrl):
270        if not self.unoClient:
271            raise "The client is not connected"
272        self.unoClient.unoDocument.save(fileUrl)
273
274    def writeDocument(self, outputstream = UnoDocument.OutputStream()):
275        self.unoClient.unoDocument.read(outputstream)
276
277#class WordsOutputStream( UnoDocument.OutputStream ):
278    #def __init__(self, unoConfig):
279            ##self.filterName = "Text (encoded)"
280            #self.filterName = "HTML (StarWriter)"
281            ##self.filterName = "writer_pdf_Export"
282
283            #import Words
284            #self.doc = Words.mainFrameSet().document()
285            #self.html = ""
286
287    #def closeOutput(self):
288        ##self.doc.setHtml(self.html)
289        ##self.html = ""
290        #pass
291    #def writeBytes(self, seq):
292        #self.html += seq.value
293    #def flush(self):
294        #if self.html != "":
295            ##print self.html
296            #self.doc.setHtml(self.html)
297            #self.html = ""
298
299def start(unoconfig, opts, args):
300    print "ARGS: ", "".join(args)
301    print "OPTS: ", "\n".join( [ "%s=%s" % (s,getattr(unoconfig,s)) for s in dir(unoconfig) if not s.startswith('_') ] )
302
303    #class ProgressThread(threading.Thread):
304        #def __init__(self, unoconfig):
305            #self.done = False
306            #self.unoconfig = unoconfig
307            #threading.Thread.__init__(self)
308            ##self.progress = self.forms.showProgressDialog("Import...", "Initialize...")
309            ##self.progress.labelText = "Loading %s" % file
310            ##self.progress.value = 0
311            ##self.progress.update()
312        #def finish(self):
313            #if not self.done:
314                ##self.progress.value = 100
315                #self.done = True
316        #def run(self):
317            #while not self.done:
318                ##if self.value == self.progress.value:
319                ##    self.value = self.value + 1
320                ##self.progress.value = self.value
321                #time.sleep(1)
322            ##self.progress.reset()
323    #progressThread = ProgressThread(unoconfig)
324    #progressThread.start()
325    #progressThread.finish()
326    #progressThread.join() # wait till the thread finished
327
328    controller = UnoController(unoconfig)
329    controller.connect()
330    try:
331        if unoconfig.loadfile:
332            controller.loadDocument( "file://%s" % unoconfig.loadfile )
333
334        if unoconfig.savefile:
335            controller.saveDocument( "file://%s" % unoconfig.savefile )
336
337        #TODO disabled for now
338        #outputstream = UnoDocument.OutputStream()
339        #controller.writeDocument(outputstream)
340        #outputstream.flush()
341
342    finally:
343        controller.disconnect()
344
345def main(argv):
346    unoconfig = UnoConfig()
347
348    def usage():
349        print "Syntax:\n  %s [options]" % os.path.split(argv[0])[1]
350        print "\n  ".join([ "Options:", "--help prints usage informations", ])
351        for s in dir(unoconfig):
352            if not s.startswith('_'):
353                v = getattr(unoconfig,s)
354                print "  --%s (%s, %s)" % (s,type(v).__name__,v)
355    try:
356        opts, args = getopt.getopt(argv[1:], "h", ["help"] + [ "%s=" % s for s in dir(unoconfig) if not s.startswith('_') ])
357    except getopt.GetoptError, e:
358        usage()
359        print "\nArgument Error: ",e,"\n"
360        sys.exit(2)
361
362    for opt, arg in opts:
363        if opt in ("-h", "--help"):
364            usage()
365            sys.exit()
366        elif opt.startswith('--'):
367            n = opt[2:]
368            if not n.startswith('_'):
369                try:
370                    t = type( getattr(unoconfig,n) )
371                    if t == bool:
372                        setattr(unoconfig, n, arg and arg != 'None' and arg != 'False' and arg != '0')
373                    else:
374                        setattr(unoconfig, n, t(arg))
375                except ValueError, e:
376                    print "Argument Error: ",e,"\n"
377                    usage()
378                    sys.exit(2)
379
380    start(unoconfig, opts, args)
381
382if __name__ == "__main__":
383    main(sys.argv)
384