feat(instagram): auto-fit story images to 9:16 with blurred background#123
Merged
Conversation
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
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 undersocial-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 cleanInstagramPublishException(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
VerticalMediaCanvasnow renders every vertical format consistently:VerticalMediaCanvasis 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 oninstagram_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;fitToCanvasthrows on undecodable bytes.InstagramPublisherTest(both files) — end-to-end story fit with the real optimizer (asserts the hostedsocial-crops/copy is 1080×1920 and is what's posted, not the external URL); plus error scenarios: download failure, undecodable 200 body → cleanInstagramPublishException, 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:
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.