Skip to content

Instantly share code, notes, and snippets.

@Foadsf
Last active March 22, 2024 05:57
Show Gist options
  • Star 18 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save Foadsf/58d401c9b9ed5d80f60deee88d1fcdfd to your computer and use it in GitHub Desktop.
Save Foadsf/58d401c9b9ed5d80f60deee88d1fcdfd to your computer and use it in GitHub Desktop.
Scripting LibreOffice with Python

This tutorial was originally written by Jannie Theunissen on onesheep.org. However, the website has been down for a while and this a clone from the web.archive.org backup. Also, the parts regarding the macOS are updated according to this post. You may find OneSheep here on Twitter and Jannie Theunissen here on StackOverflow. If you have any comments on this Gist please poke me here on Twitter, otherwise, I might miss your comments.

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 use speng.sh bash script:

speng start

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

/Applications/LibreOffice.app/Contents/Resources/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*$') # start and trailing whitespace
  search.setReplaceString('$1') 
  replaced = model.replaceAll(search) 
  
  # remove all empty paragraphs 
  search.setSearchString('^$') # empty paragraphs 
  search.setReplaceString('') 
  replaced = model.replaceAll(search) 
  
g_exportedScripts = prepareDocument,

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

/Applications/LibreOffice.app/Contents/Resources/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 toobar 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/Resources/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 refence to push_macro.py which does the deployment for us. Check it out in this gist.

Further reading

@tillbaum
Copy link

Awesome! This is a great idea.
Buttons which fire a python script on your own toolbar would be great too.
There have been some extensions regarding python.
Thank you.

@carafelix
Copy link

Linux Users document socket command:
/lib/libreoffice/program/soffice.bin --writer --accept="socket,host=localhost,port=2002;urp;StarOffice.ServiceManager"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment