# `Fil.Plugin.Thumbnails`
[🔗](https://github.com/pehbehbeh/fil/blob/v0.2.0/lib/fil/plugin/thumbnails.ex#L1)

Writes smaller copies of images next to them, resized with [libvips](https://www.libvips.org).

    disk =
      Fil.disk(adapter: Fil.Adapter.S3, bucket: "photos")
      |> Fil.Plugin.Thumbnails.attach(variants: [small: [width: 200], square: [width: 96, crop: :attention]])

    Fil.write(disk, "cats/tom.jpg", jpeg)
    # also writes thumbnails/small/cats/tom.jpg and thumbnails/square/cats/tom.jpg

    disk |> Fil.Plugin.Thumbnails.variant("cats/tom.jpg", :small) |> Fil.url()

It needs the optional dependency [Vix](https://hex.pm/packages/vix), which downloads a precompiled libvips when it's
compiled:

    {:vix, "~> 0.33"}

Unlike `Fil.Plug`, which isn't compiled without Plug, the plugin is always there and looks Vix up when it's attached
or used. So a disk from config that lists the plugin raises with a message about Vix instead of a missing function,
and adding Vix later needs no recompile of `fil`.

It's also the example of a plugin that needs all of the content at once and writes files of its own (see the
[Plugins guide](plugins.md)).

## Variants

Every variant fits into its `:width` and `:height`, keeping the image's aspect ratio (or crops to exactly that size
with `:crop`). Images are never enlarged. A variant is rotated by the image's EXIF orientation and keeps only the
colour profile of the metadata, so no GPS position ends up in a public thumbnail. That needs libvips 8.15 or later
(Vix 0.42 comes with 8.18). On an older libvips, which would keep all of the metadata, the plugin raises instead of
running.

A write of an image (by its extension, see `:extensions`) makes every variant in memory, then writes the image, then
the variants, each as a `Fil.write/3` with its content type. The variant writes go through the disk's plugins too,
and this one passes them on because `:variant_path` gives them no variants. So `Fil.Telemetry` counts an image
write as one `:write` plus one per variant, nested in it (see
[Nested operations](Fil.Telemetry.html#module-nested-operations)). With `mode: :manual`, a write makes no variants,
and `generate/1` makes them later.

## Where variants go

`:variant_path` maps the path of an image to the path of each variant. The default, `prefixed/4` with `"thumbnails"`,
puts the variants of `cats/tom.jpg` at `thumbnails/small/cats/tom.jpg`, and a variant with `format: :webp` at
`thumbnails/square/cats/tom.jpg.webp`. So `tom.jpg` and `tom.png` never share a variant, and the content type still
follows from the extension. `{Fil.Plugin.Thumbnails, :prefixed, ["media/thumbs"]}` uses another directory.
`variant/2` returns the ref of a variant. The variants are ordinary files: read them, build their URLs, and expect
them in a recursive `Fil.ls/3`.

A function of your own can put the variants next to the image instead. This one stores an image as
`images/5a95_original_158.jpg` and its variants as `images/5a95_small_158.jpg`:

    defmodule MyApp.Images do
      def variant_path(path, variant, format) do
        case String.split(path, "_original_", parts: 2) do
          [id, rest] -> "#{id}_#{variant}_#{rest}" <> if(format, do: ".#{format}", else: "")
          [_not_an_original] -> nil
        end
      end
    end

    Fil.Plugin.Thumbnails.attach(disk,
      variants: [small: [width: 200]],
      variant_path: {MyApp.Images, :variant_path, []}
    )

The function returns `nil` for a path that gets no variants, and that has to include the variants' own paths, or the
plugin makes variants of variants. `Fil.rm_rf/1` calls it with the directory and a `nil` format: a path it returns,
such as `thumbnails/small/cats` in the default layout, is deleted with everything under it, and `nil` deletes nothing
more (variants next to their images go with the directory). A path outside the disk root, the root itself or the
image's own path raises `ArgumentError`.

## Deletes, copies and renames

Once an image is deleted, copied or renamed on its disk, the plugin does the same with its variants. `Fil.rm_rf/1`
deletes the variants under the path too. Its count is what the operation itself deleted, so variants only count
when they're in the deleted directory.

A copy or a rename to another extension doesn't fit the variants any more: a rename deletes them, and a copy leaves
the source's alone. Either deletes the variants the destination had, since they belong to the file it replaced. The
same goes for a variant the source doesn't have (an image written before the plugin was attached has none).

Across disks, `Fil` copies with a read and a write, so a disk with the plugin makes the variants of the copy itself,
and a rename deletes the source's variants with the source.

## Memory and order

libvips needs the whole image, so an image write collects its content into memory with `Fil.Op.materialize/1`: up
to the whole file per image written at the same time, plus libvips' working memory. `Fil.Plug` limits uploads with
`:max_body_size`. Every other write stays a stream.

Attach the plugin first, before plugins that rewrite paths or change content (compression, encryption), so it sees
the image as the caller wrote it. Its variant writes still go through those plugins. `Fil.Plugin.ContentType`
before it does no harm.

Two writes to the same path at the same time can leave the variants of one next to the image of the other.

## Errors

  * content libvips can't decode, or encode as a variant, is refused before anything is written, with a
    `Fil.InvalidRequestError` whose `:reason` is `{:not_an_image, message}`. The same goes for an image over
    `:max_pixels`, with `{:too_many_pixels, pixels}`
  * when the image can't be written, no variant is written either
  * when a variant can't be written, the image stays written and the write returns the variant's error, with the
    variant's path. The same goes for a variant that can't be deleted, copied or renamed

## Options

* `:variants` (non-empty `t:keyword/0`) - Required. The variants by name, such as `[small: [width: 200], square: [width: 96, crop: :attention]]`. A name
  goes into the variant's path, so it's lowercase letters, digits and underscores. Each variant takes:

  * `:width` (`t:pos_integer/0`) - Required. The largest width in pixels.

  * `:height` (`t:pos_integer/0`) - The largest height in pixels. Without it, only the width limits the size, and with `:crop` the
    variant is square.

  * `:crop` - Fills the width and the height exactly and cuts off the rest: `:center` keeps the middle,
    `:attention` the part libvips finds most likely to draw attention. Without it, the whole image
    fits into the box.

  * `:format` - Converts the variant, and appends the format's extension to its path. Without it, the variant
    has the image's format.

  * `:quality` - The encoder quality of a JPEG or WebP variant. Without it, libvips' default (75).

* `:variant_path` - Where the variants of an image go: a function `(path, variant, format)` that returns the path of the
  variant, or `nil` for a path that gets no variants, such as a variant's own path. `format` is the
  variant's `:format` or `nil`. A `{module, function, args}` is called with `args` after those three, for
  disks from config. The default puts them under `thumbnails/` (see `prefixed/4` and
  [Where variants go](#module-where-variants-go)). The default value is `{Fil.Plugin.Thumbnails, :prefixed, ["thumbnails"]}`.

* `:extensions` (list of `t:String.t/0`) - The extensions of images, lowercase with the dot. A path's extension is compared in lowercase, so
  `photo.JPG` is an image too. The default value is `[".jpg", ".jpeg", ".png", ".webp"]`.

* `:mode` - `:on_write` makes the variants when an image is written. `:manual` doesn't, and your code calls
  `generate/1` instead, such as from a background job. Deletes, copies and renames follow in both modes. The default value is `:on_write`.

* `:max_pixels` (`t:pos_integer/0`) - The most pixels an image may have. It's read from the image's header, before the image is decoded, so a
  small file that decodes into a huge image is refused before it takes up memory. The default value is `100000000`.

# `attach`

```elixir
@spec attach(Fil.Disk.t(), keyword()) :: Fil.Disk.t()
```

Attaches the plugin to `disk` under the name `Fil.Plugin.Thumbnails`.

The same as `plugins: [{Fil.Plugin.Thumbnails, :call, opts}]` in `Fil.disk/1`, except that the options are validated
here instead of on the first operation.

# `generate`

```elixir
@spec generate(Fil.Ref.t()) :: {:ok, keyword(Fil.Ref.t())} | {:error, Fil.error()}
```

Makes the variants of an image that's already stored, and returns their refs by variant name.

    {:ok, [small: small, square: square]} = Fil.Plugin.Thumbnails.generate(disk, "cats/tom.jpg")

It reads the image with `Fil.read/1`, through the disk's plugins, and writes every variant, replacing what's there.
Call it with `mode: :manual`, for images written before the plugin was attached or before a variant was added, and
to repair the variants after a write returned a variant's error. An image uploaded to S3 with a presigned URL
bypasses the plugins, so it gets its variants this way too.

Returns the errors of `Fil.read/1` and those of a write of the image (see [Errors](#module-errors)). Raises
`ArgumentError` when the plugin isn't attached to the disk, or the path gets no variants: its extension isn't in
`:extensions`, or `:variant_path` returns `nil` for it.

# `generate`

```elixir
@spec generate(Fil.Disk.t(), Path.t()) ::
  {:ok, keyword(Fil.Ref.t())} | {:error, Fil.error()}
```

Makes the variants of an image that's already stored. See `generate/1`.

# `prefixed`

```elixir
@spec prefixed(String.t(), atom(), atom() | nil, String.t()) :: String.t() | nil
```

Puts the variants under `prefix`, as `<prefix>/<variant>/<path>`, with the format's extension appended when the
variant has a `:format`. It's the default `:variant_path`, with `"thumbnails"`. Paths under `prefix` get no
variants, which keeps the plugin from making variants of variants.

    iex> Fil.Plugin.Thumbnails.prefixed("cats/tom.jpg", :small, nil, "thumbnails")
    "thumbnails/small/cats/tom.jpg"
    iex> Fil.Plugin.Thumbnails.prefixed("cats/tom.jpg", :square, :webp, "thumbnails")
    "thumbnails/square/cats/tom.jpg.webp"
    iex> Fil.Plugin.Thumbnails.prefixed("thumbnails/small/cats/tom.jpg", :small, nil, "thumbnails")
    nil

Another directory is `variant_path: {Fil.Plugin.Thumbnails, :prefixed, ["media/thumbs"]}`.

# `variant`

```elixir
@spec variant(Fil.Ref.t(), atom()) :: Fil.Ref.t()
```

Returns the ref of a variant of an image. It's only a path: the variant may not exist.

    iex> disk =
    ...>   Fil.disk(adapter: Fil.Adapter.Memory)
    ...>   |> Fil.Plugin.Thumbnails.attach(variants: [small: [width: 200], square: [width: 96, format: :webp]])
    iex> Fil.Plugin.Thumbnails.variant(disk, "cats/tom.jpg", :small)
    #Fil.Ref<memory:thumbnails/small/cats/tom.jpg>
    iex> tom = Fil.ref(disk, "cats/tom.jpg")
    iex> Fil.Plugin.Thumbnails.variant(tom, :square)
    #Fil.Ref<memory:thumbnails/square/cats/tom.jpg.webp>

Raises `ArgumentError` when the plugin isn't attached to the disk, `name` isn't one of its variants, the path escapes
the disk root, or `:variant_path` gives the path no variant.

# `variant`

```elixir
@spec variant(Fil.Disk.t(), Path.t(), atom()) :: Fil.Ref.t()
```

Returns the ref of a variant of an image. See `variant/2`.
