This changelog follows Keep a Changelog. Releases use Semantic Versioning.
Unreleased
0.2.0 - 2026-10-13
Added
- SVG sources, when the optional
image_pipe_svgpackage is installed. An SVG renders straight at the size the request needs, so it stays sharp at any size, and a resize can make it larger than its ownwidthandheightwithoutenlarge, up to the input pixel limit, past which the rendered image is enlarged. Regions and crops in pixels measure the SVG at its own size. With noformatand no negotiated format, the result is a PNG. Watermark images can be SVGs too, and render at the size they're drawn. Withoutimage_pipe_svg, SVG sources still fail with415. - The
:svgoption configures SVG sources::font_dirsand:fallback_familyfor the fonts SVG text renders in (no system fonts are used), and:max_bytes,:max_render_work, and:max_render_pixelsto limit the work an SVG can cause. - A
[:source, :svg_render]span wraps rendering an SVG original or watermark, with its intrinsic and rendered sizes and the estimated work, and names the limit that rejected an SVG. The default Logger logs it. cacheandinput_cachedelete metadata files they can't read, such as ones another ImagePipe version wrote, and their bodies, once the files are more than an hour old. The[:cache, :sweep]event reports them as:unreadable.[:cache, :stage]reports writes the cache drops to keep up with its storage as:stage_dropped, for processed images and originals, and carries:pool. The default Logger logs dropped writes at:warning.[:cache, :coordination]carriesreason: :deadlinewhen a request stops waiting for another request's image because it wasn't stored in time.- The
image_pipe.requestspan carries arequest_idattribute when Logger metadata has a:request_id, such as the onePlug.RequestIdsets. - The
[:cache, :lookup],[:cache, :write], and[:cache, :stage]telemetry events carry:cache_key, the hash of the entry's cache key, and[:cache, :lookup]carries:entry, which says whether it read a processed response or a source record. The tracer adds them to their spans and span events. - The
[:cache, :write]stop event carries:estimated_cost_us, the work the cache estimates it took to make a processed image. The tracer adds it to the span. - The
[:cache, :write]stop event carries:bytes, the size of the stored body, so dividing it by the span's duration gives a rough write speed for the cache storage.[:cache, :lookup]carries:byteson a hit for a processed response. The tracer adds:bytesto both spans, and the default Logger logs it. - A
[:source, :decode_open]span wraps each time libvips opens the original, so a trace shows how often a request opens it. The default Logger doesn't log it.
Changed
- With
image_pipe_svginstalled, cache keys and ETags include the resvg version, the fonts, and the fallback family, so every cached image misses once after installing it, and again after changing the fonts or fallback family, or upgradingimage_pipe_svgto a version that renders SVGs differently. - Breaking: An HTTP source's
allowed_hostsentry without a port allows only the scheme's default port, 80 or 443, including for redirect targets. Before, any port on a listed host was reachable. Add an entry with the port, such as"assets.example.com:8443", for an origin on another port. Abase_urlwith a non-default port needs that port inallowed_hostswhen you set the list. A malformed entry, such as"assets.example.com:https", now fails at startup. - A response to a URL with
expiresdropsstale-while-revalidateandstale-if-error, including ones the host sets, because caches can't use them alongside itsmust-revalidate. - Processed images and originals cached by an earlier version are not reused, because the cache key changed and entry files are named differently. The cache refills as requests come in, and deletes the old files as leftover files.
- Requests for PNG and other sources that need no shrink-on-load open the source once instead of twice, which saves about 0.3 ms per request.
- A custom detector's
identity/1,available?/1, andready?/1receive only:classes, asdetect/2already did. They no longer see the mount's other configuration. - Breaking: The source identifiers a custom source adapter receives are
ImagePipe.Source.Path,ImagePipe.Source.URL, andImagePipe.Source.Object. ReplaceImagePipe.Plan.Source.Path,.URL, and.Objectin your adapter. - Breaking: A source's
internal_cachetakes:enabledor:disabled, and defaults to:enabled. Replaceinternal_cache: :auto, which meant the same as:enabled. - Breaking:
ImagePipe.Source.S3.CredentialProvider.fetch_credentials/2takes the bucket and the provider's options. Change your provider'sfetch_credentials/3tofetch_credentials/2by dropping the third argument, which was always[]. - Breaking:
cacheandinput_cachetake the file system cache's options directly. Replacecache: {ImagePipe.Cache.FileSystem, root: "..."}withcache: [root: "..."], and the same forinput_cache. The server's[cache]configuration is unchanged. - Breaking: The tracer now creates OpenTelemetry spans as they happen, and
needs the OpenTelemetry SDK in your application. Remove the
exporter:option fromImagePipe.Telemetry.attach_tracer/1and theImagePipe.Telemetry.Trace.OtelIdGeneratorid_generatorsetting from your:opentelemetryconfiguration. To print spans without a backend, use the SDK's stdout exporter. Span and trace IDs now come from the SDK, and each span reaches the SDK when it ends, not when the request finishes. - A request's spans join the trace of the span that is current when the
request starts, such as one from your Phoenix or Bandit instrumentation.
This includes
ImagePipe.run/4in an instrumented job. Withextract_inbound: true, the inboundtraceparentapplies only when no span is current. - Spans your own code opens inside ImagePipe's processes, such as in a custom source or detector, nest under the request's spans, with or without the tracer attached.
- A request that continues an inbound trace follows the caller's sampled flag under the SDK's default parent-based sampler. It was always recorded before.
- Breaking:
[:cache, :eviction, :stop]reports the entries a write evicts, withtrigger: :admission, and the startup scan's batches withtrigger: :startup. Update handlers that matchtrigger: :reconcile. - Responses no longer wait for the cache's disk. A processed image is stored
in the background after it's sent, and a downloaded original while the
request processes it. The
[:cache, :write]span can end after the request. - A request waiting for another request's image or original stops waiting when the image or original isn't stored 2 seconds after it was sent or downloaded.
- When the disk can't keep up, writes are dropped, and unfinished writes
count against
max_size_bytes. An entry larger than a twentieth ofmax_size_bytes, or 256 MiB, is never stored (see writing entries). trimis faster on images under a megapixel: 2 ms instead of 6.5 ms on a 256×256 image.- Breaking:
cacheandinput_cacheneedmax_size_bytes, and an ImagePipe instance (seeImagePipe.child_spec/1). Addmax_size_bytesto each cache, and to the options ofImagePipe.Cache.FileSystem.get/2, and move a configuration with a cache fromImagePipe.Plugoptions or anImagePipe.run/4call into an instance.run/4with an instance name can't setcacheorinput_cacheper call. - Breaking: Each
rootofcacheandinput_cacheserves one node. An instance writes a.image-pipe-ownerfile into each cache'sroot. While another node keeps that file updated, the instance logs an error and runs without that cache, and it takes therootover once the other node stops. Give each replica its own cache directories, such as its own volume. When a node is killed, a node on another host that starts after the kill takes over itsrootabout 30 seconds after starting. cacheandinput_cachekeep the entries that are requested often and took the most work to make for their size. For a processed image, that work is estimated from the request instead of timed (see work to make an entry). On startup a cache reads only file names, so a large cache is ready sooner.
Removed
- Breaking:
ImagePipe.Transform.Detector.Composite.new/1anddefault/0. A detector is configured as a module, so a composite struct could not be configured. ConfigureImagePipe.Transform.Detector.Compositeitself to get the default face and object detectors. - Breaking: Bounded caches keep request counts in memory only. Remove
node_id,state_dir,flush_interval,cleanup_interval, andstate_ttlfromcacheandinput_cache. After a restart, a bounded cache keeps its entries and learns again which are requested most. You can delete the.cache_statedirectory under each bounded cache'sroot. - The
[:cache, :warm_start],[:cache, :flush, :stop], and[:cache, :cleanup, :stop]telemetry events. - Breaking: The
eviction_victim_limitandreconcile_intervaloptions ofcacheandinput_cache. Remove them from your configuration. A write now evicts every entry it needs before it's stored, so the stored bodies stay undermax_size_bytesonce the startup scan has run, and the cache no longer runs a background eviction pass every minute. - Breaking: The
window_ratio,sketch_depth,sketch_width,aging_sample_size,doorkeeper_cardinality,doorkeeper_fpr, andpath_prefixoptions ofcacheandinput_cache. Remove them from your configuration. The cache no longer reads or cleans up the old entries under apath_prefixdirectory, so delete that directory. - Breaking: The
rescan_intervaloption ofcacheandinput_cache, and the[:cache, :rescan]telemetry event. Removerescan_intervalfrom your configuration. A cache deletes leftover files after its startup scan and then every 24 hours. [:cache, :stage]no longer reportscache: :stage_cleanup_error. Discarding a staged entry doesn't fail.- Breaking:
ImagePipe.Telemetry.Trace.LogExporter,ImagePipe.Telemetry.Trace.OpenTelemetryExporter,ImagePipe.Telemetry.Trace.OtelIdGenerator,ImagePipe.Telemetry.Trace.Span,ImagePipe.Telemetry.Trace.Context, and theImagePipe.Telemetry.Trace.Exporterbehaviour.
Fixed
[:cache, :write]reports a write error instead ofcache: :writewhen the cache's process isn't running, since nothing was stored.- A
cropplaced withfocuson an image with an EXIF orientation other than 1 or 5 could land one pixel off from the same crop on an untagged image, when the crop's position fell on a half pixel. - With
max-bytes, a quality search that ran out of encode attempts shipped the lowest quality, even when it had already encoded a higher one that fit the budget. It now ships the highest encoded quality that fits. When only the lowest quality fits, theX-ImagePipe-AQ-Outcomedebug header and the[:encode, :search]telemetry event reporthitinstead ofbest_effort. - With
allow_originset, cross-origin scripts can send conditional requests such asIf-None-Match, and can read response headers such asETagandContent-Disposition. Preflight requests now getAccess-Control-Allow-Headers: *, and responses getAccess-Control-Expose-Headers: *. - A comma inside a quoted
Acceptparameter, as inimage/webp;x="a,b";q=0, no longer splits the entry. The split dropped theq=0, so ImagePipe could choose a format the client had excluded. input_cacherejectsmax_body_bytesinstead of ignoring it, and configuration fails with an unknown-option error. Remove the option frominput_cache.- Concurrent fetches from an HTTP source (
ImagePipe.Source.HTTP) just after startup could exit with:noproc. They now wait for the connection pool to start. - A CMYK or other non-RGB source with an embedded color profile and more
pixels than
max_intermediate_pixelsfails with422, like other oversized images, instead of415. - The source cache stores and reuses originals whose
Cache-Controlhas a qualifiedprivate="..."orno-cache="..."naming only headers ImagePipe doesn't keep, such asprivate="Set-Cookie". They were treated as unqualifiedprivate(never stored) orno-cache(revalidated on every request).
0.1.0 - 2026-10-09
Added
First release of ImagePipe, with image processing powered by elixir-image/image, Vix, and libvips.
- Serve transformed images on demand from Phoenix or any Plug application
through
ImagePipe.Plug. Reuse the same processing plans and configuration for uploads, files, and background jobs through the Elixir API. - Read originals from local files, HTTP(S), S3-compatible storage, or custom source adapters.
- Compose resizing, cropping, orientation, effects, and watermarks in explicit processing groups, with optional face and object detection, or your own detector.
- Define reusable presets and request defaults in your configuration, statically or through a host preset lookup backed by a database or cache.
- Encode JPEG, PNG, WebP, and AVIF, with format negotiation, quality controls, byte budgets, and color-profile policies. Automatic quality gives each image the lowest quality that still looks as good as a target you choose. Generate BlurHash, LQIP CSS, and image-info JSON.
- Configure input and output caches, conditional HTTP responses, streamed delivery, request safety limits, and processing concurrency. Observe requests through telemetry, logging, and tracing.