├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── test ├── test_helper.exs └── imgex_test.exs ├── config ├── config.exs └── test.exs ├── .formatter.exs ├── .gitlab-ci.yml ├── .gitignore ├── mix.exs ├── CHANGELOG.md ├── LICENSE.md ├── mix.lock ├── README.md └── lib └── imgex.ex /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ianwalter 2 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | if Mix.env() === :test, do: import_config("test.exs") 4 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :imgex, 4 | imgix_domain: "https://my-social-network.imgix.net", 5 | secure_token: "FOO123bar" 6 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: elixir:latest 2 | 3 | test: 4 | stage: test 5 | script: 6 | - mix local.hex --force 7 | - mix deps.get --only test 8 | - mix test 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | container: 7 | image: elixir 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@master 11 | - name: Install Hex 12 | run: mix local.hex --force 13 | - name: Install Dependencies 14 | run: mix deps.get --only test 15 | - name: Test 16 | run: mix test 17 | -------------------------------------------------------------------------------- /.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 | imgex-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | 28 | # macOS 29 | .DS_Store 30 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Imgex.Mixfile do 2 | use Mix.Project 3 | 4 | @github_url "https://github.com/ianwalter/imgex" 5 | @version "0.3.0" 6 | 7 | defp package do 8 | [ 9 | description: "An Elixir client library for generating image URLs with imgix", 10 | files: ["lib", "mix.exs", "README.md", "LICENSE.md"], 11 | maintainers: ["Ian Walter"], 12 | licenses: ["ISC"], 13 | links: %{ 14 | "GitHub" => @github_url 15 | } 16 | ] 17 | end 18 | 19 | def project do 20 | [ 21 | app: :imgex, 22 | version: @version, 23 | elixir: "~> 1.9", 24 | build_embedded: Mix.env() == :prod, 25 | start_permanent: Mix.env() == :prod, 26 | package: package(), 27 | deps: deps(), 28 | docs: docs() 29 | ] 30 | end 31 | 32 | def application do 33 | [extra_applications: [:logger]] 34 | end 35 | 36 | defp deps do 37 | [ 38 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false} 39 | ] 40 | end 41 | 42 | defp docs do 43 | [ 44 | extras: [ 45 | "LICENSE.md": [title: "License"], 46 | "README.md": [title: "Overview"] 47 | ], 48 | main: "readme", 49 | source_url: @github_url, 50 | source_ref: "v#{@version}", 51 | formatters: ["html"] 52 | ] 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changes to imgex 2 | 3 | ## Unreleased 4 | 5 | * [#15 Fix compilation warnings and update deps](https://github.com/ianwalter/imgex/pull/15) 6 | * [#17 Switch to using keyword lists internally](https://github.com/ianwalter/imgex/pull/17) 7 | 8 | ## v0.3.0 9 | 10 | * [#8 Add Imgex.srcset to generate srcset pairs](https://github.com/ianwalter/imgex/pull/8) 11 | * [#11 docs: update README for imgix docs](https://github.com/ianwalter/imgex/pull/11) 12 | * [#12 Doc/Readme Updates](https://github.com/ianwalter/imgex/pull/12) 13 | 14 | ## v0.2.0 15 | 16 | * [#6: Changing main repo to gitlab.recursive.run/ianwalter/imgex](https://gitlab.recursive.run/ianwalter/imgex/merge_requests/6) 17 | * [#7: Special-case handling of empty map](https://github.com/ianwalter/imgex/pull/7) 18 | 19 | **Breaking Changes:** 20 | 21 | * `Imgex.proxy_url/3` and `Imgex.url/3` no longer support params with value 22 | `nil`. Instead either don't pass the argument or pass in an empty map (`%{}`) 23 | 24 | ## v0.1.1 25 | * [#4: Extract path generation to remove Elixir 1.3 warning](https://github.com/ianwalter/imgex/issues/4) 26 | * [#5: Fix Elixir 1.4 missing parentheses warnings](https://github.com/ianwalter/imgex/issues/5) 27 | 28 | ## v0.1.0 29 | * [#1: Add functions to generate URLs from other sources.](https://github.com/ianwalter/imgex/issues/1) 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # ISC License 2 | 3 | Copyright (c) 2019 Ian Walter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | * The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | * The software may not be used by individuals, corporations, governments, or 16 | other groups for systems or activities that actively and knowingly endanger, 17 | harm, or otherwise threaten the physical, mental, economic, or general 18 | well-being of individuals or groups in violation of the United Nations 19 | Universal Declaration of Human Rights 20 | (https://www.un.org/en/universal-declaration-human-rights/). 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 24 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 25 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 26 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 27 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | 29 | This license is derived from the MIT License, as amended to limit the impact of 30 | the unethical use of open source software. 31 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"}, 3 | "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, 4 | "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, 5 | "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, 6 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, 7 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, 8 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # imgex 3 | > An Elixir client library for generating image URLs with imgix 4 | 5 | [![CI](https://github.com/ianwalter/imgex/actions/workflows/ci.yml/badge.svg)](https://github.com/ianwalter/imgex/actions/workflows/ci.yml) 6 | [![Module Version](https://img.shields.io/hexpm/v/imgex.svg)](https://hex.pm/packages/imgex) 7 | [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/imgex/) 8 | [![Total Download](https://img.shields.io/hexpm/dt/imgex.svg)](https://hex.pm/packages/imgex) 9 | [![License](https://img.shields.io/hexpm/l/imgex.svg)](https://github.com/ianwalter/imgex/blob/master/LICENSE.md) 10 | [![Last Updated](https://img.shields.io/github/last-commit/ianwalter/imgex.svg)](https://github.com/ianwalter/imgex/commits/master) 11 | 12 | 13 | 14 | - [Installation](#installation) 15 | - [Documentation](#documentation) 16 | - [Configuration](#configuration) 17 | - [Usage](#usage) 18 | 19 | ## Installation 20 | 21 | imgex is [available in Hex](https://hex.pm/packages/imgex), the package can be 22 | installed as: 23 | 24 | Add `:imgex` to your list of dependencies in `mix.exs`: 25 | 26 | ```elixir 27 | def deps do 28 | [ 29 | {:imgex, "~> 0.2.0"}, 30 | ] 31 | end 32 | ``` 33 | 34 | ## Documentation 35 | 36 | The source is really small so reading through it should be straight-forward but 37 | the full package documentation is available at https://hexdocs.pm/imgex. 38 | 39 | ## Configuration 40 | 41 | To use the library you have to configure your imgix domain and secure token or 42 | pass them as an options map `%{domain: "domain", token: "token"}` as the 43 | third parameter to `Imgex.url/3` or `Imgex.proxy_url/3`. 44 | See `config/test.exs` for an example of how to configure this. 45 | 46 | ## Usage 47 | 48 | To generate an imgix URL based on a path (Web Folder and S3 sources) and 49 | optional parameters do: 50 | 51 | ```elixir 52 | url = Imgex.url "/images/cats.jpg", %{w: 700} 53 | ``` 54 | 55 | To generate an imgix URL based on a public URL (Web Proxy sources) and optional 56 | parameters do: 57 | 58 | ```elixir 59 | url = Imgex.proxy_url "https://some-public-url.com/cats.jpg", %{w: 700} 60 | ``` 61 | 62 | ## Copyright and License 63 | 64 | Copyright (c) 2019 [Ian Walter](https://ianwalter.dev) 65 | 66 | This work is free. You can redistribute it and/or modify it under the 67 | terms of the ISC License. See the [LICENSE.md](./LICENSE.md) file for more details. 68 | -------------------------------------------------------------------------------- /test/imgex_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ImgexTest do 2 | use ExUnit.Case 3 | doctest Imgex 4 | 5 | test "proxy_url/2 when params are an empty map generates an appropriate url" do 6 | assert Imgex.proxy_url("http://avatars.com/john-smith.png", %{}) == 7 | "https://my-social-network.imgix.net/http%3A%2F%2Favatars.com%2Fjohn-smith.png?s=493a52f008c91416351f8b33d4883135" 8 | end 9 | 10 | test "url/2 when params are an empty map generates an appropriate url" do 11 | assert Imgex.url("/images/jets.png", %{}) == 12 | "https://my-social-network.imgix.net/images/jets.png?s=7c6a3ef8679f4965f5aaecb66547fa61" 13 | end 14 | 15 | test "url/2 supports a map params are an empty map generates an appropriate url" do 16 | url = Imgex.url("/images/jets.png", %{fm: "auto", h: 100}) 17 | uri = URI.new!(url) 18 | 19 | assert %{"fm" => "auto", "h" => "100"} = URI.decode_query(uri.query) 20 | end 21 | 22 | describe "srcset/3" do 23 | @default_srcset_widths ~w( 24 | 100 116 134 156 182 210 244 282 328 380 442 512 594 688 798 926 1074 25 | 1246 1446 1678 1946 2258 2618 3038 3524 4088 4742 5500 6380 7400 8192 26 | ) |> Enum.map(&Integer.parse/1) |> Enum.map(fn {val, ""} -> val end) 27 | 28 | test "by default, generates 31 width pairs" do 29 | path = "/images/lulu.jpg" 30 | srcset = Imgex.srcset(path) 31 | split = String.split(srcset, ",\n") 32 | assert length(split) == 31 33 | 34 | @default_srcset_widths 35 | |> Enum.with_index() 36 | |> Enum.each(fn {width, i} -> 37 | src = Enum.at(split, i) 38 | assert src == "#{Imgex.url(path, %{w: width})} #{width}w" 39 | end) 40 | end 41 | 42 | test "with only a height, generates 31 width pairs" do 43 | path = "/images/lulu.jpg" 44 | 45 | srcset = Imgex.srcset(path, h: 100) 46 | split = String.split(srcset, ",\n") 47 | assert length(split) == 31 48 | 49 | @default_srcset_widths 50 | |> Enum.with_index() 51 | |> Enum.each(fn {width, i} -> 52 | src = Enum.at(split, i) 53 | assert src == "#{Imgex.url(path, w: width, h: 100)} #{width}w" 54 | end) 55 | end 56 | 57 | test "with a height and aspect ratio, generates 5 dpr pairs" do 58 | path = "/images/lulu.jpg" 59 | srcset = Imgex.srcset(path, %{ar: "3:4", h: 100}) 60 | split = String.split(srcset, ",\n") 61 | assert length(split) == 5 62 | 63 | [1, 2, 3, 4, 5] 64 | |> Enum.each(fn dpr -> 65 | src = Enum.at(split, dpr - 1) 66 | assert src == "#{Imgex.url(path, dpr: dpr, h: 100, ar: "3:4")} #{dpr}x" 67 | end) 68 | end 69 | 70 | test "with a height, aspect ratio, and other params, generates 5 dpr pairs" do 71 | path = "/images/lulu.jpg" 72 | params = [ar: "3:4", crop: "faces,entropy,left", h: 100] 73 | srcset = Imgex.srcset(path, params) 74 | split = String.split(srcset, ",\n") 75 | assert length(split) == 5 76 | 77 | [1, 2, 3, 4, 5] 78 | |> Enum.each(fn dpr -> 79 | src = Enum.at(split, dpr - 1) 80 | assert src == "#{Imgex.url(path, Keyword.put(params, :dpr, dpr))} #{dpr}x" 81 | end) 82 | end 83 | 84 | test "with only a width, generates 5 dpr pairs" do 85 | path = "/images/lulu.jpg" 86 | srcset = Imgex.srcset(path, %{w: 100}) 87 | split = String.split(srcset, ",\n") 88 | assert length(split) == 5 89 | 90 | [1, 2, 3, 4, 5] 91 | |> Enum.each(fn dpr -> 92 | src = Enum.at(split, dpr - 1) 93 | assert src == "#{Imgex.url(path, dpr: dpr, w: 100)} #{dpr}x" 94 | end) 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/imgex.ex: -------------------------------------------------------------------------------- 1 | defmodule Imgex do 2 | @moduledoc """ 3 | Provides functions to generate secure Imgix URLs. 4 | """ 5 | 6 | @doc """ 7 | Provides configured source information when it's not passed explicitly to 8 | `url/3` or `proxy_url/3`. 9 | """ 10 | def configured_source, 11 | do: %{ 12 | token: Application.get_env(:imgex, :secure_token), 13 | domain: Application.get_env(:imgex, :imgix_domain) 14 | } 15 | 16 | @doc """ 17 | Generates a secure Imgix URL from a Web Proxy source given: 18 | * `path` - The full public image URL. 19 | * `params` - (optional) A map containing Imgix API parameters used to manipulate the image. 20 | * `source` - (optional) A map containing Imgix source information: 21 | * `:token` - The secure token used to sign API requests. 22 | * `:domain` - The Imgix source domain. 23 | 24 | ## Examples 25 | 26 | iex> Imgex.proxy_url "http://avatars.com/john-smith.png" 27 | "https://my-social-network.imgix.net/http%3A%2F%2Favatars.com%2Fjohn-smith.png?s=493a52f008c91416351f8b33d4883135" 28 | iex> Imgex.proxy_url "http://avatars.com/john-smith.png", %{w: 400, h: 300} 29 | "https://my-social-network.imgix.net/http%3A%2F%2Favatars.com%2Fjohn-smith.png?h=300&w=400&s=a201fe1a3caef4944dcb40f6ce99e746" 30 | 31 | """ 32 | def proxy_url(path, params \\ %{}, source \\ configured_source()) when is_map(params) do 33 | # URI-encode the public URL. 34 | path = "/" <> URI.encode(path, &URI.char_unreserved?/1) 35 | 36 | # Return the generated URL. 37 | url(path, params, source) 38 | end 39 | 40 | @doc """ 41 | Generates custom `srcset` attributes for Imgix URLs, pre-signed with the 42 | source's secure token (if configured). These are useful for responsive 43 | images at a variety of screen sizes or pixel densities. 44 | 45 | By default, the `srcset` generated will allow for responsive size switching 46 | by building a list of image-width mappings. This default list of image 47 | width mappings covers a wide range of 31 different widths from 100px to 48 | 8192px. This default results in quite a long `srcset` attribute, though, so 49 | it is preferable to instead give more specific size guidance by specifying 50 | either: 51 | 52 | * a width, _OR_ 53 | * a height + aspect ratio 54 | 55 | When these params are provided, the returned `srcset` will cover 5 56 | different pixel densities (1x-5x). 57 | 58 | ## Arguments 59 | 60 | * `path` - The URL path to the image. 61 | * `params` - (optional) A map containing Imgix API parameters used to manipulate the image. 62 | * `source` - (optional) A map containing Imgix source information: 63 | * `:token` - The secure token used to sign API requests. 64 | * `:domain` - The Imgix source domain. 65 | 66 | ## Examples 67 | 68 | iex> Imgex.srcset("/images/lulu.jpg", %{w: 100}) 69 | "https://my-social-network.imgix.net/images/lulu.jpg?dpr=1&w=100&s=9bd210f344a0f65032951a9cf171c40e 1x, 70 | https://my-social-network.imgix.net/images/lulu.jpg?dpr=2&w=100&s=33520b8f84fa72afa28539d66fb2734f 2x, 71 | https://my-social-network.imgix.net/images/lulu.jpg?dpr=3&w=100&s=97d0f1731b4c8d8dd609424dfca2eab5 3x, 72 | https://my-social-network.imgix.net/images/lulu.jpg?dpr=4&w=100&s=b96a02e08eeb50df5a75223c998e46f5 4x, 73 | https://my-social-network.imgix.net/images/lulu.jpg?dpr=5&w=100&s=9ba1ab37db9f09283d9194223fbafb2f 5x" 74 | iex> Imgex.srcset("/images/lulu.jpg", ar: "3:4", h: 500) 75 | "https://my-social-network.imgix.net/images/lulu.jpg?dpr=1&ar=3%3A4&h=500&s=842a70d9c7ead7417b4a8056f45a88b3 1x, 76 | https://my-social-network.imgix.net/images/lulu.jpg?dpr=2&ar=3%3A4&h=500&s=7cce91f2cd0d2bd1d252ca241523c09b 2x, 77 | https://my-social-network.imgix.net/images/lulu.jpg?dpr=3&ar=3%3A4&h=500&s=509e0045d21a08324811d2db978c874c 3x, 78 | https://my-social-network.imgix.net/images/lulu.jpg?dpr=4&ar=3%3A4&h=500&s=cc5790442b6185768435a48a44e040c9 4x, 79 | https://my-social-network.imgix.net/images/lulu.jpg?dpr=5&ar=3%3A4&h=500&s=cf724f11656961377da13f8608c60b4a 5x" 80 | """ 81 | def srcset( 82 | path, 83 | params \\ [], 84 | source \\ configured_source() 85 | ) 86 | when (is_list(params) or is_map(params)) do 87 | params = to_list(params) 88 | width = params[:w] 89 | height = params[:h] 90 | aspect_ratio = params[:ar] 91 | 92 | if width || (height && aspect_ratio) do 93 | build_dpr_srcset(path, params, source) 94 | else 95 | build_srcset_pairs(path, params, source) 96 | end 97 | end 98 | 99 | @doc """ 100 | Generates a secure Imgix URL given: 101 | * `path` - The URL path to the image. 102 | * `params` - (optional) A map containing Imgix API parameters used to manipulate the image. 103 | * `source` - (optional) A map containing Imgix source information: 104 | * `:token` - The secure token used to sign API requests. 105 | * `:domain` - The Imgix source domain. 106 | 107 | ## Examples 108 | 109 | iex> Imgex.url "/images/jets.png" 110 | "https://my-social-network.imgix.net/images/jets.png?s=7c6a3ef8679f4965f5aaecb66547fa61" 111 | iex> Imgex.url "/images/jets.png", %{con: 10}, %{domain: "https://cannonball.imgix.net", token: "xxx187xxx"} 112 | "https://cannonball.imgix.net/images/jets.png?con=10&s=d982f04bbca4d819971496524aa5f95a" 113 | 114 | """ 115 | def url(path, params \\ [], source \\ configured_source()) when (is_map(params) or is_list(params)) do 116 | params = to_list(params) 117 | # Add query parameters to the path. 118 | path = path_with_params(path, params) 119 | 120 | # Use a md5 hash of the path and secret token as a signature. 121 | signature = Base.encode16(:erlang.md5(source.token <> path), case: :lower) 122 | 123 | # Append the signature to verify the request is valid and return the URL. 124 | if params == [] do 125 | source.domain <> path <> "?s=" <> signature 126 | else 127 | source.domain <> path <> "&s=" <> signature 128 | end 129 | end 130 | 131 | defp to_list(params) when is_list(params), do: params 132 | defp to_list(params) when is_map(params), do: Map.to_list(params) 133 | 134 | defp path_with_params(path, params) when params == [], do: path 135 | 136 | defp path_with_params(path, params) when is_list(params) do 137 | path <> "?" <> URI.encode_query(params) 138 | end 139 | 140 | # Default set of target widths, borrowed from JS and Ruby Imgix libraries. 141 | @default_srcset_target_widths [ 142 | 100, 143 | 116, 144 | 134, 145 | 156, 146 | 182, 147 | 210, 148 | 244, 149 | 282, 150 | 328, 151 | 380, 152 | 442, 153 | 512, 154 | 594, 155 | 688, 156 | 798, 157 | 926, 158 | 1074, 159 | 1246, 160 | 1446, 161 | 1678, 162 | 1946, 163 | 2258, 164 | 2618, 165 | 3038, 166 | 3524, 167 | 4088, 168 | 4742, 169 | 5500, 170 | 6380, 171 | 7400, 172 | 8192 173 | ] 174 | 175 | @default_srcset_target_ratios [1, 2, 3, 4, 5] 176 | 177 | defp build_srcset_pairs(path, params, source) when is_list(params) do 178 | @default_srcset_target_widths 179 | |> Enum.map(fn width -> 180 | updated_params = Keyword.put(params, :w, width) 181 | url(path, updated_params, source) <> " #{width}w" 182 | end) 183 | |> Enum.join(",\n") 184 | end 185 | 186 | defp build_dpr_srcset(path, params, source) when is_list(params) do 187 | Enum.map(@default_srcset_target_ratios, fn ratio -> 188 | updated_params = Keyword.put(params, :dpr, ratio) 189 | url(path, updated_params, source) <> " #{ratio}x" 190 | end) 191 | |> Enum.join(",\n") 192 | end 193 | end 194 | --------------------------------------------------------------------------------