Keeping Photo Editor Previews and Exports in Sync with TypeScript and Sharp
A user positions a portrait inside a frame, checks the preview, and clicks Export. If the downloaded image crops their hair differently, the editor has broken a basic promise: the layout they approved should survive the export. I build Photocard.ai, a tool for creating custom photocards. Its Maker pipeline combines uploaded photos with template artwork. That means handling photo placement, background removal, decorative layers, and multiple output sizes. The useful architectural decision was to make those composition decisions explicit and save them in a versioned render plan. Preview and high-resolution rendering can then consume the same decisions. This article focuses on that template renderer. The TypeScript examples are simplified illustrations of the design, with storage and application plumbing omitted. The goal is consistent composition across resolutions. A lightweight browser preview can still differ from a final render in hair detail, filtering, and edge quality. Those are separate things to validate. Save the decisions that produced the image A template thumbnail shows the intended appearance. It cannot tell the renderer which source belongs in each slot, where to crop it, or which decoration belongs in front of the person. In the Maker implementation, a template manifest describes those rules. A render plan resolves them for a particular set of uploads. A reduced representation looks like this: type Rect = { x: number; y: number; width: number; height: number; }; type RenderSlot = { sourceIndex: number; sourceCrop: Rect; // Relative to the oriented source image destination: Rect; // Relative to the output canvas zIndex: number; subjectAlphaKey?: string; refinedForegroundKey?: string; effectSeed: number; }; type RenderPlan = { version: 2; templateVersionId: string; assetPackageVersion: string; outputVariant: string; slots: RenderSlot[]; }; The application has additional fields for clipping, filters, subject treatments, and edge processing. The principle stays small: save the information needed to explain the composition. When an export starts, it should load the saved crop. Running an automatic crop detector again creates another opportunity to choose a different face position. The same applies to decorative effects. If a paper-edge treatment uses randomness, save its seed so another render does not choose a different pattern. Version numbers also need something concrete behind them. Keep the corresponding artwork and processing behavior available for as long as old cards must remain editable. A version string cannot recover an overwritten asset. Let each template slot choose its source A portrait inside a film frame may need the complete original photo. A sticker composition may need a transparent person. The manifest makes that choice explicit: type SourceComposition = "original_photo" | "cutout_subject"; type TemplateSlot = { sourceIndex: number; sourceComposition: SourceComposition; frame: Rect; zIndex: number; }; This prevents a subtle failure: applying background removal to a slot simply because a cached mask happens to exist. For the normal full-photo path, the renderer retains the scene. For a cutout slot, a missing required mask is a validation error. Explicit editing features, such as an outline mode, can introduce their own rules, but the presence of an optional asset should never decide the visual policy by accident. This matters for group photos, too. A single-person mask and the original group image contain different subjects. Choosing between them changes the content of the card. Separate source coordinates from canvas coordinates There are two rectangles in a crop operation: The source crop selects a region of the uploaded image. The destination frame places that region on the output canvas. Both can use normalized coordinates. An x value of 0.1 means ten percent of the relevant image's width. The source rectangle is measured against the oriented upload; the destination is measured against the canvas. Consider a destination frame of { x: 0.1, y: 0.1, width: 0.8, height: 0.8 }: Canvas Left Top Frame width Frame height 600 × 900 60 90 480 720 2400 × 3600 240 360 1920 2880 These are illustrative sizes with the same aspect ratio. The placement scales without a new layout decision. Convert to pixels at the rendering boundary. This helper covers rectangles fully inside an image: function toPixels(rect: Rect, width: number, height: number) { const values = [rect.x, rect.y, rect.width, rect.height]; if ( !Number.isInteger(width) || width
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to