
Project Story
AI photo enhancement and background removal that sizes a family's photo for the print it goes on
Families often hand a funeral home one small or old photo, and Messenger prints it on everything from an 8.5 x 11 program to a 24 x 36 banner. We added two AI edits to the My Designer Lab editor, Enhance and Remove Background, that size the result for where the photo is placed. A person compares before and after on a slider and pays only if they keep it.
- Client
- Messenger
- Industry
- Memorial print
- When
- 2026
- Status
- Released in October 2026, behind an admin switch
- React
- Vite
- Recoil
- Express
- Bull
- Redis
- Sharp
- Pintura
- OpenAI image edits API
- Next.js
- tRPC
- Zod
- Prisma
- PostgreSQL
- Amazon S3
- Datadog
One photo, printed very large
When someone dies, the family picks the photo, and it’s often small, old or both. The funeral home uploads it to a case in My Designer Lab, and the photo goes on everything from the program to a casket panel. A photo that looks fine on a phone comes out soft at 24 x 36 inches.
The editor already did some of this. In December 2025 we shipped the Messenger Image Optimizer, which runs on every upload and returns gamma, brightness, contrast and saturation adjustments that the editor applies through Pintura, the image editing library inside My Designer Lab. That helps a dark photo and leaves a small one exactly as small.
So in October 2026 we added two AI edits. Enhance is a generative upscale, run through OpenAI’s image edits endpoint, that draws new detail into a blurry or low-resolution photo. Remove Background cuts the person out, using an open-source background removal model on a private GPU endpoint in Messenger’s own account. Both cost money every time they run, so both show the person a before and after and bill only for results they keep.
Sized for the print
Enhance first topped out at 2,496 pixels on the long edge. At 200 pixels per inch, the threshold Messenger uses for paper, that’s about 12.5 inches, which is why banners and casket panels kept showing the low-resolution warning after an enhancement.
Now every request asks the model for the largest size it accepts at the photo’s shape: both edges a multiple of 16, neither over 3,840 pixels, at most 8,294,400 pixels in all. A 2:3 portrait comes back at 2,336 x 3,520. The source is shrunk to a 4,096 pixel long edge before it’s sent, with the same restoration prompt every time.
The bigger change is where the request starts. The canvas in the editor measures each placed photo’s effective resolution at the size it’s printed. When a photo there is under target, its warning offers “Enhance image size”, and that request carries the pixel size the placement needs to print at 200 ppi. At a fixed placement, effective ppi grows in step with pixel size, so the size needed is the measured size times 200 over the measured ppi. When the model’s output is smaller than that, a Lanczos resample grows it the rest of the way. The model draws the detail and the resample supplies the size. An output that already covers the target is kept exactly as the model returned it.
For a photo filling its spot, before and after:
- 8.5 x 11 page: 196 to 275 ppi
- 24 x 36 banner: 69 to 200 ppi
- Casket panel: 79 to 200 ppi
- Full-bleed floor banner: 31 to about 93 ppi
The floor banner is the one we couldn’t fix. Targets are capped at 36 megapixels so the editor and the PDF renderer stay responsive, which is enough for a 24 x 36 print at 200 ppi. A full-bleed photo on the 33.75 x 79.25 inch floor banner would need about 60 megapixels even at 150 ppi. It still warns. Bigger outputs also mean longer waits. A generation at the model’s largest size takes about 100 seconds.
There’s one shape the model won’t take. The endpoint refuses anything whose long edge is more than three times its short edge, so a panorama strip can’t be enhanced. The editor hides Enhance once it has measured a photo like that, and the backend refuses it with ASPECT_UNSUPPORTED before any row exists, so nothing reaches the model and nothing is billed.
Every attempt is a paid call
Each generation becomes one row in CaseImageVariant, with a kind (UPSCALE or BG_REMOVAL), a status, the source variant it was made from, whether it was accepted and which order line billed it. That one row is both the job status the editor polls and the billing record. The upload pipeline around it already existed: the browser puts the file straight into S3 through a presigned URL, and a Bull worker runs Sharp and the optimizer before the photo is committed.
A variant job goes onto the same queue. The start route creates the row and enqueues the job in one idempotent step, so a double click or a second tab reuses the generation already running instead of paying for another. Discarding a result and pressing Enhance again reopens that same result for free. The editor polls every 2 seconds for up to 240, which is the model’s 180 second budget plus a minute for the queue, the resample and the upload, so a slow generation that succeeds is never shown as an error.
Every failure is final. We turned retries off in the OpenAI SDK, because a retry is a second paid generation, and the only retry is the person pressing “Try again”. Failures carry a stable code that the editor translates, and each code decides whether “Try again” is offered:
MODERATION_BLOCKEDwhen the model refuses the photoSPEND_LIMITwhen the API account has hit its limitMODEL_ERRORandENQUEUE_FAILED, which can be retriedDAILY_CAP_REACHED, from a ceiling of 200 Enhance generations a day per environment, where 0 works as a kill switch
The background removal endpoint scales to zero when it’s idle, so a request to a cold endpoint gets a 503 while its T4 GPU wakes up. The client waits 15 seconds and tries again, inside a 240 second budget measured in elapsed time, and only starts a retry when at least 30 seconds are left, about what a warm removal takes. A timeout or a connection reset partway through a request is final, since the GPU may already have done the work.
For local development and tests, each model has a fake adapter that refuses to load in production. The fake upscaler runs the same resample step, so local results come out at production sizes.
Compare, then keep it or don't
The first time someone runs Enhance, the editor shows what it costs per use and a notice Messenger wrote. Generative upscaling invents new detail to fix blur, and it can occasionally change a subtle facial expression, a skin texture or something in the background. That matters more on a photo of someone who just died than almost anywhere else. The notice is in English, Spanish and French, and it shows again on the result screen before the billable button, because a person who dismissed the first screen skips it after that.
The result opens in a compare slider, a draggable and keyboard-operable divider between the original and the enhancement. “Accept Enhancement” is the only action that can bill. Discard leaves the result unaccepted.
Once accepted, the enhancement becomes the photo. One rule picks which file represents a case photo (the newest accepted upscale, then the optimizer’s copy, then the upload), and every reader uses it: the editor canvas, the PDF renderer, the proof screenshots, the memorial slideshow and the Memorial Maker storefront. Accepting moves every saved placement of that photo on every layout page. The tile and the canvas item carry an “Edited with AI” tag, which is drawn only in the editor and never prints.
There’s no un-accept, because the charge stands. “Revert changes” shows the original again for everyone and everywhere the photo is used, and “Show enhancement” brings it back at no charge.
Remove Background is a tab in the editor’s Adjust Image modal, beside Pintura’s own crop and filter tools, and it follows the same steps with the same slider. It cuts out whatever the editor is showing. When that’s an accepted enhancement, the backend cuts out the enhancement’s file, or a photo shown at 3,840 pixels would come back as a cutout the size of the original upload. A photo stored sideways with an EXIF orientation tag is turned upright first, since the model ignores the tag. If the item already has crops or filters, those edits are applied again to the cutout and saved as a PNG so the transparency survives.
Billing once, at checkout
Nothing is charged at the moment of accept. The first order for a case that checks out counts every accepted variant no earlier checkout billed, writes one charge row per kind on that order and marks those variants billed in the same transaction. If two checkouts race over one case, the one that can’t mark every variant it charged rolls back. The checkout page sends the AI total it showed, and when a price changed in between, checkout refuses and reloads, so nobody pays an amount they didn’t see.
An admin sets each kind’s price in Memorial Maker’s Site Admin, and the editor asks for the current price every time a tool opens. Remove Background won’t start until that price is on screen.
Five days after launch, Messenger asked that edits a designer makes on a family’s behalf not be charged to the family. A designer opens the editor through an order’s edit link, which the backend resolves as the order’s owner, so their accepts looked exactly like the customer’s. We added an acceptedByStaff flag, written in the same update that marks the accept, and every billing query leaves those rows out. Designer and QC links and internal staff sessions are covered. A staff member working on the customer’s own link is still billed like the customer, because that link carries the customer’s role, and we wrote that limit into the docs.
An off switch, and where it's offered
Both edits sit behind one switch for every funeral home at once, and it started off. A super admin flips it in Site Admin. The write carries the value the admin’s page showed, so if another admin flipped it in the meantime the save is refused, and each change writes an audit row with the old and new value. The backend reads it on every request, so turning it on or off needs no deploy. An edit accepted while the switch was on keeps showing and printing after it’s turned off.
In November, Messenger asked that the edits be offered only on Memorial Maker orders. The backend now refuses any start, accept or revert from an editor opened on a My Designer Lab order before it reads the switch, and the editor hides every entry point there.
To be clear, we don’t have usage numbers yet. The switch stayed off in production through the follow-up release on October 23, and we can’t tell you how many families have kept an enhancement or what it has earned Messenger.
How it shipped
It went out on October 20 as a stack of 13 pull requests, each reviewed on its own, from the shared contract (error codes, Zod schemas, the size rules) up through the worker, the editor, checkout and the admin switch. Eleven more followed from QA and code review and were tested together, among them the fix that moves every saved placement of a photo on accept.
The editor’s tests run the real components, hooks and store with only the backend calls replaced. One follows a photo from the canvas warning through the server’s size rule and checks the warning clears on a 24 x 36 banner, a casket panel and a 5 x 7 card and stays on the floor banner. Checkout and the admin prices run against a real database.
Related case study
Messenger & My Designer LabQuestions about this project
HQ, a software design and development agency in Ogden, Utah, built AI photo enhancement and background removal into My Designer Lab, the editor Messenger's funeral home customers use to design memorial print products. Enhance uses OpenAI's image edits endpoint to upscale a photo, and a resample sizes the result to print at 200 ppi where it's placed. Remove Background runs an open-source model on a private GPU endpoint. Each result is compared on a slider and billed only if the person accepts it. It shipped in October 2026.
The editor measures a placed photo's effective resolution, and when it's under target the low-resolution warning sends the pixel size that placement needs to print at 200 ppi. The model generates at the largest size it accepts, up to 3,840 pixels on an edge, and a Lanczos resample grows the result the rest of the way, capped at 36 megapixels. A photo filling a 24 x 36 inch banner went from 69 to 200 ppi. A full-bleed photo on the largest floor banner only reaches about 93 ppi because of that cap.
Each generation is stored as a variant row, and only a variant the person accepts can be billed. Checkout counts accepted variants no earlier checkout billed, writes one charge per kind on the order and marks the variants billed in the same transaction, rolling back if two checkouts race over one case. Failed generations are never billed. Edits accepted through a designer or QC link or by internal staff aren't charged to the customer, at Messenger's request. An admin sets each kind's price, and the editor shows the current price before anything billable starts.
Every failure is final, because each attempt is a paid model call, so retries are off in the OpenAI SDK and the person decides whether to try again. Failures carry stable codes such as MODERATION_BLOCKED, SPEND_LIMIT and DAILY_CAP_REACHED, and each code decides whether the editor offers "Try again". A daily ceiling of 200 Enhance generations per environment, with 0 as a kill switch, caps spend. The background removal endpoint scales to zero, so the client retries only a 503 or a connection that never reached it, inside a 240 second budget.
Yes. The editor shows the original and the enhancement side by side on a compare slider before anything is accepted, and after an accept, "Revert changes" shows the original again for everyone and everywhere the photo is used, including the printed PDF. "Show enhancement" brings the enhancement back at no extra charge. Before any enhancement runs, Messenger's notice explains in English, Spanish and French that generative upscaling can change subtle facial expressions, skin textures or background details.
Let's build together
Whether you're looking to design, build, update, or refresh an existing app or website, we can help. Even if we're not a good fit, we will help point you to somebody who might be.
Contact us