A pure-Go binding for LibreOfficeKit — the C API of LibreOffice — built on goexlib/cgo + purego to call liblibreofficekit.so dynamically. No cgo, no CGO_ENABLED.
- Pure Go: builds to a static binary; only requires a LibreOffice installation at runtime
- Covers the main LOKit API surface: document loading, tiled rendering, PDF/PNG export, text/graphic selection, clipboard, commands (
PostUnoCommand), multi-view, event callbacks, and more - Native event callback support (
RegisterCallback) with a fullCallbackTypeenum andString()descriptions - No cgo: cross-compiles anywhere, with runtime dynamic loading of the shared library
- Go >= 1.24
- LibreOffice 7.x (developed and verified against Linux with LibreOffice 7.3), standard install path
/usr/lib/libreoffice/program, overridable viaNewOffice(installPath)
go get github.com/docgo/libreofficekitLoad a document and render the first page to PNG:
package main
import (
"fmt"
"image"
"image/png"
"os"
"github.com/docgo/libreofficekit"
)
func main() {
office, err := libreofficekit.NewOffice("/usr/lib/libreoffice/program")
if err != nil {
panic(err)
}
defer office.Destroy()
doc, err := office.DocumentLoad("file:///path/to/doc.docx")
if err != nil {
panic(err)
}
defer doc.Destroy()
doc.InitializeForRendering("")
w, h := doc.GetDocumentSize() // twips
buf := make([]byte, w*h*4)
doc.PaintTile(buf, int(w), int(h), 0, 0, int(w), int(h)) // 4 bytes per pixel
if doc.GetTileMode() == libreofficekit.TILEMODE_BGRA {
// PaintTile returns BGRA on most builds; convert to RGBA.
for i := 0; i < len(buf); i += 4 {
buf[i], buf[i+2] = buf[i+2], buf[i]
}
}
img := image.NewRGBA(image.Rect(0, 0, int(w), int(h)))
copy(img.Pix, buf)
f, _ := os.Create("page.png")
defer f.Close()
png.Encode(f, img)
fmt.Println("rendered", w, "x", h)
}Run it (important — see Concurrency model below):
GODEBUG=asyncpreemptoff=1 go run main.goA complete runnable example lives in test/main.go (loads test/test.pptx and renders the first page).
Register a callback to be notified about document events (selection changes, tile invalidations, state changes, ...):
doc.RegisterCallback(func(typ libreofficekit.CallbackType, payload string, viewID int) {
switch typ {
case libreofficekit.CALLBACK_INVALIDATE_TILES:
// re-render the affected area
case libreofficekit.CALLBACK_TEXT_SELECTION:
// payload is the selection rectangle list in JSON
}
})Never call any LOKit API from inside a callback — see Concurrency model.
The full API is documented on pkg.go.dev. In short:
| Method | Description |
|---|---|
NewOffice(installPath) |
Initialize LOKit (once per process) |
DocumentLoad(url) / DocumentLoadWithOptions(url, options) |
Load a document (returns Document, see GetError on failure) |
GetVersionInfo() / GetFilterTypes() |
Version and filter information |
RegisterCallback(cb) |
Register global callbacks (status indicator, password prompt, ...) |
SetOptionalFeatures / SetOption / SetDocumentPassword |
Feature flags and options |
RunMacro / SignDocument / RunLoop(poll, wake) |
Macros, signing, event-loop mode |
GetError() |
Retrieve the last underlying error |
- Parts/rendering:
GetParts,SetPart/GetPart,GetPartName,GetPartPageRectangles,GetDocumentSize,SetClientZoom,PaintTile,PaintPartTile,PaintWindow(DPI),RenderFont,RenderShapeSelection - Export:
SaveAs(url, format, filterOptions)(e.g."pdf") - Selection/text:
SetTextSelection,GetTextSelection(mimeType),ResetSelection,SetGraphicSelection,GetSelectionType,Paste(mimeType, data) - Clipboard:
GetClipboard(requested []string)/SetClipboard(data)(ClipboardData{MimeType, Data}) - Input/commands:
PostKeyEvent,PostMouseEvent,PostUnoCommand(command, args, notify),GetCommandValues - Views:
CreateView(WithOptions),SetView,DestroyView,GetViewsCount,SetViewLanguage,SelectPart,MoveSelectedParts - Signatures/certificates:
GetSignatureState,InsertCertificate,AddCertificate - Callbacks:
RegisterCallback(per document)
Callback payloads are JSON/text produced by LibreOffice, one per CallbackType (see enums.go).
The LibreOfficeKit C API is not thread-safe, and every call is serialized by LibreOffice's global SolarMutex. Measured on LO 7.3:
GODEBUG=asyncpreemptoff=1is mandatory: LibreOffice installs its own signal handlers withoutSA_ONSTACK; Go 1.24's preemption signal (SIGURG) then aborts withnon-Go code set up signal handler without SA_ONSTACK. This applies to every usage, with or without callbacks.- Concurrent calls from multiple goroutines in one process (concurrent
DocumentLoad, operating on differentDocumentobjects) are safe (without callbacks), but give no speedup: SolarMutex serializes everything (measured slightly slower than sequential). - Callbacks fire on LibreOffice-internal threads (foreign threads, not Go goroutines): keep handlers to enqueueing/locked state updates only, and never call LOKit APIs from them (the callback thread may hold SolarMutex — re-entrancy deadlocks).
- True parallelism requires multiple processes: one
Officeper process (usecapi.LokInit2(installPath, userProfileURL)with a distinct user profile per process to avoid profile-lock contention), kept warm in a process pool to amortize the ~400 ms Office cold start. - Recommended single-process pattern: one goroutine driving the whole
Officeserially (see theverify/main.goharness).
verify/ contains an end-to-end integration check: 24 samples (docx/xlsx/pptx) compared against an independent oracle covering text selection, rendering, export and clipboard round-trips:
go build -o out/verify ./verify
GODEBUG=asyncpreemptoff=1 ./out/verify
python3 verify/compare.pySee verify/README.md for details. Known LO 7.3 limitations (not binding bugs): comment callbacks are never emitted, Impress has no text selection, Calc multi-sheet text selection does not switch sheets, headless clipboard read-back returns nothing.
- Requires
github.com/goexlib/cgo >= v0.1.0(in v0.0.9CStringreturned Go heap memory and crashed onFree; fixed in v0.1.0) - LO 7.3 crashes when a second view is created on a second document (LO bug); prefer one view per document
Contributions are welcome. Please:
- Run
gofmt -l .andgo build ./...before submitting. - Keep the API style consistent with the existing binding (see
document.go/office.gofor the doc-comment convention).