Electrical plans that open fast and stay sharp on a TV
Opening an architect’s PDF on the showroom TV meant seconds of blank screen, and zooming in blurred the lines. Every page is now pre-rendered as an image that opens at once, and redrawn from the PDF when someone zooms in.
The problem
A plan’s PDF took seconds to open on the TV, and zooming in blurred the drawing.
The fix
Pre-render every page as an image, and redraw from the PDF only when someone zooms.
- 1,800 px
- pre-rendered page, about 200 to 350 KB
- 3,200 px
- redrawn from the PDF on zoom (2,400 on the TV)
- 2
- full pages decoded at once for thumbnails
Every building page in the deals map carries its electrical plans: the architect’s drawings of where each switch, light and socket goes. “We already have your building’s plans” is one of the strongest things a consultant can say to a visitor, so a plan has to open at once on the showroom TV and stay readable when someone leans in. At first it did neither. Claude Code wrote most of the code; I decided what the TV should never have to do.
First, whose plan is this?
Plans arrive as PDFs attached to deals in the CRM. Of 887 plans, 367 had no building name of their own and 32 more just said “NA”, so matching on the plan’s own field placed only 475. The fix was a second route: when that field doesn’t name a known building, use the building on the deal the plan was uploaded against. A column records which route placed each plan, so a wrong match can be traced.
Architect names needed the same care. An earlier enrichment step had stored its guesses side by side, one read off the drawing and one from a web search, or simply “Not Found”. The app shows a name only when it reads like a practice: an architect, a studio, a design firm. Otherwise it shows nothing, because on a showroom screen no name beats the wrong one.
A PDF is a slow way to show a picture
In a TV’s browser, pdf.js has to download about 600 KB, start a worker and draw an A3 sheet before anything appears: a second or two of blank screen with a visitor watching.
So every page is rendered once, ahead of time, to a 1,800 px WebP image. That’s enough to read a room label with the sheet filling a 4K screen, and it keeps a typical sheet near 200 to 350 KB. Opening a plan became an image load. Plans that were already images are linked as they are.
The batch that kept stalling
The rendering runs in headless Chrome, driven by a Node script, because pdf.js wants a real canvas and Chrome already has one. Two things went wrong at scale:
- One malformed PDF hung pdf.js, and the run sat on that file for nine minutes before anyone noticed. Every call now races a timer, and a failure gets a whole new browser.
- The browser leaked. After about 450 plans, every call timed out and reloading no longer helped. Chrome is now replaced every 80 plans.
The job only queues plans with no rendered pages yet, so it can stop and restart without losing work.
Thumbnails without the memory bill
There are no thumbnail files, and showing full pages in a grid would decode about 11 MB of bitmap per card, on a TV with 2 GB of memory. Instead each page is loaded once, drawn down to 360 px and kept as a small JPEG, and the full image is let go. Two load at a time, so the most it ever holds is two full pages.
Sharp when zoomed
In the viewer, OK zooms to 2× and then 3×, and the arrows pan. At 2× on the TV, every line of the 1,800 px image is stretched. So when the original is a PDF, the page is redrawn from it at the zoomed size:
// longest side capped for memory: 3,200 px, or 2,400 on the 2 GB TV
scale = min(CAP / longestSide(page), shownWidth * zoom * pixelRatio / page.width)
render(page, scale) // then swap in with the same box and zoom, so nothing jumps
Until the sharper page is ready, the picture stays as it is and a small “Sharpening…” label shows. Every request carries a token, so a page that finishes after you’ve moved on is thrown away instead of landing on the wrong sheet. And a plan’s PDF is released when the viewer closes or moves on; pdf.js had been keeping every one it opened until the page reloaded.
What’s still open
Opening one plan by hand once showed another building’s name in its title block. A script that reads every title block and flags plans filed under the wrong building is written. It needs a run, and a person to settle each mismatch.
What I’d keep
- Pre-render for speed, keep the original for detail. The image opens; the PDF zooms.
- Give batch jobs timeouts and fresh starts. Hope isn’t a retry policy.
- Decode small. A thumbnail shouldn’t cost a full page.
- Swap in place. Same box, same zoom, no flicker.
- Record how each match was made. A wrong one should be traceable.