OneSheep OneSheep

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
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/ --writer --accept="socket,host=localhost,port=2002;urp;StarOffice.ServiceManager"


speng start

Next we launch the copy of Python which is included in LibreOffice:


To start controlling our document, we type in the following:

import uno
localContext = uno.getComponentContext()
resolver = localContext.ServiceManager.createInstanceWithContext("", localContext)
context = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext")
desktop = context.ServiceManager.createInstanceWithContext("", 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 file:

import uno

def getModel():
# get the uno component context from the PyUNO runtime
localContext = uno.getComponentContext()

# create the UnoUrlResolver
resolver = localContext.ServiceManager.createInstanceWithContext(
"", 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("", 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 

To run your script against the open document you issue this command:

/Applications/ -c "import scriptlight; scriptlight.prepareDocument()"


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:

  1. There is no one canonical place to look. Both OpenOffice and LibreOffice have docs that point to each other.
  2. 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.


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.


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

TEMPLATE_FOLDER="~/Library/Application\ Support/LibreOffice/4/user/template"

usage () {
  echo "Usage: $0 (start|run |deploy|open|change)"

case "$1" in
  /Applications/ --writer --accept="socket,host=localhost,port=2002;urp;StarOffice.ServiceManager"
  /Applications/ -c "import scriptlight; scriptlight.$2()"
  # opens the current folder and the sample folder as a project in my code editor
  # edit this file
  edit $0
  # test if script is sourced
  if [[ $0 = ${BASH_SOURCE} ]] ; then
  # quickly navigate to the project folder when we type ". speng"

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 which does the deployment for us. Check it out in this gist.

Further reading

Posted on Jan 19, 2017 by Jannie Theunissen

Back to all posts