├── .formatter.exs ├── .github └── workflows │ ├── CODEOWNERS │ └── pipeline.yml ├── .gitignore ├── .tool-versions ├── README.md ├── lib ├── gotenberg_adapter.ex ├── pdf_generator.ex └── utils │ └── casing.ex ├── mix.exs ├── mix.lock └── test ├── pdf_generator_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/workflows/CODEOWNERS: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rum-and-code/elixir-pdf-generator/572ff68d76f351973d9f8ce46a1972a4652c45d3/.github/workflows/CODEOWNERS -------------------------------------------------------------------------------- /.github/workflows/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: CI pipeline 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | ci-pipeline: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | otp_version: ['24.3', '25.3'] 15 | elixir_version: ['1.13', '1.14'] 16 | 17 | name: CI pipeline (Elixir ${{matrix.elixir_version}} / OTP ${{matrix.otp_version}} ) 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: erlef/setup-beam@v1 21 | with: 22 | otp-version: ${{matrix.otp_version}} 23 | elixir-version: ${{matrix.elixir_version}} 24 | - name: Restore mix dependencies cache 25 | uses: actions/cache@v2 26 | with: 27 | path: | 28 | deps 29 | _build 30 | key: ${{ runner.os }}-${{matrix.elixir_version}}-${{matrix.otp_version}}-mix-${{ hashFiles('mix.lock') }} 31 | - run: | 32 | mix deps.get 33 | mix pipeline 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | pdf_generator-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | erlang 25.3 2 | elixir 1.14.4-otp-25 3 | nodejs 16.16.0 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ⚠️ BRANCH MAIN IS DEPRECATED ⚠️ 2 | Change your mix file to use a specific tag instead 3 | # Elixir Pdf Generator 4 | 5 | ## Installation 6 | 7 | ```elixir 8 | def deps do 9 | [ 10 | {:elixir_pdf_generator, git: "git@github.com:rum-and-code/elixir-pdf-generator.git", tag: "v0.1.2"} 11 | ] 12 | end 13 | ``` 14 | Once you've added PdfGenerator to your list, update your dependencies by running: 15 | 16 | ``` 17 | $ mix deps.get 18 | ``` 19 | 20 | ## Configuration 21 | This lib is just a tiny wrapper to call our Gotenburg service. But it is build to accept different adapter if we change service one day. 22 | 23 | Here are the config needed for the Gotenberg Adapter. 24 | ```elixir 25 | config :pdf_generator, 26 | adapter: PdfGenerator.GotenbergAdapter, #Defaults to GotenbergAdapter if not specified. 27 | host_url: "http://your-app.com", 28 | pdf_generator_url: "http://your-gotenburg-service.com" 29 | ``` 30 | 31 | -------------------------------------------------------------------------------- /lib/gotenberg_adapter.ex: -------------------------------------------------------------------------------- 1 | defmodule PdfGenerator.GotenbergAdapter do 2 | @moduledoc """ 3 | This module provides an adapter for the Gotenberg API. 4 | """ 5 | 6 | @behaviour PdfGenerator 7 | @type path :: String.t() 8 | @type options :: [Keyword.t()] 9 | @type request_options :: [Keyword.t()] 10 | 11 | defp host_url, do: Application.fetch_env!(:pdf_generator, :host_url) 12 | defp pdf_generator_url, do: Application.fetch_env!(:pdf_generator, :pdf_generator_url) 13 | 14 | @impl PdfGenerator 15 | @doc """ 16 | Converts a path to a PDF. 17 | 18 | ## Examples 19 | 20 | iex> PdfGenerator.convert_path_to_pdf("/path/to/template") 21 | {:ok, "PDF content"} 22 | 23 | ## Options 24 | Refer to https://gotenberg.dev/docs/modules/chromium for a list of available options. 25 | 26 | By default, we use the option `prefer_css_page_size: true`. This allow us to use the @page css rule to define the page size like so 27 | ```css 28 | @page { 29 | size: letter landscape; 30 | } 31 | ``` 32 | """ 33 | @spec convert_path_to_pdf(path(), options(), request_options()) :: 34 | {:ok, binary()} | {:error, any()} 35 | def convert_path_to_pdf(path, options, request_options) do 36 | url = host_url() <> path 37 | 38 | url 39 | |> build_body(options) 40 | |> build_request(request_options) 41 | |> HTTPoison.request() 42 | |> case do 43 | {:ok, response} -> {:ok, response.body} 44 | {:error, error} -> {:error, error} 45 | end 46 | end 47 | 48 | defp headers() do 49 | [ 50 | {"Content-Type", "multipart/form-data"} 51 | ] 52 | end 53 | 54 | defp build_body(url, options) do 55 | defaults = [ 56 | url: url, 57 | prefer_css_page_size: true 58 | ] 59 | 60 | defaults 61 | |> Keyword.merge(options) 62 | |> Enum.map(fn {key, value} -> 63 | {PdfGenerator.Utils.Casing.camelize(key), to_string(value)} 64 | end) 65 | end 66 | 67 | defp build_request(body, options) do 68 | %HTTPoison.Request{ 69 | method: :post, 70 | url: pdf_generator_url(), 71 | headers: headers(), 72 | body: {:multipart, body}, 73 | options: options 74 | } 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/pdf_generator.ex: -------------------------------------------------------------------------------- 1 | defmodule PdfGenerator do 2 | @moduledoc """ 3 | This module provides a simple interface to convert a path to a PDF. 4 | """ 5 | 6 | @type path :: String.t() 7 | @type options :: [Keyword.t()] 8 | @type request_options :: [Keyword.t()] 9 | @callback convert_path_to_pdf(path(), options(), request_options()) :: 10 | {:ok, binary()} | {:error, any()} 11 | 12 | @doc """ 13 | Converts a path to a PDF. 14 | 15 | For more information, refer to the documentation of the adapter you are using. 16 | """ 17 | @spec convert_path_to_pdf(path(), options(), request_options()) :: 18 | {:ok, binary()} | {:error, any()} 19 | def convert_path_to_pdf(path, options \\ [], request_options \\ []) do 20 | adapter().convert_path_to_pdf(path, options, request_options) 21 | end 22 | 23 | def adapter do 24 | Application.get_env(:pdf_generator, :adapter, PdfGenerator.GotenbergAdapter) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/utils/casing.ex: -------------------------------------------------------------------------------- 1 | defmodule PdfGenerator.Utils.Casing do 2 | @moduledoc """ 3 | This module provides various helpers to handle casing 4 | """ 5 | @camelize_regex ~r/(?:^|[-_])|(?=[A-Z][a-z])/ 6 | 7 | @doc """ 8 | Converts a string to camel case. 9 | 10 | ## Options 11 | * `:upper` - Uppercase the first letter. 12 | 13 | ## Examples 14 | iex> Casing.camelize("foo_bar") 15 | "fooBar" 16 | 17 | iex> Casing.camelize("foo_bar", :upper) 18 | "FooBar" 19 | """ 20 | 21 | def camelize(word, option \\ :lower) do 22 | case Regex.split(@camelize_regex, to_string(word)) do 23 | words -> 24 | words 25 | |> Enum.filter(&(&1 != "")) 26 | |> camelize_list(option) 27 | |> Enum.join() 28 | end 29 | end 30 | 31 | defp camelize_list([], _), do: [] 32 | 33 | defp camelize_list([h | tail], :lower) do 34 | [String.downcase(h)] ++ camelize_list(tail, :upper) 35 | end 36 | 37 | defp camelize_list([h | tail], :upper) do 38 | [String.capitalize(h)] ++ camelize_list(tail, :upper) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule PdfGenerator.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :pdf_generator, 7 | version: "0.1.1", 8 | elixir: "~> 1.13", 9 | start_permanent: Mix.env() == :prod, 10 | aliases: aliases(), 11 | deps: deps(), 12 | preferred_cli_env: [pipeline: :test] 13 | ] 14 | end 15 | 16 | # Run "mix help compile.app" to learn about applications. 17 | def application do 18 | [ 19 | extra_applications: [:logger] 20 | ] 21 | end 22 | 23 | # Run "mix help deps" to learn about dependencies. 24 | defp deps do 25 | [ 26 | {:httpoison, "~> 1.0"}, 27 | {:credo, "~> 1.6", only: [:dev, :test], runtime: false} 28 | ] 29 | end 30 | 31 | defp aliases do 32 | [ 33 | pipeline: ["format --check-formatted", "compile --warnings-as-errors", "test", "credo"] 34 | ] 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, 3 | "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, 4 | "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, 5 | "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, 6 | "elixir_utils": {:git, "git@github.com:rum-and-code/elixir-utils.git", "00443e70492e16e659d74d549deea5d85737ed1c", []}, 7 | "expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"}, 8 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 9 | "gettext": {:hex, :gettext, "0.22.1", "e7942988383c3d9eed4bdc22fc63e712b655ae94a672a27e4900e3d4a2c43581", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "ad105b8dab668ee3f90c0d3d94ba75e9aead27a62495c101d94f2657a190ac5d"}, 10 | "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, 11 | "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, 12 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 13 | "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, 14 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 15 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 16 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 17 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, 18 | "timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"}, 19 | "tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"}, 20 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 21 | } 22 | -------------------------------------------------------------------------------- /test/pdf_generator_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PdfGeneratorTest do 2 | use ExUnit.Case 3 | doctest PdfGenerator 4 | end 5 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------