Repository navigation
Paint a layout as vectors: paint.PaintVector, PageLayout, pdfcanvas - #297
Merged
Merged
Conversation
paint.Canvas is a small vector surface (clip, fill, shade, image, glyph runs); paint.PaintVector walks the box tree in paintBoxContent's order and geometry and draws through it: backgrounds, borders and sharp shadows as paths, linear and radial gradients as shadings split into runs of constant opacity, text as positioned glyphs of the faces the layout measured with, images placed in their boxes. What has no vector form (opacity < 1, filter, mask-image, rotation, blurred shadows, fading gradients, form controls) is painted by the raster painter as a bitmap of that element only, its text laid over it invisibly so that it stays selectable. Engine.LayoutHTML/LayoutDocument return a PageLayout that can be painted as an image (identical to RenderHTML) or as vectors, so pages can be laid out concurrently and painted in order. pdfcanvas is the Canvas for a go-pdfkit page (v0.15.0: ShowGlyphs, NewFont, radial shadings). css.GradientSampler.Geometry exposes the resolved axis/ellipse and stops. The raster path is untouched. paint keeps 100 % coverage; the vector tests compare a reference rasterisation of the Canvas calls with PaintFull. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
What
A vector backend for the paint layer, and the API to drive it page by page:
paint.Canvas— a small vector surface (Save/Restore,Clip(path, evenOdd),Fill(path, colour, evenOdd),Shade(Shading),Image(img, rect),Glyphs(*GlyphRun)), withPath(lines, cubics,RoundRect,Ellipse).paint.PaintVector(c, box, fonts, imgs, bgImgs, page) VectorStats— walks the box tree inpaintBoxContent's order with its geometry and clipping, emitting paths, shadings, images and glyph runs.engine.(*Engine).LayoutHTML/LayoutDocument→*PageLayoutwithPaint()(the imageRenderHTMLreturns, byte-identical — tested),PaintVector(c),Links(),Info(),Page().pdfcanvas— thepaint.Canvasfor a go-pdfkit page (needs go-pdfkit v0.15.0:ShowGlyphs,NewFont, radial shadings).css.GradientSampler.Geometry()— the resolved axis/ellipse and stops, so a backend can emit a shading instead of sampling.Design note — why this shape
Read first.
paint.gopaints straight into an*image.RGBA(go-widgets/painter for rects, coverage masks for glyphs, per-pixel loops for gradients/shadows/tiles); there is no display list or painter interface. Two options:paint.gobehind an interface so raster and vector share one walker. It touches ~1,800 lines that other sessions edit daily, and every raster primitive (per-pixel erf shadows, group buffers, filter chains) would need a vector twin or an escape hatch inside the abstraction — a large, risky diff for the raster path, which must stay byte-identical.layout.Boxtree (chosen). Layout is shared — positions, line breaks, fonts and image sizes come from the one layout pass — so the vector page has the raster page's layout by construction; only painting is duplicated, and it mirrorspaintBoxContentstep for step (shadows, background, layers, inset shadows, borders, overflow/clipclipping, marker, inline fragments, items, children). The raster path is untouched.What is vector: backgrounds and borders (rounded: a ring between two rounded rects, even-odd), sharp box-shadows (clip-excluded / even-odd), linear and radial gradients (type 2 / type 3 shadings; a CSS gradient is split into runs of constant opacity — the uncover theme's hard stop to
transparentis vector), text as positioned glyphs of the faces the layout measured with (exact advances, asFonts.Measure; fallback-face runs;rligligatures carry their whole text for/ToUnicode),<img>and background images placed in their boxes (the source bitmap, so identical images dedupe across pages), list markers, underlines, overflow andclip: rect()clipping.Per-element raster fallback for what has no vector form here: a subtree under
opacity< 1,filter,mask-image,transform: rotate()orbackdrop-filter; a blurred box-shadow; a gradient whose opacity fades between stops; a form control; a background image tiled > 1024 times. The raster painter paints just that piece into a page-sized scratch buffer (bounded: 64 Mpx), cropped to its ink and placed as an image. The text of a rasterised subtree is laid over its bitmap invisibly (text render mode 3), so it stays selectable.VectorStatscounts it all.Alternative considered: go-pdfkit/html2pdf already drives the layout tree into pdfkit, but it is a paginating document exporter with a reduced painter (no radius, gradients, shadows,
@font-face, opacity); a full-fidelity per-page painter belongs next to the raster painter it mirrors, and html2pdf can adoptPaintVectorlater.Bibliography (opened)
ISO 32000-1:2008 (Adobe's PDF32000_2008.pdf): §8.5.4 Clipping Path Operators (
W/W*n), §8.7.4.5.3 Type 2 (axial) and §8.7.4.5.4 Type 3 (radial) shadings, §9.3.6 Text Rendering Mode (mode 3), §9.4.3 Text-Showing Operators (TJ), §9.10.3 ToUnicode CMaps, §11.6.4.4 Constant Shape and Opacity, §11.6.5.3 Soft-Mask Images. Engine:paint/paint.go,paint/fonts.go,css/gradient_sample.go,engine.go(renderCore,newCanvas),linkmap.go.Verification
paint/vector_test.gohas a reference rasteriser for the Canvas (point-sampled winding, shadings, images, glyph masks) and checks pixel parity againstPaintFullon every feature (backgrounds, borders, rounded rings, 13 gradient shapes, background tiles, 8 shadow kinds, markers, images with object-fit and filters, groups, overflow andclip): ≤ 2–4 % of pixels off by > 64 (antialiasing and sub-pixel glyph positions).paintcoverage stays 100 %,css99.5 %.pdfcanvastests read the PDF back with rsc.io/pdf: Type0 fonts with/ToUnicode(é included), axial and radial shadings, XObjects,3 Trfor the ghost text.pdftoppmat 96 dpi vs the raster render, per slide): 100-slide deck 0.46 % mean pixels off (worst slide 1.2 %), text extracted bypdftotext; 758 KiB against 10 MiB raster and marp-cli's 837 KiB.MINOR: new API → next minor tag after merge.
🤖 Generated with Claude Code