Carousel PDF export, per-page PNGs, and a display-independent export size - #5
Open
pallaoro wants to merge 3 commits into
Open
Carousel PDF export, per-page PNGs, and a display-independent export size#5pallaoro wants to merge 3 commits into
pallaoro wants to merge 3 commits into
Conversation
…export size A swipeable LinkedIn carousel can only be published as a multi-page document post, i.e. a PDF. The editor already had multi-page designs, but Export only ever emitted the active page — so a finished carousel had no way out. Export is now a menu: - Carousel PDF — every page, in canvas order, one file - This page — PNG - All pages — one PNG per page Exports are named after the design instead of always "design.png", and the PDF is assembled in the browser, so no page content leaves the machine. Also fixes the export resolution. Each canvas is backed at the display's device pixel ratio, and that ratio is already folded into the dimensions toDataURL scales, so a fixed 2x multiplier produced a 4320px file on a retina laptop and 2160px on a 1x monitor from the same design. Dividing the multiplier by the canvas zoom cancels the ratio out and pins every export to 2x the design's own size. Adds the 1080x1350 (4:5) carousel canvas size, and skips jsPDF's optional extras — canvg pulls in a native node-canvas build that would make `pnpm install` require cairo.
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.
Why
A swipeable carousel on LinkedIn is a document post. Per LinkedIn's own help page, that means PDF/DOC/DOCX/PPT/PPTX, up to 300 pages and 100 MB. The short-lived native multi-image carousel LinkedIn shipped in 2022 was removed in June 2023, with existing carousel posts deleted that December. (Carousel Ads is a separate paid ads product, still live, and unrelated.)
OpenDesign already had multi-page designs and a page-per-slide canvas. It had no way to get them out: Export emitted only the active page, as a PNG always named
design.png. You could build a 10-slide carousel in this editor and then be stuck.What changed
Export is a menu:
Files are named after the design (
q3-recap.pdf), notdesign.png. The PDF is assembled client-side with jsPDF, so page content never leaves the machine and the template keeps working when self-hosted.Export resolution is now display-independent. Each canvas is backed at the display's device pixel ratio, and fabric folds that ratio into the dimension
toDataURLscales — so a fixedmultiplier: 2produced a 4320px file on a retina laptop and 2160px on a 1x monitor, from the same design. Dividing the multiplier by the canvas zoom cancels the ratio back out and pins every export to exactly 2x the design's own size.Also: adds the 1080x1350 (4:5)
LinkedIn Carouselcanvas size.Why client-side, given the house pattern is the PDF service
open-slides,open-booksandopen-personal-trainingall render PDFs throughservices.clawnify.com/pdf/render. That is the right call for those: their content is HTML, so an HTML→PDF renderer produces vector text directly.This app's content is a fabric canvas, not HTML. Routing it through the service would mean wrapping N base64 PNGs in HTML and POSTing multi-MB bodies to a remote browser — same raster output as doing it locally, plus a round trip, plus metered quota, plus a
501when self-hosted. It would also reintroduce a fidelity class this repo has already been bitten by:open-slides/src/server/reveal.ts:122-125records reveal.js re-laying-out atpage.pdf()time and overflowing Cloudflare's renderer, which forced a deterministic fixed-block workaround. Rasterizing the canvas locally cannot have that bug.A vector PDF via
canvas.toSVG()→ svg2pdf was considered and rejected: font embedding for 10 Google Fonts is the hard part, and LinkedIn rasterizes document posts for display anyway, so the vector win is lost at the destination.Cost
jsPDF's optional extras (
canvg,html2canvas,dompurify) backdoc.svg()anddoc.html(). They are referenced only atjspdf.es.js:21291-21345and:13482-13750respectively;addImageis at:9328andaddPageat:3628, and neither touches them. They're skipped at install (canvgpulls a native node-canvas build that would makepnpm installneed cairo) and aliased to a stub so the bundler can still resolve the specifiers.The export no longer deselects your object mid-export — fabric sets
skipControlsDrawingwhile rendering, so handles never reach the file anyway.Verification
tsc --noEmitintroduces no new errors andvite buildpasses. The export path was exercised headlessly against real jsPDF with real PNG bytes:The landscape case guards a real trap: jsPDF silently swaps the page box to match
orientation(jspdf.es.js:14213-14225), so a landscape design comes out portrait unless the orientation agrees with the format array.Not verified: the toolbar menu has not been clicked in a browser — it is typechecked and builds, but no human or agent has driven the UI.
Known ceiling
"All pages" fires one download per page, staggered 250ms. That is fine for the 5-20 slides a social carousel runs to. It is not the binding limit: the editor mounts every page's canvas simultaneously at DPR-squared backing size, so a very long design exhausts memory in the editor long before export becomes the problem. Both are pre-existing to this PR.