Skip to content

feat(instagram): auto-fit story images to 9:16 with blurred background#123

Merged
paulocastellano merged 8 commits into
mainfrom
feat/story-blurred-background
Jul 17, 2026
Merged

feat(instagram): auto-fit story images to 9:16 with blurred background#123
paulocastellano merged 8 commits into
mainfrom
feat/story-blurred-background

Conversation

@paulocastellano

@paulocastellano paulocastellano commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Problem

A customer reported that Instagram story images that aren't 9:16 get clipped — the platform crops anything that doesn't fill the 9:16 frame. While fixing it, the same cropping showed up in the editor's vertical previews (stories, reels, TikTok, Shorts).

Publish-time fit (Instagram story)

Story images are now fitted onto a 1080×1920 canvas at publish time: the original is contained (never cropped) and a blurred, slightly darkened copy of itself fills the gaps. The background color adapts from the image — no color choice to make.

  • MediaOptimizer::fitToCanvas() — the blurred-background fit (no-op when the ratio already matches; two image decodes, not three).
  • CropsImageForAspectRatio::fitImageToCanvas() — downloads the source, fits it, hosts the derivative under social-crops/ (mirrors the existing crop helper), so publishing never depends on an external URL. If the source can't be downloaded or decoded, it fails with a clean InstagramPublishException (actionable message) instead of a raw decode error, and never leaks its temp files.
  • InstagramPublisher::publishStory() — image stories post the hosted, fitted copy.

Instagram only — Facebook stories are video-only in our flow.

Editor previews (all vertical formats)

The vertical previews rendered media full-bleed, so a correctly-sized 9:16 image was clipped by the phone mockup (which is taller than 9:16). A shared VerticalMediaCanvas now renders every vertical format consistently:

  • Image → shown in full with a blurred-background extension filling the gaps. No crop.
  • Video → full-bleed, like the real feed.

VerticalMediaCanvas is wired into all six vertical previews: IG Story, IG Reel, FB Story, FB Reel, TikTok and YouTube Short. Note the image (blurred-background) branch only ever renders where the format actually accepts images — Instagram Story and TikTok Photo. The other four (IG Reel, FB Story, FB Reel, YouTube Short) are video-only, so only the full-bleed video branch appears there.

The aspect-ratio warning is also suppressed for story images (autoFitsImage, set only on instagram_story), since they're auto-fitted.

Tests

  • MediaOptimizerTest — off-ratio image → 9:16 fit with a darkened, image-derived background asserted by pixel color (not just dimensions); already-9:16 image scales down with no letterbox band; fitToCanvas throws on undecodable bytes.
  • InstagramPublisherTest (both files) — end-to-end story fit with the real optimizer (asserts the hosted social-crops/ copy is 1080×1920 and is what's posted, not the external URL); plus error scenarios: download failure, undecodable 200 body → clean InstagramPublishException, story container-creation failure, and no temp-file leak when hosting fails.

What to test manually

Publish-time fit — the only combination that exercises it:

  • Instagram → Story → image that is deliberately off-ratio (wide 16:9 or square 1:1). Publish and confirm on Instagram: the whole image is visible (no crop) over a blurred, slightly darkened backdrop.
    • Control: an already-9:16 image → no backdrop (short-circuits).
    • Control: a Story video → passes through untouched.

Editor preview — blurred-background image branch is visible for: Instagram Story (image) and TikTok Photo (image, first item).

Editor preview — video-only (don't expect a blur, images are blocked on attach): Instagram Reel, Facebook Story, Facebook Reel, YouTube Short.

Aspect-ratio warning: on Instagram Story + off-ratio image, confirm no "too narrow / too wide" warning appears.

Story images that aren't 9:16 were clipped by Instagram. They are now
fitted onto a 1080x1920 canvas — the image is contained and a blurred,
darkened copy of itself fills the letterbox gaps, so nothing is cropped
and the background color adapts to the image.

The fit happens at publish time (the hosted copy lives in social-crops/),
and the post editor preview now renders the same blurred-background fit
for stories, so the user sees exactly what will publish. The aspect-ratio
warning is suppressed for story images since they're auto-fitted.

Instagram only — Facebook stories are video-only in our flow.
Vertical previews (stories, reels, TikTok, Shorts) rendered media full-bleed,
so a correctly-sized 9:16 image was clipped by the phone mockup, which is
taller than 9:16.

A shared VerticalMediaCanvas now renders every vertical format consistently:
images show in full with a blurred-background extension filling the gaps (no
crop), while videos stay full-bleed like the real feed. This mirrors how the
platforms themselves display the content and matches the story blur applied at
publish time.

Also reduce fitToCanvas to two image decodes instead of three (reuse the probe
as the foreground), lowering peak memory on the story publish path.
- Guard MediaOptimizer::fitToCanvas and cropToAspectRatio against huge-dimension sources (getimagesize budget check) so they fail cleanly instead of exhausting GD memory.
- Fit and crop now translate decode/process failures into a clean InstagramPublishException and remove their temp files via finally; the two paths are symmetric.
- publishStory reads the story canvas dimensions via data_get.
- Previews: restore the IG empty-state background, align the autoFitsImage suppression predicate to isImage, drop the explanatory comment, and use single-quote imports.
- Tests: real end-to-end story fit, undecodable/download/container failures, the memory guard (fit and crop), temp-file cleanup, blurred-background pixel assertions, and the aspect-ratio warning suppression.
… errors

- Extract estimatedDecodeMemory() + a MAX_DECODE_MEMORY_BYTES constant so optimizeImage (fallback) and the crop/fit guard (throw) share one estimate instead of duplicating the formula and threshold.
- Correct the cropFailureException docblock: it now covers download, crop, and story-fit failures, not just downloads.
- Add a Facebook crop process-failure test and an Instagram cropped-temp-leak test so the crop path matches the fit path's error coverage.
@paulocastellano
paulocastellano marked this pull request as ready for review July 16, 2026 23:34
… effect)

These previews are video-only, so routing them through VerticalMediaCanvas changed nothing; keep the shared canvas on Instagram (Story) and TikTok (Photo) where it actually fits off-ratio images.
- Rewrite fitToCanvas to build the blurred story background with Imagick: scale the image to fill the width, heavily gaussian-blur it so shapes dissolve into a colour wash, gamma-lighten it, and mirror the top half onto the bottom for a symmetric background; the foreground is contained (fills the width, never cropped). Falls back to a GD downscale-blur on hosts without ext-imagick.
- Clean up the fit temp file if the blur/encode step throws.
- Update the editor preview (VerticalMediaCanvas) to a matching mirrored, lightened blur so it tracks the publish output.
- Cover the lightened image-derived background, the vertical mirror, and the GD fallback path with unit tests.
@paulocastellano
paulocastellano merged commit 67cf43a into main Jul 17, 2026
5 checks passed
@paulocastellano
paulocastellano deleted the feat/story-blurred-background branch July 17, 2026 13:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant