Packages
phoenix_kit
2.64.1
2.64.1
2.64.0
2.63.0
2.62.2
2.62.1
2.62.0
2.61.0
2.60.3
2.60.2
2.60.1
2.60.0
2.59.0
2.58.0
2.57.1
2.57.0
2.56.1
2.56.0
2.55.1
2.55.0
2.54.2
2.54.1
2.54.0
2.53.0
2.52.2
2.52.1
2.52.0
2.51.0
2.50.0
2.49.1
2.49.0
2.48.0
2.47.0
2.46.0
2.45.0
2.44.0
2.43.1
2.43.0
2.42.1
2.42.0
2.41.6
2.41.4
2.41.3
2.41.2
2.41.1
2.41.0
2.40.1
2.40.0
2.39.0
2.38.1
2.38.0
2.37.5
2.37.4
2.37.3
2.37.2
2.37.1
2.37.0
2.36.1
2.36.0
2.35.0
2.34.0
2.33.0
2.32.1
2.32.0
2.31.1
2.31.0
2.30.0
2.29.1
2.29.0
2.28.2
2.28.1
2.28.0
2.27.2
2.27.1
2.27.0
2.26.1
2.26.0
2.25.0
2.24.0
2.23.3
2.23.2
2.23.1
2.23.0
2.22.24
2.22.23
2.22.22
2.22.21
2.22.20
2.22.19
2.22.18
2.22.17
2.22.16
2.22.15
2.22.14
2.22.13
2.22.12
2.22.11
2.22.10
2.22.9
2.22.8
2.22.7
2.22.6
2.22.5
2.22.4
2.22.3
2.22.2
2.22.1
2.22.0
2.21.5
2.21.4
2.21.3
2.21.2
2.21.1
2.21.0
2.20.0
2.19.0
2.18.1
2.18.0
2.17.0
2.16.0
2.15.1
2.15.0
2.14.2
2.14.1
2.14.0
2.13.19
2.13.18
2.13.17
2.13.16
2.13.15
2.13.13
2.13.12
2.13.11
2.13.10
2.13.9
2.13.8
2.13.7
2.13.6
2.13.5
2.13.4
2.13.3
2.13.2
2.13.1
2.13.0
2.12.1
2.12.0
2.11.0
2.10.0
2.9.0
2.8.1
2.8.0
2.7.0
2.6.0
2.5.0
2.4.0
2.3.0
2.2.0
2.1.0
2.0.1
2.0.0
1.7.236
1.7.235
1.7.234
1.7.233
1.7.232
1.7.231
1.7.230
1.7.229
1.7.228
1.7.227
1.7.226
1.7.225
1.7.224
1.7.223
1.7.222
1.7.221
1.7.220
1.7.219
1.7.218
1.7.217
1.7.216
1.7.215
1.7.214
1.7.213
1.7.212
1.7.211
1.7.210
1.7.209
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/modules/storage/tile_source.ex
defmodule PhoenixKit.Modules.Storage.TileSource do
@moduledoc """
The image deep-zoom tiles are cut from.
Tiles are cut by libvips (Tessera) from the file's original. libvips as shipped
with the `vix` package can read an iPhone's HEIC for its size but has no HEVC
decoder, so it cannot read a single pixel of it: without help a HEIC photo has no
tiles, and the viewer stops at the largest rendition (1920 px) where a JPEG
zooms to the last pixel. ImageMagick decodes HEIC, HEIF and AVIF.
For those types the first request for a tile makes **one full-resolution JPEG**
of the original with ImageMagick (`ImageProcessor.full_jpeg/3`), stores it
beside the tiles under `_tiles/<base>_source.jpg` (a system-managed file of the
original, so it goes with it), and every tile is cut from it. The key carries the
original's version, so an edited original makes a new one. Any other original is
used as it is. The caller holds the file's tile lock, so concurrent requests make
the JPEG once.
"""
require Logger
alias PhoenixKit.Modules.Storage.{ImageProcessor, Manager, TesseraAdapter}
@convert_mimes ~w(image/heic image/heif image/avif)
@type prepared :: {:ok, Path.t(), (-> :ok)} | {:error, term()}
@doc "Whether tiles of this original have to be cut from a converted copy."
@spec convert?(map() | nil) :: boolean()
def convert?(%{mime_type: mime}) when is_binary(mime),
do: String.downcase(mime) in @convert_mimes
def convert?(_instance), do: false
@doc "The key, under the tiles prefix, of the converted copy of the original `base` names."
@spec key(String.t()) :: String.t()
def key(base), do: base <> "_source.jpg"
@doc """
A local path to an image the tiles can be cut from, and a function that removes
whatever temporary files were made for it. `source` is the tile source map
(`:base`, `:file_uuid`, `:width`, `:height`), `instance` the original's file
instance.
"""
@spec prepare(map(), map()) :: prepared()
def prepare(source, instance) do
if convert?(instance), do: converted(source, instance), else: original(instance)
end
defp original(instance) do
path = temp("tessera-src")
case Manager.retrieve_file(instance.file_name, destination_path: path) do
{:ok, _} -> {:ok, path, remover([path])}
{:error, _} = error -> discard(path, error)
end
end
defp converted(source, instance) do
key = key(source.base)
destination = TesseraAdapter.destination_for(key)
jpeg = temp("tessera-jpeg") <> ".jpg"
if Manager.file_exists?(destination) do
case Manager.retrieve_file(destination, destination_path: jpeg) do
{:ok, _} -> {:ok, jpeg, remover([jpeg])}
{:error, _} = error -> discard(jpeg, error)
end
else
build(source, instance, key, jpeg)
end
end
defp build(source, instance, key, jpeg) do
original = temp("tessera-heic")
result =
with {:ok, _} <- Manager.retrieve_file(instance.file_name, destination_path: original),
{:ok, _} <- ImageProcessor.full_jpeg(original, jpeg),
:ok <- same_size(jpeg, source) do
store(jpeg, key, source)
{:ok, jpeg, remover([jpeg])}
end
File.rm(original)
case result do
{:ok, _, _} = ok ->
ok
{:error, reason} = error ->
File.rm(jpeg)
Logger.warning("[TileSource] no JPEG made of #{source.file_uuid}: #{inspect(reason)}")
error
end
end
# Tiles are cut by the file's recorded size; a copy of another size would put
# every tile in the wrong place, so it is refused rather than used.
defp same_size(jpeg, %{width: width, height: height}) do
case ImageProcessor.extract_dimensions(jpeg) do
{:ok, {^width, ^height}} -> :ok
{:ok, {w, h}} -> {:error, {:dimension_mismatch, {w, h}, {width, height}}}
{:error, _} = error -> error
end
end
# Kept for the next tile. A copy that could not be kept is still used for this
# one; the next request makes it again.
defp store(jpeg, key, source) do
case TesseraAdapter.put(jpeg, key,
parent_file_uuid: source.file_uuid,
mime_type: "image/jpeg",
metadata: %{"kind" => "tile_source"}
) do
:ok ->
:ok
{:error, reason} ->
Logger.warning(
"[TileSource] could not keep the JPEG of #{source.file_uuid}: #{inspect(reason)}"
)
end
end
defp discard(path, error) do
File.rm(path)
error
end
defp remover(paths), do: fn -> Enum.each(paths, &File.rm/1) end
defp temp(prefix),
do: Path.join(System.tmp_dir!(), "#{prefix}-#{System.unique_integer([:positive])}")
end