Scenic.Assets.Static (Scenic v0.11.0-beta.0) View Source

Manages static assets, which are resources such as fonts or images (jpg or png) that ship with your application and do not change over time.

These assets live as seperate files and are hashed so that they are rejected if they change in any way after you compile your application. They are cacheable by the relay server if you remote your Scenic UI.

In previous versions of Scenic, static assets were rather complicated to set up and maintain. Starting with v0.11, Scenic has an assets build pipeline that manages the static assets library for you.

Required Configuration

Setting up the static asset pipeline requites several inputs that need to be maintained.

  • Assets Directory: Typically /assets in your main app source directory. This is the folder that holds your raw asset files.
  • Assets Module: A module in your application that builds and holds the asset library.
  • Assets Config: Configuration scripts in your application that indicates where the assets directory is and your assets module.

Assets Directory

The assets directory typically is typically called /assets and lives at the root of your application source directory. This can be changed in the config options.

Example:

my_app_src
  assets
    fonts
      roboto.ttf
      custom_font.ttf
    images
      parrot.jpg
      my_logo.png
  config
  lib
  etc...

Once the rest of the configuration is complete, adding a new font is as simple as dropping the *.ttf file into the /assets/fonts directory and compiling your assets module. Similar is true for images.

Assets Library

When your application is running, there needs to be a module that contains the built asset library referring to your static assets. This library holds things like the hash of the contents of each asset, and it's parsed metadata.

You must create this module and compile it with your application. The following example is what this module should look like. Replace MyApplication and :my_application with the actual name of your application.

defmodule MyApplication.Assets do
  use Scenic.Assets.Static,
    otp_app: :my_application,
    sources: [
      "assets",
      {:scenic, "deps/scenic/assets"}
    ],
    alias: [
      parrot: "images/parrot.jpg"
    ]
end

Notice that there are several configuration sections in your assets module. Sources is the list of folders to look in to find assets. For example, if you take a dependency on a package that contains assets, you will need to add it's assets folder here. If can omit the sources section if you only use a single assets folder and scenic's default fonts. In other words, the sources configuration shown above is also the default.

The :alias list creates shortcuts that refer to the files in the assets library. This is useful if you think an asset id may change during development but want a constant way to refer to it in your code. In the above example, the atom :parrot is mapped to the file images/parrot.jpb and are interchangeable with each other in a graph.

In this example, the two rect fills are identical as the :parrot alias was created in the configuration script.

Graph.build()
  |> rect({100, 50}, fill: {:image, "images/parrot.jpg"})
  |> rect({100, 50}, fill: {:image, :parrot})

The fonts fonts/roboto.ttf and fonts/roboto_mono.ttf are considered the default fonts for Scenic and are automatically aliased to :roboto and :roboto_mono. It is expected that you will include those two fonts in your /assets/fonts directory.

IMPORTANT NOTE: When you add a new asset to the assets directory, you may need to force this module to recompile for them to be usable. Adding or removing a return at the end should do the trick. In the future, there will be a file system watcher (much like Phoenix has) that will do this automatically. Until then, it is pretty easy to do manually.

Assets Configuration

The final piece is some configuration that connects scenic and your assets module togther. Put this in your application's config.exs file.

config :scenic, :assets,
  module: MyApplication.Assets

Troubleshooting

If you have added an asset to your assets directory and you think it should be in your library, but it isn't, or you can't compile a scene because the asset can't be found, then start troubleshooting with the following steps.

  1. Force your assets module to rebuild. Touch it in some way such as adding or removing a carriage return at the end, then compile again.
  2. Check that you are using the correct id for the asset in your graph.
  3. If you are using an alias, check it's spelling and its assignment in the config script.
  4. Confirm that the asset itself has valid contents, whether it is a font (.ttf) or an image (.jpg or .jpeg or .png)

That usually does it.

Under the Covers

When your assets module is compiled, several steps are executed by Scenic.Assets.Static

  1. The files in your assets directory are parsed for validity and metadata. Valid files move on to the next step
  2. The valid assets files are hashed to create a cryptographic signature that is used later when the files are loaded to confirm that they are unchanged.
  3. The asset files are copied into the /priv/static directory, which is where they are actually loaded from at run time. The name of the file in this directory is a Base.url_encode64/2 version of the hash of the file's contents.
  4. A map is created, which is the actual asset library used at runtime. This map has the original file name as keys and holds the hashes, and parsed metadata as the contents. This map is stored as a literal object in your assets module and is the reason it needs to be compiled when you add a new asset.

If you are curious and want to see the library yourself, you can query the MyApplication.Assets.library/0 function, which is added at compile time. Alternately, the function Scenic.Assets.Static.library/0 should return the same library.

Future Work

There are two pieces of work to the static assets pipeline that are planned for the future.

First is a file system watcher that automatically flags your assets module to be recompiled when the contents of the assets directory changes. This would work in a similar way to the file system watcher used by Phoenix.

The second, larger, piece of work is to include optional transform scripts/code when your assets module is compiled. This would let you do things like putting a very high resolution image in the sources folder and down-scaling at compile time as appropriate for the target device you are compiling for. In the meantime, just put in the assets you want to use directly.

Link to this section Summary

Functions

Return the compiled asset library.

Load the binary contents of an asset given it's id or hash.

Fetch the metadata for an asset by id.

Return the configured asset library module.

Transform an asset id into the file hash.

Link to this section Types

Specs

id() :: String.t() | atom() | {atom(), String.t()}

Specs

t() :: %Scenic.Assets.Static{
  aliases: map(),
  hash_type: :sha3_256,
  meta_hash: binary(),
  metas: map(),
  module: module(),
  otp_app: atom()
}

Link to this section Functions

Link to this function

assign(lib, atom, key, value)

View Source

Return the compiled asset library.

Specs

load(id :: any()) ::
  {:ok, data :: binary()}
  | {:error, :not_found}
  | {:error, :hash_failed}
  | {:error, File.posix()}

Load the binary contents of an asset given it's id or hash.

Return is in the form of {:ok, bin}

If the asset is not in the library, {:error, :not_found} is returned.

The contents of the file will be hashed and compared against the hash found in the library. If this test fails, {:error, :hash_failed} is returned.

If the output file cannot be read, it returns a posix error.

Specs

load(library :: t(), id :: any()) ::
  {:ok, data :: binary()}
  | {:error, :not_found}
  | {:error, :hash_failed}
  | {:error, File.posix()}

Specs

meta(id :: any()) :: {:ok, meta :: any()} | :error

Fetch the metadata for an asset by id.

Return is in the form of {:ok, metadata}

If the hash is not in the library, :error is returned.

Example:

{:ok, meta} = Scenic.Assets.Static.fetch( :parrot )

Specs

meta(library :: t(), id :: any()) :: {:ok, meta :: any()} | :error

Return the configured asset library module.

Specs

to_hash(id :: any()) :: {:ok, hash :: any()} | :error

Transform an asset id into the file hash.

If you pass in a valid hash, it is returned unchanged

Example:

alias Scenic.Assets.Static
library = Scenic.Assets.Static.library()

{:ok, "VvWQFjblIwTGsvGx866t8MIG2czWyIc8by6Xc88AOns"} = Static.hash( library, :parrot )
{:ok, "VvWQFjblIwTGsvGx866t8MIG2czWyIc8by6Xc88AOns"} = Static.hash( library, "images/parrot.png" )
{:ok, "VvWQFjblIwTGsvGx866t8MIG2czWyIc8by6Xc88AOns"} = Static.hash( library, "VvWQFjblIwTGsvGx866t8MIG2czWyIc8by6Xc88AOns" )

Specs

to_hash(library :: t(), id :: any()) :: {:ok, hash :: any()} | :error