Scripting LibreOffice with Python
We were recently asked to automate some editing tasks for the Spotlight English editors who use LibreOffice Writer to prepare their episode copy.
With LibreOffice’s UNO (Universal Network Objects) component model, which has bindings to many programming languages, we were quite spoiled for choice. We could have gone with JavaScript, Basic, Python and the Java-like BeanShell scripting languages. Python was our favourite in that bunch and once you get going it is a very powerful and productive stack to work with. However, it was difficult to find information on how to set up a development environment. It was also a challenge to find good examples of how to code against the extensive API. So, here is what we learned:
Set up a project folder
With any project we like to have our code under version control so we can collaborate and roll back to earlier versions. So, the first step is to set up a project folder where we can edit, build, test and deploy our code. And after the project is delivered we can easily archive the folder.
mkdir scriptlight && cd scriptlight touch scriptlight.py dev.py git init
Our production macros will go into scriptlight.py which we will later embed into our document. dev.py will have some useful scaffolding methods that we want to call on while we are developing the code.
Quick feedback loop
To run a macro in LibreOffice, the scripting file must be in a special system folder or embedded into the document. We could put a symlink to our scriptlight folder in that system folder and configure a keyboard shortcut or toolbar button to trigger it, but it turns out that when we make changes to the macro, the script has to be reloaded by closing and opening the document. This won’t do.
Happily, LibreOffice can expose it’s API to the shell by running with an open socket. Let’s try it.
First we launch LibreOffice Writer with a new document and an open socket to communicate with from the Python shell:
/Applications/LibreOffice.app/Contents/MacOS/soffice --writer --accept="socket,host=localhost,port=2002;urp;StarOffice.ServiceManager"
or
speng start
Next we launch the copy of Python which is included in LibreOffice:
/Applications/LibreOffice.app/Contents/MacOS/python
To start controlling our document, we type in the following:
import uno localContext = uno.getComponentContext() resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext) context = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") desktop = context.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", context) model = desktop.getCurrentComponent() text = model.Text cursor = text.createTextCursor() text.insertString(cursor, 'Hello world!', 0)
This should now insert our message into the open document.
Run from a file
When the script is not running through a socket connection, there is a shorter way to get hold of the active document or “model”, but while we have not embedded the script we have to use the steps above. Let’s package this as a function in our dev.py
file:
import uno def getModel(): # get the uno component context from the PyUNO runtime localContext = uno.getComponentContext() # create the UnoUrlResolver resolver = localContext.ServiceManager.createInstanceWithContext( "com.sun.star.bridge.UnoUrlResolver", localContext) # connect to the running office context = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext") manager = context.ServiceManager # get the central desktop object desktop = manager.createInstanceWithContext("com.sun.star.frame.Desktop", context) # access the current writer document return desktop.getCurrentComponent()
Now in scriptlight.py we can have the following:
import dev # remove before deployment def getModel(): """ just before deployment we can change this to return XSCRIPTCONTEXT.getDocument() # embedded """ return dev.getModel() # via socket def prepareDocument(): model = getModel() search = model.createSearchDescriptor() # dev.printObjectProperties(search) # explore the object search.setPropertyValue('SearchRegularExpression', True); # remove all paragraph padding search.setSearchString('^\s*(.+?)\s*
To run your script against the open document you issue this command:
/Applications/LibreOffice.app/Contents/MacOS/python -c "import scriptlight; scriptlight.prepareDocument()"
Or…
speng run prepareDocument
Ah, now we are in business. We can make a small change to the script and hit up arrow enter in terminal to test it.
Some things to note: you can expose any function in the script to be available for calling from the document by including the name in the list on the last line. Note the inconsistent use of the comma. If you had two functions it would look like this:
g_exportedScripts = prepareDocument,countForeignWords
Exploring the API
The API docs are not the easiest to work with:
- There is no one canonical place to look. Both OpenOffice and LibreOffice have docs that point to each other.
- The docs have code samples in many different languages and most of them use the interface-orientated architecture that is unnecessarily indirect.
To help with this a little we have these functions in dev: dev.printObjectProperties
and dev.printInterfaces
which will list any object’s properties and the interfaces they implement respectively.
Deployment
When we have developed and tested our macro via the convenient socket interface, we are ready to deploy it into a template or document. The first deploy step is to comment out all uses of the dev helper script. Then push the macro into a newly prepared LibreOffice document file. It turns out that a LibreOffice document is actually a zipped folder with various content, formatting and meta data files inside. To add a macro file, we must add the file to a special folder inside the zip and register the new file in the zip manifest. This can be done manually with a long recipe or like this:
speng deploy
Once the script is deployed, you can open the document and the macro should show up under the run macro menu. Typically you will right-click on a visible toolbar and select Customize toolbar… from the context menu to assign your macro to a toolbar button or a shortcut key.
Running the embedded script is a lot faster than running it through a socket, but you might not notice the difference if your script isn’t doing a lot of work.
Speng
Rather than adding a load of command aliases to our bash profile with every new project, we like to make a small bash script with intuitive shortcuts to all our repeating CLI commands for the project. This way we can file it away with our project once it is done and if we ever need to come back to the project months later, we don’t have to go read up again how to start, build or deploy the project. So for this project we picked “speng” SPotlight ENGlish:
touch speng && chmod +x speng && ln -s `pwd`/speng ~/bin/speng
This is how mine looks:
#!/usr/bin/env bash PROJECT_FOLDER="/Users/jannie/Desktop/scriptlight" TEMPLATE_FOLDER="~/Library/Application\ Support/LibreOffice/4/user/template" SAMPLE_FOLDER="/Applications/LibreOffice.app/Contents/Resources/Scripts/python" SCRIPT="scriptlight.py" HOSTING_DOCUMENT="/Users/jannie/Desktop/test.odt" usage () { echo "Usage: $0 (start|run |deploy|open|change)" } case "$1" in start) /Applications/LibreOffice.app/Contents/MacOS/soffice --writer --accept="socket,host=localhost,port=2002;urp;StarOffice.ServiceManager" ;; run) /Applications/LibreOffice.app/Contents/MacOS/python -c "import scriptlight; scriptlight.$2()" ;; deploy) cd $PROJECT_FOLDER python push_macro.py $SCRIPT $HOSTING_DOCUMENT ;; open) # opens the current folder and the sample folder as a project in my code editor edit . $SAMPLE_FOLDER ;; change) # edit this file edit $0 ;; *) # test if script is sourced if [[ $0 = ${BASH_SOURCE} ]] ; then usage else # quickly navigate to the project folder when we type ". speng" cd $PROJECT_FOLDER fi ;; esac
It might look scary and overkill to write all that just to save a few keystrokes, but once you have a template like this it is easy to adapt for any project and it is a great way to document what you need to do next time you pull the project from the shelf.
You will see a reference to push_macro.py
which does the deployment for us. Check it out in this gist.
Further reading
- Official API docs – good luck
- Christopher Bourez’s blog – excellent article with lots of good examples. Especially his Calc macros.
- Jamie Boyle’s Cookbook – learned some good debugging tips from this guide
Posted on