Follow-ups captured during development. Items here are not blockers for the current milestone but should be addressed before the v0.1 release.
Aligned Image.Plug.SourceResolver.File (now uses File.stream!(path, 2048, []) |> Image.open()) and documented the full source→encoder→conn chain in Image.Plug.Pipeline.Encoder's moduledoc, citing the stream_image_test.exs reference shape. Added Image.Plug.StreamingPipelineTest as a regression that exercises both the verbatim Image-library chain and our plug end-to-end (asserting conn.state == :chunked). The encoder stays conn-agnostic (returns {:stream, Enumerable.t()}); the plug does the Plug.Conn.chunk/2 reduce so we keep the conn for header manipulation, error fallbacks, and telemetry — equivalent to what Image.write(image, conn, suffix: ext) does internally.
Image.Plug.Pipeline.Normaliser rewritten to enforce a Sharp-style canonical order (Trim → Background → Resize → Rotate → Flip → Border → Adjust → Blur → Sharpen → Draw → Segment) regardless of the order the provider emits the ops in. Cardinality enforced for every single-instance op kind. Extended no-op folding (Adjust all-1.0, Sharpen/Blur sigma 0, Flip nil direction, Border/explicit Trim all-zero sides). 13 new regression tests including the explicit "Resize lands before any post-resize op no matter when the user appends it" case the original TODO called out.
Spun out into a sibling package at /Users/kip/Development/image/image_components/ (mix app :image_components, module namespace Image.Component.* — singular namespace, plural package name to match the existing project slot). The earlier one-line image_components placeholder was deleted in the same change. Hard dep on :phoenix_live_view. Self-contained — its own Cloudflare URL builder; no dep on :image_plug (the two packages share the URL grammar, not code).
Includes the full unpic-style port: layout modes (:fixed | :constrained | :full_width) with CLS-prevention CSS, both width-descriptor and density-descriptor srcsets, format-fallback <picture type> markup, art-direction <picture media> markup (Image.Component.Picture.picture/1), and a :host option for cross-host CDN setups (mirrors unpic's domain).
See image_components's own README and CHANGELOG for any further follow-ups.
Image 0.67.0 landed the Group A and Group B helpers; image_plug now wires every one of them through to its providers. The sections below capture what shipped and what's still outstanding.
-
Image.gamma/2—image_plug's interpreter now calls this directly instead of dropping intoVix.Vips.Operation.gamma. Used by Cloudflare/imgix/Cloudinarygamma/gam/e_gamma. -
Selective EXIF preservation —
Image.minimize_metadata/2accepts a:keeplist. Cloudflaremetadata=copyrightis now wired end-to-end inImage.Plug.Pipeline.Encoderviakeep: [:copyright, :orientation]. Conformance⚠️ → ✅. -
Image.sepia/2— wired into imgixsepia=Nand Cloudinarye_sepia[:N]. Conformance ❌ → ✅ in both. -
Image.tint/2— wired into imgixmonochrome=<hex>(replaces the plain-B&W workaround). Conformance⚠️ → ✅. -
Image.set_orientation/2— wired into imgixor=N. The encoder snapshots and restores theorientationheader acrossImage.minimize_metadata/2so the override survives metadata stripping. Conformance ❌ → ✅. -
Image.posterize/2— wired into Cloudinarye_cartoonify[:level_count]. Conformance ❌ → ✅. -
Image.pixelate/2— wired into Cloudinarye_pixelate[:block_size]. Conformance ❌ → ✅. (Imgix'spx=Nis still⚠️ —Image.pixelate/2exists but the imgix parser hasn't been wired; trivial follow-up if anyone needs it.) -
Image.fade/2— wired into Cloudinarye_fade[:N](bottom-edge fade). Conformance ❌ → ✅. Cloudinary's directional flavours (e_fade_topetc.) aren't modelled. -
Image.opacity/2— wired into Cloudinaryo_<n>(0..100 percentage). Conformance ❌ → ✅. -
Image.rounded/2— wired into Cloudinaryr_<n>/r_max. Already SVG-mask-based inImage, no draw functions involved. Conformance ❌ → ✅. -
Image.drop_shadow/2— wired into ImageKite-shadow[-bl-<n>_st-<n>_x-<n>_y-<n>_c-<hex>]. Conformance ❌ → ✅. -
Encoder
:lossy,:progressive,:chroma_subsamplingflags —Image.write/3accepts all three;image_plug'sFormatIR carries them through; Cloudinaryfl_lossy/fl_progressiveand ImageKitlo-/pr-/cp-are wired to set them. Conformance ❌ → ✅ in Cloudinary and ImageKit.
-
Image.enhance/2— luminance equalisation + saturation boost + sharpen. Wired into Cloudinarye_improve/e_auto_brightness/e_auto_color/e_auto_contrast, imgixauto=enhance, ImageKite-retouch.⚠️ in conformance guides because the hosted versions are ML-driven (we approximate). -
Image.vignette/2wired into Cloudinarye_vignette[:N]. ❌ → ✅. -
imgix
px=Nwired toImage.pixelate/2. ❌ → ✅. -
ImageKit
ar-<W>-<H>wired (provider-side dimension derivation, noImagechange). ❌ → ✅. -
ImageKit
z-<n>wired to the existingface_zoomfield on Resize (parsed and stored, interpreter is still a no-op pending face detection in:image). ❌ →⚠️ .
-
ICC profile colourspace —
Image.to_colorspace/3shipped in:image0.67. The IR hasOps.IccTransform{profile, intent}and the interpreter is wired. Custom-ICC paths (Adobe RGB, ProPhoto) are deliberately not synthesised from URL strings —cs_adobergb1998/cs=adobergb1998still return:unsupported_option. ConstructIccTransformops directly when composing pipelines, or add an application-level:icc_aliasesoption to map URL tokens onto known profile paths. -
Auto-quality model — content-aware quality picker for Cloudinary
q_auto. Out of scope forImage; would need a calibrated heuristic. -
Animated-image frame trim — ImageKit
tr=t-<from>-<to>. NeedsImage.extract_frames/3or a pages-by-time-range helper. -
Face-aware crop / zoom — needs face detection inShipped.:image(probably via:image_vision). The IR fields exist (face_zoom,gravity: :face) but the interpreter doesn't act onface_zoomyet.Image.Plug.FaceAwarewrapsImage.FaceDetection.crop_largest/2(gated behindCode.ensure_loaded?/1) and the interpreter pre-crops to the largest face whengravity: :faceis set.face_zoomcontrols padding (0= loose context,1= tight crop).Ops.PixelateFaces(Cloudinarye_pixelate_faces) pixelates only the detected face regions. All face-aware ops fall back gracefully when:image_visionis absent. -
Auto-contrast (content-aware) — ImageKit
e-contrastis currently approximated asAdjust{contrast: 1.1}. A content-aware version (one of theenhance/1family) would be sharper. -
AI-driven background removal —
Image.Background.remove/2andImage.Background.mask/2ship in:image_vision(BiRefNet-lite via Ortex). The wire-up pattern is the same one used for face detection (seeImage.Plug.FaceAware): anOps.RemoveBackground{}IR op whose interpreter clause delegates toImage.Background.remove/2only whenCode.ensure_loaded?(Image.Background)is true. Maps to ImageKite-bgremove/e-removedotbg. Not yet implemented. -
Other AI-driven calls — super-resolution, generative edits. Permanent
:imagegap (live in:image_visioninstead).
Each adapter ships with a documented gap matrix (✅ / ⚠️ / ❌) in guides/<provider>_conformance.md. The Group A + B work and this follow-up cycle together moved 22 entries from ❌/⚠️ to ✅ / ⚠️.
Shipped in both image_plug (Image.Plug.Provider.Cloudinary — URL recogniser, options parser, signing, wiring) and image_components (Image.Component.CDN.Cloudinary — URL builder, signing). 42 unit tests + 10 integration tests on the plug side, 18 unit tests + 2 integration tests on the component side, all passing. SHA-256 wire-format-compatible signing with the in-path s--<sig>-- segment and 32 url-safe-base64-character truncation. Multi-stage chained transforms recognised but flattened to one comma-joined option set (the v0.1 IR doesn't model chained transforms; documented in the conformance guide as guides/cloudinary_conformance.md for the per-option matrix and the documented gaps.
Shipped in both image_plug (Image.Plug.Provider.ImageKit — URL recogniser, options parser, signing, wiring) and image_components (Image.Component.CDN.ImageKit — URL builder, signing). 39 unit tests + 10 integration tests on the plug side, 17 unit tests + 2 integration tests on the component side, all passing. HMAC-SHA1 wire-format-compatible signing with ?ik-s=<hex> and ?ik-t=<unix>. Both URL forms supported on inbound (path-prefix tr:... and query-string ?tr=...); the component emits the path-prefix form. See guides/image_kit_conformance.md for the per-option matrix and the documented gaps.
Mix app renamed :image_server → :image_plug; module namespace Image.Server.* → Image.Plug.* (request plug merged into the top-level Image.Plug module); supervisor / telemetry prefix / default ETS table / response headers / app env key / log prefix all updated; lib + test directory tree moved; README, CHANGELOG, plans, and TODO updated. The on-disk project directory is still image_server/ — leave that for whenever the parent repo restructures, or rename in a separate filesystem-only commit.