Tessera

DZI deep-zoom + multi-layer progressive-quality layer for Fresco-based image viewers in Phoenix. Generate DZI (Deep Zoom Image) tile pyramids from images via ImageMagick — eagerly or lazily one tile at a time — and render them as a Fresco layer that swaps between source qualities as the user zooms.

A tessera is a single tile in a mosaic. Tessera the library produces and consumes those tiles, layered on top of a Fresco viewer.


Install

def deps do
[
{:fresco, "~> 0.7"},
{:tessera, "~> 0.3"}
]
end

System requirement: ImageMagick (magick binary) on the host PATH for tile generation.

In your assets/js/app.js, import the JS hooks (Fresco first, then Tessera):

import "../../deps/fresco/priv/static/fresco.js"
import "../../deps/tessera/priv/static/tessera.js"
let liveSocket = new LiveSocket("/live", Socket, {
hooks: { ...window.FrescoHooks, ...window.TesseraHooks, ...colocatedHooks }
})

Render the viewer

Mount a Fresco viewer/canvas with a cheap preview, then attach a Tessera layer with the raster ladder (and, optionally, a DZI manifest for deep zoom). As the user zooms, Tessera swaps to a sharper raster while preserving viewport bounds; past the sharpest raster it streams DZI tiles.

Raster ladder only — medium + large + original

<Fresco.canvas id="photo" canvas={@canvas} class="w-full h-[80vh] rounded" />
<Tessera.layer
fresco_id="photo"
sources={[
%{url: ~p"/uploads/photo-medium.jpg", width: 800},
%{url: ~p"/uploads/photo-large.jpg", width: 1920},
%{url: ~p"/uploads/photo-original.jpg", width: 6000}
]}
/>

Raster ladder + DZI deep zoom (for gigapixel images)

<Fresco.canvas id="poster" canvas={@canvas} class="w-full h-[80vh] rounded" />
<Tessera.layer
fresco_id="poster"
sources={[
%{url: ~p"/uploads/photo-medium.jpg", width: 800},
%{url: ~p"/uploads/photo-large.jpg", width: 1920},
%{url: ~p"/uploads/photo-original.jpg", width: 6000}
]}
dzi_url={~p"/dzi/photo.dzi"}
/>

Each source carries its intrinsic pixel width; Tessera swaps to the next source up once the image is displayed wider than the current source (with hysteresis to prevent flicker). When dzi_url is set, Tessera activates tile streaming past the sharpest raster — so the cheap raster ladder covers everyday zoom and the DZI pyramid keeps gigapixel images crisp at extreme zoom.


Generate tiles

Eager — full pyramid in one shot

{:ok, %{manifest: manifest, tiles_dir: tiles_dir}} =
Tessera.generate("/uploads/photo.jpg", "/var/www/dzi")

Output:

/var/www/dzi/photo.dzi # XML manifest
/var/www/dzi/photo_files/0/0_0.jpg # zoom level 0 (smallest tile)
...
/var/www/dzi/photo_files/N/c_r.jpg # zoom level N, col c, row r

Options:

Tessera.generate(input, output_dir,
tile_size: 256, # pixels per tile edge
overlap: 1,
format: :jpg, # :jpg | :png
base_name: "img" # defaults to input basename without extension
)

Lazy — one tile at a time, on demand

For very large images, eagerly building the whole pyramid is wasteful — most tiles will never be looked at. Generate the manifest cheaply, then produce individual tiles on first request:

:ok = Tessera.generate_manifest({width, height}, "photo",
storage: Tessera.Storage.Local,
storage_opts: [root: "/var/cache/dzi"]
)
:ok = Tessera.generate_tile("/uploads/photo.jpg", {level, col, row}, "photo",
image_width: width,
image_height: height,
storage: Tessera.Storage.Local,
storage_opts: [root: "/var/cache/dzi"]
)

Pluggable storage

Tessera.Storage is a one-callback behaviour — Tessera writes generated tiles to a temp file, then hands them off via put/3:

defmodule MyApp.S3TileStorage do
@behaviour Tessera.Storage
def put(content_path, key, opts) do
bucket = Keyword.fetch!(opts, :bucket)
ExAws.S3.put_object(bucket, key, File.read!(content_path)) |> ExAws.request() |> case do
{:ok, _} -> :ok
{:error, reason} -> {:error, reason}
end
end
end
Tessera.generate_tile(input, {1, 0, 0}, "photo",
image_width: w, image_height: h,
storage: MyApp.S3TileStorage,
storage_opts: [bucket: "my-tiles"]
)

Reads / existence checks / deletes are the consumer's job — Tessera never reads back what it wrote.


Notes


What changed in 0.3

Fresco 0.5 dropped OpenSeadragon for its own CSS-transform engine, which broke Tessera 0.2 (it registered a DZI source provider with OSD and read viewport.getZoom()). Tessera 0.3 is a Fresco peer layer (the same model as Etcher): it gets the Fresco handle via window.Fresco.onReady/2, reads the live transform, swaps rasters with swapSourcePreservingBounds, and renders a DZI tile overlay aligned to the transform. <Tessera.layer> gains an optional dzi_url attribute; sources is unchanged.

The server-side Tessera.generate/3, Tessera.generate_manifest/3, Tessera.generate_tile/4, and Tessera.Storage API are unchanged.


License

MIT — see LICENSE.