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.
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:
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.
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.
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
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.
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.
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.
- 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
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.