Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python-UNO bridge to libreoffice inside docker container. #108

Merged
merged 11 commits into from
Feb 6, 2024
10 changes: 8 additions & 2 deletions docker/lo-ubuntu2204/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# Stage 1: Build LibreOffice
FROM ubuntu:22.04
SHELL ["/bin/bash", "-c"]

Expand All @@ -18,6 +17,9 @@ RUN DEBIAN_FRONTEND="noninteractive" apt-get install -y \
curl git vim sudo wget tzdata \
x11-xserver-utils
RUN apt-get install -y libreoffice
RUN apt-get update && \
apt-get install -y python3 python3-pip python3-uno libreoffice-dev
RUN pip install Flask

# clean up the apt cache to reduce the image size
RUN rm -rf /var/lib/apt/lists/*
Expand All @@ -31,11 +33,15 @@ RUN useradd --create-home -s /bin/bash $user \
&& adduser $user sudo \
&& echo "$user ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers

COPY --chown=$user:$user container/* $home
blattms marked this conversation as resolved.
Show resolved Hide resolved
RUN chmod +x $home/start.sh
RUN chmod +x $home/update-libreoffice-config.sh

WORKDIR $home

ENV USER=$user
ENV SHELL=/bin/bash
USER $user

CMD ["/bin/bash"]
CMD ["./start.sh"]

39 changes: 39 additions & 0 deletions docker/lo-ubuntu2204/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Run LibreOffice from Ubuntu 22.04 docker container

The scripts in this folder allows you to run LibreOffice version 7.5.9 inside a docker
container. Currently, there are two scripts that can be used to run LibreOffice:

- `docker-soffic.sh` : This script runs LibreOffice the same way that you would use
outside the container. So running `./docker-soffice.sh main.fodt` will open the
main document.
- `start-container.sh` : This script will run LibreOffice as a daemon (a standalone background process that
listens to messages over a socket) inside the docker container. The host can then communicate
with this daemon using a Python UNO bridge script that is running inside the docker container.
The communication between the host machine and docker container is done using a rest API against a
flask web server running inside the docker container. The API currently only has a single
end point "open-document" which will open a given FODT document and also update its indexes.

To simplify the use of this end point, you can use the Python script `lodocker-open-file <file.fodt>`.
See information about installing the Python script below.

## Installation of the python scripts
- Requires python3 >= 3.10

### Using poetry
For development it is recommended to use poetry:

- Install [poetry](https://python-poetry.org/docs/)
- Then run:
```
$ poetry install
$ poetry shell
```

### Installation into virtual environment
If you do not plan to change the code, you can do a regular installation into a VENV:

```
$ python -m venv .venv
$ source .venv/bin/activate
$ pip install .
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For dummies like me we should probably mention that one has to be in subdirectory docker/lo-ubuntu2204 for this to work

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes thanks for the reminder! I have updated the README.md with this information

```
2 changes: 1 addition & 1 deletion docker/lo-ubuntu2204/build-image.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#! /bin/bash

docker build --network=host -t lo-ubuntu2204 .
docker build -t lo-ubuntu2204 .
81 changes: 81 additions & 0 deletions docker/lo-ubuntu2204/container/docker-server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import logging
import os
#import subprocess
from pathlib import Path

from flask import Flask, request

import uno

from unohelper import systemPathToFileUrl, absolutize
from com.sun.star.beans import PropertyValue

app = Flask(__name__)

def update_indexes(doc):
"""
Update all indexes in the given document.
"""
try:
indexes = doc.getDocumentIndexes()
for i in range(0, indexes.getCount()):
index = indexes.getByIndex(i)
index.update()
logging.info("Indexes updated successfully.")
except Exception as e:
logging.error("Error updating indexes and tables: " + str(e))

def open_document_with_libreoffice(doc_path: str):
# Connect to the running instance of LibreOffice
local_context = uno.getComponentContext()
resolver = local_context.ServiceManager.createInstanceWithContext(
"com.sun.star.bridge.UnoUrlResolver", local_context
)
port = int(os.getenv('LIBREOFFICE_PORT', 2002))
ctx = resolver.resolve(f"uno:socket,host=localhost,port={port};urp;StarOffice.ComponentContext")
smgr = ctx.ServiceManager
desktop = smgr.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)
# Load the document
cwd = systemPathToFileUrl( os.getcwd() )
path_ = "parts" / Path(doc_path)
file_url = absolutize( cwd, systemPathToFileUrl(str(path_)) )

load_props = []
#load_props.append(PropertyValue(Name="Hidden", Value=True)) # Open document hidden
#load_props.append(PropertyValue(Name="UpdateDocMode",
# Value=uno.getConstantByName("com.sun.star.document.UpdateDocMode.QUIET_UPDATE")))
load_props = tuple(load_props)

logging.info("Loading {}".format(file_url))
doc = desktop.loadComponentFromURL(file_url, "_blank", 0, load_props)

update_indexes(doc)

# Save the document
# The user can save the document from the menu for now.
# If we want to automate the saving process we can do that by running libreoffice
# in headless mode using doc.store() as shown below
#doc.store()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this intended to be commented out?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this is intended. The user can save the document from the menu for now. If we want to automate the saving process we can do that by running libreoffice in headless mode using doc.store() as you mention.

Copy link
Collaborator Author

@hakonhagland hakonhagland Feb 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment in the source code also, see latest commit

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks :)

#doc.dispose()
logging.info("Done")


@app.route('/open-document', methods=['POST'])
def open_document():
# Extract the document path from the request
doc_path = request.json.get('path')

# Replace this with the command to open the document using LibreOffice
open_document_with_libreoffice(doc_path)
return "Document opened", 200

if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
# ASSUME: libreoffice deamon process has already been started and listening
# on port 2002 at localhost :
#
# libreoffice --accept="socket,host=localhost,port=2002;urp;" --pidfile=lo_pid.txt
#
port = int(os.getenv('FLASK_PORT', 8080))
app.run(debug=True, host='0.0.0.0', port=port)

20 changes: 20 additions & 0 deletions docker/lo-ubuntu2204/container/open-blank-document.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

import logging
import os
import uno

try:
# Connect to the running instance of LibreOffice
local_context = uno.getComponentContext()
resolver = local_context.ServiceManager.createInstanceWithContext(
"com.sun.star.bridge.UnoUrlResolver", local_context
)
port = int(os.getenv('LIBREOFFICE_PORT', 2002))
ctx = resolver.resolve(f"uno:socket,host=localhost,port={port};urp;StarOffice.ComponentContext")
smgr = ctx.ServiceManager
desktop = smgr.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)

# Open a blank document
document = desktop.loadComponentFromURL("private:factory/swriter", "_blank", 0, ())
except Exception as e:
logging.error(f"Error in opening blank document: {e}")
56 changes: 56 additions & 0 deletions docker/lo-ubuntu2204/container/start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/bin/bash

# This file is run when the Docker container is started "docker run" without additional arguments.
# We have wrapped the "docker run" in a script "start-container.sh" that takes care of
# mounting the shared directory and the fonts directory, and also sets up the X11 connection.
#
# This script "start.sh" starts the LibreOffice daemon and the Python Flask server in the
# Docker container that enables the host to communicate with the Docker container using a REST API.

# Further, we would like to get rid of the popup dialog when opening a file with links to other files.
# The popup dialog has the message:
#
# "The document main.fodt contains one or more links to external data.
# Would you like to change the document, and update all links to get
# the most recent data?", see:
#
# https://ask.libreoffice.org/t/avoid-popup-dialog-at-startup-would-you-like-to-update-all-links-to-get-the-most-recent-data/99189
#
# I first tried to pass "com.sun.star.document.UpdateDocMode.QUIET_UPDATE" to the loadComponentFromURL()
# method in the python script "docker-server.py", but that did not work.
# I have submitted a PR to fix this: https://gerrit.libreoffice.org/c/core/+/161628
#
# In the mean time, I found a workaround by modifying the user profile file
# "registrymodifications.xcu". This file is located in the directory
# "/home/docker-user/.config/libreoffice/4/user/" and needs to be modified
# as shown in the script "update-libreoffice-config.sh".
#
# However, we cannot modify this file before LibreOffice has been started once
# to initialize the user profile. Therefore we start LibreOffice, wait 5 seconds,
# kill it, modify the file, and start it again. See below:

libreoffice --accept="socket,host=localhost,port=${LIBREOFFICE_PORT};urp;" --headless \
--norestore --nofirststartwizard --nologo --nodefault --pidfile=/home/docker-user/lo_pid.txt &


echo "Waiting 5 seconds for LibreOffice to start and intialize..."
sleep 5

# Call a script that opens a blank document in LibreOffice Writer
python3 open-blank-document.py
echo "Waiting 5 seconds for LibreOffice to open a blank document..."
sleep 5
echo "Killing LibreOffice..."
kill $(cat /home/docker-user/lo_pid.txt)

# Modify the user profile to get rid of popup dialog when opening a file with
# links to other files
./update-libreoffice-config.sh

echo "Restarting LibreOffice..."
# Restart LibreOffice with the modified user profile
libreoffice --accept="socket,host=localhost,port=${LIBREOFFICE_PORT};urp;" \
--norestore --nofirststartwizard --nologo --nodefault --pidfile=/home/docker-user/lo_pid.txt &

# Start the Flask server
exec python3 docker-server.py
38 changes: 38 additions & 0 deletions docker/lo-ubuntu2204/container/update-libreoffice-config.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/bin/bash

# This file is needed to avoid the popup dialog with message:
# "The document main.fodt contains one or more links to external data.
# Would you like to change the document, and update all links to get
# the most recent data?", see:
#
# https://ask.libreoffice.org/t/avoid-popup-dialog-at-startup-would-you-like-to-update-all-links-to-get-the-most-recent-data/99189
#

CONFIG_FILE="/home/docker-user/.config/libreoffice/4/user/registrymodifications.xcu"
TEMP_FILE="/home/docker-user/.config/libreoffice/4/user/temp_registrymodifications.xcu"

# Append the SecureURL item
append_secure_url() {
echo '<item oor:path="/org.openoffice.Office.Common/Security/Scripting"><prop oor:name="SecureURL" oor:op="fuse"><value><it>$(home)/parts</it></value></prop></item>' >> "$TEMP_FILE"
}

# Append the Update LinkMode item
append_update_link_mode() {
echo '<item oor:path="/org.openoffice.Office.Writer/Content/Update"><prop oor:name="Link" oor:op="fuse"><value>2</value></prop></item>' >> "$TEMP_FILE"
}

# Copy original content excluding the last line (</oor:items>)
head -n -1 "$CONFIG_FILE" > "$TEMP_FILE"

# Append new configuration items
append_secure_url
append_update_link_mode

# Append the final closing tag
echo '</oor:items>' >> "$TEMP_FILE"

# Replace the original file with the modified one
mv "$TEMP_FILE" "$CONFIG_FILE"

# Display a message
echo "Modified registrymodifications.xcu successfully"
Loading