Current section
Files
Jump to
Current section
Files
lib/urp.ex
defmodule URP do
@moduledoc """
Pure Elixir client for document conversion via LibreOffice's UNO Remote Protocol.
Talks directly to a `soffice` process over TCP.
No Python, no unoserver, no Gotenberg.
## Setup
Add `:urp` to your dependencies — that's it. A default connection pool
starts automatically, connecting to `localhost:2002`.
# config/runtime.exs (optional — defaults shown)
config :urp, :default,
host: "soffice",
port: 2002,
pool_size: 1
## Usage
{:ok, pdf_bytes} = URP.convert_stream(docx_bytes)
{:ok, pdf_bytes} = URP.convert_file_stream("/path/to/input.docx")
:ok = URP.convert_stream(docx_bytes, sink: {:path, "/tmp/out.pdf"})
## Named pools
For multiple soffice instances, configure named pools:
config :urp, :pools,
spreadsheets: [host: "soffice-2", port: 2002, pool_size: 3]
{:ok, pdf} = URP.convert_stream(bytes, pool: :spreadsheets)
Named pools are started on first use.
## Testing
URP.Test.stub(fn _input, _opts -> {:ok, "fake pdf"} end)
{:ok, "fake pdf"} = URP.convert_stream(docx_bytes)
See `URP.Test` for details.
"""
@type sink :: {:path, Path.t()} | (binary() -> any())
@type opt ::
{:pool, atom()}
| {:filter, String.t()}
| {:sink, sink()}
| {:timeout, non_neg_integer()}
@doc """
Convert document bytes to PDF via streaming.
## Options
* `:pool` — named pool to use (default: `URP.Pool.Default`)
* `:filter` — export filter name (default `"writer_pdf_Export"`)
* `:sink` — output destination: `{:path, path}` or `fun/1` (default: in-memory)
* `:timeout` — checkout timeout in ms (default `120_000`)
"""
@spec convert_stream(binary(), [opt()]) :: {:ok, binary()} | :ok | {:error, String.t()}
def convert_stream(input_bytes, opts \\ []) when is_binary(input_bytes) do
case URP.Test.__fetch_stub__() do
{:ok, fun} ->
fun.(input_bytes, opts)
:error ->
{pool, opts} = resolve_pool(opts)
URP.Pool.convert_stream(pool, input_bytes, opts)
end
end
@doc """
Convert a local file to PDF via file-backed streaming.
Accepts the same options as `convert_stream/2`.
"""
@spec convert_file_stream(Path.t(), [opt()]) :: {:ok, binary()} | :ok | {:error, String.t()}
def convert_file_stream(input_path, opts \\ []) when is_binary(input_path) do
case URP.Test.__fetch_stub__() do
{:ok, fun} ->
fun.(input_path, opts)
:error ->
{pool, opts} = resolve_pool(opts)
URP.Pool.convert_file_stream(pool, input_path, opts)
end
end
@doc """
Convert a file to PDF via file:// URLs (requires shared filesystem).
## Options
* `:pool` — named pool to use (default: `URP.Pool.Default`)
* `:filter` — export filter name (default `"writer_pdf_Export"`)
* `:timeout` — checkout timeout in ms (default `120_000`)
"""
@spec convert(Path.t(), Path.t() | nil, [opt()]) ::
{:ok, Path.t()} | {:error, String.t()}
def convert(input_path, output_path \\ nil, opts \\ []) do
case URP.Test.__fetch_stub__() do
{:ok, fun} ->
fun.(input_path, opts)
:error ->
{pool, opts} = resolve_pool(opts)
URP.Pool.convert(pool, input_path, output_path, opts)
end
end
defp resolve_pool(opts) do
{pool_name, opts} = Keyword.pop(opts, :pool)
pool =
if pool_name do
ensure_pool!(pool_name)
else
URP.Pool.Default
end
{pool, opts}
end
@doc false
def ensure_pool!(name) do
pid_name = pool_process_name(name)
case GenServer.whereis(pid_name) do
pid when is_pid(pid) ->
pid_name
nil ->
pools = Application.get_env(:urp, :pools, [])
case Keyword.fetch(pools, name) do
{:ok, config} ->
opts = [
name: pid_name,
host: Keyword.get(config, :host, "localhost"),
port: Keyword.get(config, :port, 2002),
pool_size: Keyword.get(config, :pool_size, 1)
]
case DynamicSupervisor.start_child(URP.PoolSupervisor, {URP.Pool, opts}) do
{:ok, _pid} -> pid_name
{:error, {:already_started, _pid}} -> pid_name
end
:error ->
raise ArgumentError,
"pool #{inspect(name)} is not configured. " <>
"Add it to config :urp, :pools, #{name}: [host: \"...\", port: 2002]"
end
end
end
defp pool_process_name(name), do: :"URP.Pool.#{name}"
end