URP

Hex.pmDocs

Pure Elixir client for the UNO Remote Protocol. Talks directly to a soffice process over a TCP socket — no Python, no wrappers, no sidecars.

Why?

Existing approaches to LibreOffice integration — unoserver, Gotenberg, Python UNO bindings — each add an intermediate layer with its own deployment complexity and failure modes.

URP speaks the binary protocol directly over TCP. Document conversion is the primary use case, but the same connection gives you access to soffice diagnostics — registered services, available export filters, document types, locale settings, and version info.

Installation

Add urp to your dependencies in mix.exs:

def deps do
  [
    {:urp, "~> 0.7"}
  ]
end

Prerequisites

A running soffice process with a URP socket listener. Build from benchmarks/Dockerfile.soffice-debian or use your own Debian/Ubuntu image with LibreOffice installed:

docker build --tag soffice --file benchmarks/Dockerfile.soffice-debian benchmarks/
docker run --detach --name soffice --publish 2002:2002 soffice

Note

Any image with soffice listening on a TCP socket works — including libreofficedocker/alpine. See PERFORMANCE.md for trade-offs.

Usage

A default pool connects to localhost:2002 automatically.

Document conversion

{:ok, pdf_path} =
  URP.convert("/path/to/input.docx",
    filter: "writer_pdf_Export",
    filter_data: [
      UseLosslessCompression: false,
      Quality: 90,
      ReduceImageResolution: true,
      MaxImageResolution: 150,
      ExportBookmarks: true,
      ExportFormFields: false
    ],
    output: "/tmp/output.pdf"
  )

Input can be a file path, {:binary, bytes}, or any Enumerable (e.g. File.stream!/2). Output defaults to a temp file path; pass an explicit path or output: :binary to get bytes in memory. See filter names for all formats (calc_pdf_Export, impress_pdf_Export, etc.) and FilterData properties for PDF export options.

Diagnostics

{:ok, "25.8.1.1"} = URP.version()
{:ok, services}   = URP.services()   # all registered UNO service names
{:ok, filters}    = URP.filters()    # available export filters
{:ok, types}      = URP.types()      # known document type names
{:ok, locale}     = URP.locale()     # soffice locale setting

Configuration

# config/runtime.exs
config :urp, :default,
  host: "soffice",
  port: 2002,
  pool_size: 1

Testing

Stub conversions in tests — no running soffice needed:

test "generates invoice PDF" do
  URP.Test.stub(fn _input, _opts ->
    {:ok, "/tmp/fake.pdf"}
  end)

  assert {:ok, _pdf} = MyApp.generate_invoice(order)
end

Stubs are per-process and propagate through $callers (Tasks, GenServers). See URP.Test for details.

Performance

See PERFORMANCE.md for benchmarks against Gotenberg and container image recommendations.

Scope

Implements document conversion and read-only soffice introspection via UNO. Export format is controlled by filter names (writer_pdf_Export, calc_pdf_Export, impress_pdf_Export, Markdown, etc.). Mutating UNO APIs (editing, formatting, macros) are not implemented.

Architecture

Module Role
URP Public API — convert, diagnostics, test stubs
URP.Bridge Mid-level — UNO operations (handshake, convert, diagnostics, streaming)
URP.Stream Bidirectional URP dispatch for XInputStream/XOutputStream
URP.Protocol Low-level — binary wire format (framing, encoding, reply parsing)

References

License

MIT — see LICENSE.

This is an independent implementation based on the public UNO protocol spec. LibreOffice source was consulted as documentation for protocol details not covered by the spec. No code was copied.