// Package d2raster renders the supported d2scene subset with a pure-Go raster // kernel. Unsupported scene features are rejected during preflight, before the // final output canvas is allocated.
package d2raster import ( ) // FrameOptions defines the pixel mapping and hard resource ceilings for one // render. Every limit and Scale must be positive; callers select the limits so // production ceilings are explicit rather than hidden package defaults. type FrameOptions struct { Scale float64 Time time.Duration Background color.Color MaxWidth int MaxHeight int MaxPixels int64 // Stored vector definitions are charged once by structural graph // validation. Every rendered vector Image then charges its instantiated // subtree again. Animation limits follow the same retained-plus-instantiated // policy and are charged before per-node animation bookkeeping is allocated. // Retained MaxDepth validation includes the unavoidable host Image at depth // one, so an intrinsically unplaceable definition is rejected even if unused. // MaxNodes also bounds aggregate filter entries and synthesized color-glyph // layers as separate structural dimensions, preventing arbitrarily long // identity-filter chains or color-font layer lists. MaxNodes int MaxDepth int MaxPathCommands int // MaxTextRunesPerRun bounds one non-interruptible segmentation/shaping // call. Zero selects the default; negative values are invalid. // Aggregate shaped glyphs remain bounded by MaxPathCommands. MaxTextRunesPerRun int // MaxFontFacesPerText bounds primary plus fallback face selection for one // TextRun. MaxTextCoverageChecks and MaxTextShapingRuns are aggregate frame // work ceilings. Zero selects bounded defaults. MaxFontFacesPerText int MaxTextCoverageChecks int64 MaxTextShapingRuns int MaxAnimationTracks int MaxAnimationKeyframes int MaxAssets int MaxAssetBytes int64 // MaxDecodedAssetBytes bounds the cumulative resolver-declared decoded // footprint of all retained raster assets. MaxAssetBytes separately bounds // their encoded bytes together with retained font bytes. MaxDecodedAssetBytes int64 // MaxImportDepth bounds active vector-asset nesting. Retained vector roots // and visible vector Image instances start at depth one; raster images do // not consume this budget. MaxImportDepth int // MaxOffscreenBytes bounds the peak live pixel backing storage used by // retained pattern tiles, temporary RGBA effect/filter layers, Alpha blur // and effect masks, and retained scanline-rasterizer scratch. It excludes // decoded assets (bounded separately), the final frame canvas, and returned // image. MaxOffscreenBytes int64 // MaxEvenOddClipWork bounds aggregate point-in-edge evaluations used by // supersampled even-odd fills and clips. MaxEvenOddClipWork int64 // MaxScanlineWork bounds aggregate operation units used by non-zero fills, // strokes, streamed paint coverage, and clips. Zero selects a bounded default; // negative values are invalid. MaxScanlineWork int64 } type preparedDocument struct { width int height int background *color.NRGBA root *preparedNode patterns []*preparedPatternTile resources renderResources } type preparedNode struct { opacity float64 blend d2scene.BlendMode isolated bool primitive *preparedPrimitive children []*preparedNode filters []preparedFilter clip *preparedClip mask *preparedMask contentBounds image.Rectangle bounds image.Rectangle } type preparedPrimitive struct { referenceBounds image.Rectangle subpaths []subpath strokeRuns []strokeRun transform d2scene.Matrix fill *preparedPaint fillRule d2scene.FillRule evenOddSubpaths []subpath evenOddEdges int64 stroke *preparedStroke image *preparedImage vector *preparedNode bounds image.Rectangle } type preparedClip struct { referenceBounds image.Rectangle subpaths []subpath fillRule d2scene.FillRule bounds image.Rectangle edges int64 } type preparedMask struct { kind d2scene.MaskType root *preparedNode } type preparedStroke struct { referenceBounds image.Rectangle paint *preparedPaint width float64 cap d2scene.LineCap join d2scene.LineJoin miterLimit float64 dashes []float64 dashOffset float64 } type animationOverrides struct { fillColor *color.NRGBA strokeColor *color.NRGBA dashOffset *float64 dropShadows map[int]d2scene.DropShadow } type preflight struct { ctx context.Context document *d2scene.Document options FrameOptions frameBounds image.Rectangle nodes int pathSegments int textRunes int textCoverageChecks int64 textShapingRuns int shapedGlyphs int animationTracks int animationKeyframes int filters int active map[*d2scene.Node]bool activeAssets map[d2scene.AssetID]bool fonts map[d2scene.AssetID]*preparedFont rasters map[d2scene.AssetID]*preparedRasterAsset vectors map[d2scene.AssetID]d2scene.VectorAsset patternTiles map[preparedPatternTileKey]*preparedPatternTile assetBytes int64 decodedBytes int64 session *RenderSession shapingWorkspace fontface.ShapingWorkspace } type rasterScratch struct { regionFilters bool rasterizer *scanline.Rasterizer rasterizerEdges int rasterizerBounded bool offscreen offscreenBudget patternTiles []*preparedPatternTile patternBytes int64 scanlineWork scanline.WorkBudget scanlineWorkSet bool } func ( *rasterScratch) ( image.Rectangle) *scanline.Rasterizer { if .rasterizer == nil { .rasterizer = scanline.NewRasterizer(.Dx(), .Dy()) if .rasterizerBounded { .rasterizer.ReserveEdges(.rasterizerEdges) } } else { .rasterizer.Reset(.Dx(), .Dy()) } return .rasterizer } func ( *rasterScratch) () *scanline.WorkBudget { if !.scanlineWorkSet { .scanlineWork = scanline.NewWorkBudget(defaultMaxScanlineWork) .scanlineWorkSet = true } return &.scanlineWork } // Render preflights document and returns a newly allocated NRGBA frame. The // viewbox is mapped to LogicalWidth x LogicalHeight and then multiplied by // FrameOptions.Scale. func ( context.Context, *d2scene.Document, FrameOptions) (*image.NRGBA, error) { return render(, , , nil, nil) } // Render preflights and renders one frame while reusing this session's bounded // parsed-font and decoded-raster cache. func ( *RenderSession) ( context.Context, *d2scene.Document, FrameOptions) (*image.NRGBA, error) { if == nil { return nil, fmt.Errorf("d2raster: nil render session") } return render(, , , , nil) } // RenderWorkspace retains at most one final-frame backing store and one // exact-plan scanline rasterizer for a sequence of renders. Before each call it // drops rasterizer capacity that exceeds the new MaxOffscreenBytes limit. Its // zero value is ready for use. Render serializes concurrent calls, while the // separate RenderSession remains safe to share with other workspaces. // Canvas capacity can retain the largest frame reached by a call, including a // call canceled after painting begins, for the workspace's lifetime. Reset // releases all retained storage when an operation ends. A RenderWorkspace must // not be copied after first use. // // The frame passed to consume is borrowed and is valid only until consume // returns; callers that retain frames must continue to use RenderSession.Render, // which returns independently owned pixels. Every call still performs complete // preflight and applies all limits from its FrameOptions. type RenderWorkspace struct { mu sync.Mutex canvas image.RGBA rasterizer *scanline.Rasterizer rasterizerPlan renderWorkspaceRasterizerPlan } type renderWorkspaceRasterizerPlan struct { width, height int edges int bytes int64 } // Render paints one frame into reusable workspace storage and calls consume // before that storage can be reused. The workspace remains exclusively held // while consume runs, so consume must not re-enter the same RenderWorkspace. func ( *RenderWorkspace) ( context.Context, *RenderSession, *d2scene.Document, FrameOptions, func(*image.NRGBA) error) error { if == nil { return fmt.Errorf("d2raster: nil render workspace") } if == nil { return fmt.Errorf("d2raster: nil render session") } if == nil { return fmt.Errorf("d2raster: nil render workspace consumer") } if == nil { = context.Background() } .mu.Lock() defer .mu.Unlock() .trimScratch(.MaxOffscreenBytes) , := render(, , , , ) if != nil { return } if := .Err(); != nil { return } if := (); != nil { return } return .Err() } // Reset releases all final-frame and scanline storage retained by w. It waits // for an active Render and is safe to call on a nil workspace. func ( *RenderWorkspace) () { if == nil { return } .mu.Lock() .canvas = image.RGBA{} .rasterizer = nil .rasterizerPlan = renderWorkspaceRasterizerPlan{} .mu.Unlock() } func ( *RenderWorkspace) ( int64) { if <= 0 { .rasterizer = nil return } if .rasterizer != nil && .rasterizerPlan.bytes > { .rasterizer = nil } } func ( *RenderWorkspace) ( image.Rectangle) *image.RGBA { := .Dx() * .Dy() * 4 if cap(.canvas.Pix) < { .canvas = *image.NewRGBA() } else { .canvas.Pix = .canvas.Pix[:] .canvas.Stride = .Dx() * 4 .canvas.Rect = } return &.canvas } func ( *RenderWorkspace) ( renderResources) *scanline.Rasterizer { := renderWorkspaceRasterizerPlan{ width: .rasterizerWidth, height: .rasterizerHeight, edges: .rasterizerEdges, bytes: .rasterizerBytes, } if .rasterizerPlan != { .rasterizer = nil } := .rasterizer .rasterizer = nil .rasterizerPlan = return } func ( *RenderWorkspace) ( *scanline.Rasterizer) { .rasterizer = } func render( context.Context, *d2scene.Document, FrameOptions, *RenderSession, *RenderWorkspace) (*image.NRGBA, error) { if == nil { = context.Background() } , := prepareWithSession(, , , ) if != nil { return nil, } if := .Err(); != nil { return nil, } := &rasterScratch{ rasterizerEdges: .resources.rasterizerEdges, rasterizerBounded: true, offscreen: offscreenBudget{limit: .MaxOffscreenBytes}, scanlineWork: scanline.NewWorkBudget(.resources.scanlineWork), scanlineWorkSet: true, } if != nil { .rasterizer = .takeRasterizer(.resources) defer func() { .putRasterizer(.rasterizer) }() } defer .releasePatternTiles() , := .offscreen.reserveBytes(.resources.rasterizerBytes, "scanline rasterizer working storage") if != nil { return nil, } defer func() { if != 0 { .offscreen.release() } }() for , := range .patterns { if := .render(, ); != nil { return nil, } } := image.Rect(0, 0, .width, .height) var *image.RGBA if == nil { = image.NewRGBA() } else { = .canvasFor() } if .background != nil && .background.A == 0xff { fillOpaquePixels(.Pix, *.background) } else if .background != nil && .background.A != 0 { draw.Draw(, .Bounds(), image.NewUniform(*.background), image.Point{}, draw.Src) } else if != nil { clear(.Pix) } if := renderNode(, , .root, ); != nil { return nil, } := + .patternBytes if .offscreen.live != { return nil, fmt.Errorf( "d2raster: internal offscreen reservation leak: %d bytes live, want retained rasterizer and patterns %d", .offscreen.live, , ) } if .offscreen.peak > .resources.peakOffscreenBytes { return nil, fmt.Errorf( "d2raster: internal offscreen peak %d exceeds preflight plan %d", .offscreen.peak, .resources.peakOffscreenBytes, ) } .releasePatternTiles() if == nil { .rasterizer = nil } .offscreen.release() = 0 if := .Err(); != nil { return nil, } // An opaque full-canvas background remains opaque because every supported // final-canvas operation is source-over: As = 1 or Ab = 1 implies // Ao = As + Ab*(1-As) = 1. Clips, masks, and filters operate on temporary // layers before those layers are composited. At alpha 255, premultiplied RGBA // channels are already the straight-alpha NRGBA channels, so avoid even the // in-place conversion scan used for transparent output. := .Bounds() if .background == nil || .background.A == 0 { // A zeroed canvas stays zero outside the conservative prepared root // bounds. Hidden RGB on a fully transparent NRGBA background is also // discarded when draw converts it to premultiplied RGBA, so those pixels // need no straight-alpha conversion either. = image.Rectangle{} if .root != nil { = .root.bounds.Intersect(.Bounds()) } } , := rgbaToNRGBAInPlaceBounds(, , , .background != nil && .background.A == 0xff) if != nil { return nil, } return , nil } func fillOpaquePixels( []byte, color.NRGBA) { if len() < 4 { return } [0], [1], [2], [3] = .R, .G, .B, 0xff for := 4; < len(); { += copy([:], [:]) } } // rgbaToNRGBAInPlaceBounds returns an NRGBA view over source's backing store. // When opaque is false, it first converts the premultiplied channels within // conversionBounds to straight alpha in place. Render needs no second pixel // allocation merely to change color models. Callers may set opaque only when every source // pixel is known to have alpha 255. // // The arithmetic matches image.NRGBA.SetRGBA64 and preserves hidden color // channels on zero-alpha pixels. func rgbaToNRGBAInPlaceBounds( context.Context, *image.RGBA, image.Rectangle, bool) (*image.NRGBA, error) { := &image.NRGBA{Pix: .Pix, Stride: .Stride, Rect: .Rect} if { return , .Err() } := .Intersect(.Bounds()) := .Dx() for := .Min.Y; < .Max.Y; ++ { if (-.Min.Y)&31 == 0 { if := .Err(); != nil { return nil, } } := .PixOffset(.Min.X, ) := .Pix[ : +*4] if >= 64 { := 0 for := range 8 { := [*4+3] if != 0 && != 0xff { ++ } } if >= 7 { // Rows dominated by partial alpha cannot use uniform-block skips. // Keep their inner loop as compact as the scalar conversion. for := 0; < ; ++ { if != 0 && &4095 == 0 { if := .Err(); != nil { return nil, } } := * 4 := uint32([+3]) if != 0 && != 0xff { [] = uint8((uint32([]) * 0xffff / ) >> 8) [+1] = uint8((uint32([+1]) * 0xffff / ) >> 8) [+2] = uint8((uint32([+2]) * 0xffff / ) >> 8) } } continue } } for := 0; < ; += 64 { if != 0 && &4095 == 0 { if := .Err(); != nil { return nil, } } := min(+64, ) := if - == 64 { := [*4+3] if ( == 0 || == 0xff) && [(-1)*4+3] == { const = uint64(0xff000000ff000000) := uint64(0) if == 0xff { = } := 0 for +8 <= 63 { := ( + ) * 4 := (binary.LittleEndian.Uint64([:+8]) ^ ) | (binary.LittleEndian.Uint64([+8:+16]) ^ ) | (binary.LittleEndian.Uint64([+16:+24]) ^ ) | (binary.LittleEndian.Uint64([+24:+32]) ^ ) if & != 0 { break } += 8 } for +2 <= 63 { := ( + ) * 4 if binary.LittleEndian.Uint64([:+8])& != { if [+3] == { ++ } break } += 2 } if < 63 && [(+)*4+3] == { ++ } if == 63 { continue } // The probe proved that the prefix needs no conversion. += } } for ; < ; ++ { := * 4 := uint32([+3]) if != 0 && != 0xff { [] = uint8((uint32([]) * 0xffff / ) >> 8) [+1] = uint8((uint32([+1]) * 0xffff / ) >> 8) [+2] = uint8((uint32([+2]) * 0xffff / ) >> 8) } } } } return , .Err() } // EncodePNG deterministically encodes img as PNG. func ( context.Context, image.Image) ([]byte, error) { if == nil { = context.Background() } if := .Err(); != nil { return nil, } if == nil { return nil, fmt.Errorf("d2raster: cannot encode a nil image") } var bytes.Buffer := png.Encoder{CompressionLevel: png.BestSpeed} if := .Encode(contextWriter{ctx: , output: &}, ); != nil { if := .Err(); != nil { return nil, } return nil, fmt.Errorf("d2raster: encode PNG: %w", ) } if := .Err(); != nil { return nil, } return .Bytes(), nil } type contextWriter struct { ctx context.Context output io.Writer } func ( contextWriter) ( []byte) (int, error) { if := .ctx.Err(); != nil { return 0, } , := .output.Write() if != nil { return , } if := .ctx.Err(); != nil { return , } return , nil } func prepareWithSession( context.Context, *d2scene.Document, FrameOptions, *RenderSession) (*preparedDocument, error) { return prepareWithSessionBands(, , , , 0) } func prepareWithSessionBands( context.Context, *d2scene.Document, FrameOptions, *RenderSession, int) (*preparedDocument, error) { if := .Err(); != nil { return nil, } if == nil { return nil, fmt.Errorf("d2raster: nil document") } if .MaxTextRunesPerRun == 0 { .MaxTextRunesPerRun = min(.MaxPathCommands, defaultMaxTextRunesPerRun) } if .MaxFontFacesPerText == 0 { .MaxFontFacesPerText = min(.MaxAssets, defaultMaxFontFacesPerText) } if .MaxTextCoverageChecks == 0 { .MaxTextCoverageChecks = defaultMaxTextCoverageChecks } if .MaxTextShapingRuns == 0 { .MaxTextShapingRuns = min(.MaxPathCommands, defaultMaxTextShapingRuns) } if .MaxScanlineWork == 0 { .MaxScanlineWork = defaultMaxScanlineWork } if := validateOptions(); != nil { return nil, } if := validateBox(.ViewBox); != nil || .ViewBox.Width == 0 || .ViewBox.Height == 0 { return nil, fmt.Errorf("d2raster: invalid viewbox") } if !finite(.LogicalWidth) || !finite(.LogicalHeight) || .LogicalWidth <= 0 || .LogicalHeight <= 0 { return nil, fmt.Errorf("d2raster: invalid logical dimensions") } if .ViewportFit > d2scene.ViewportMeet { return nil, fmt.Errorf("d2raster: invalid viewport fit %d", .ViewportFit) } if .ViewportAlign > d2scene.ViewportAlignXMidYMid { return nil, fmt.Errorf("d2raster: invalid viewport alignment %d", .ViewportAlign) } := .LogicalWidth * .Scale := .LogicalHeight * .Scale if !finite() || !finite() || <= 0 || <= 0 { return nil, fmt.Errorf("d2raster: invalid scaled dimensions") } := math.Ceil() := math.Ceil() if > float64(.MaxWidth) { return nil, fmt.Errorf("d2raster: frame width %.0f exceeds limit %d", , .MaxWidth) } if > float64(.MaxHeight) { return nil, fmt.Errorf("d2raster: frame height %.0f exceeds limit %d", , .MaxHeight) } := int(^uint(0) >> 1) if >= float64() || >= float64() { return nil, fmt.Errorf("d2raster: scaled dimensions exceed the platform integer domain") } := int() := int() if int64() > .MaxPixels/int64() { return nil, fmt.Errorf("d2raster: frame pixels exceed limit %d", .MaxPixels) } := if > 0 { = min(, ) } if , := finalFrameStorageBytes(, ); != nil { return nil, } if .Root == nil { return nil, fmt.Errorf("d2raster: document has no root node") } := / .ViewBox.Width := / .ViewBox.Height , := 0.0, 0.0 if .ViewportFit == d2scene.ViewportMeet { := math.Min(float64()/.ViewBox.Width, float64()/.ViewBox.Height) , = , if .ViewportAlign == d2scene.ViewportAlignXMidYMid { = (float64() - .ViewBox.Width*) / 2 = (float64() - .ViewBox.Height*) / 2 } } := d2scene.Matrix{ A: , D: , E: - .ViewBox.X*, F: - .ViewBox.Y*, } if !.IsFinite() { return nil, fmt.Errorf("d2raster: viewbox mapping is non-finite") } := &preflight{ ctx: , document: , options: , frameBounds: image.Rect(0, 0, , ), active: make(map[*d2scene.Node]bool), session: , } if := .assets(); != nil { return nil, } , := .node(.Root, , 1, 0) if != nil { return nil, } if := .Err(); != nil { return nil, } , := collectPreparedPatterns(, , .frameBounds) if != nil { return nil, } var renderResources if > 0 { if := setBandReferenceBounds(, , .frameBounds); != nil { return nil, } for , := range { if := setBandReferenceBounds(, .root, .bounds); != nil { return nil, } } , = planBandResources(, , .frameBounds, , , ) } else { , = planRenderResources(, , .frameBounds, , .MaxOffscreenBytes) } if != nil { return nil, } if .peakOffscreenBytes > .MaxOffscreenBytes { return nil, fmt.Errorf( "d2raster: peak offscreen pixel storage %d bytes exceeds limit %d", .peakOffscreenBytes, .MaxOffscreenBytes, ) } if .evenOddClipWork > .MaxEvenOddClipWork { return nil, fmt.Errorf( "d2raster: even-odd clip work %d exceeds limit %d", .evenOddClipWork, .MaxEvenOddClipWork, ) } if .scanlineWork > .MaxScanlineWork { return nil, fmt.Errorf( "d2raster: scanline work %d exceeds limit %d", .scanlineWork, .MaxScanlineWork, ) } := &preparedDocument{width: , height: , root: , patterns: , resources: } if .Background != nil { := color.NRGBAModel.Convert(.Background).(color.NRGBA) .background = & } return , nil } const ( defaultMaxTextRunesPerRun = 100_000 defaultMaxFontFacesPerText = 64 defaultMaxTextCoverageChecks = int64(10_000_000) defaultMaxTextShapingRuns = 100_000 defaultMaxScanlineWork int64 = 4_000_000_000 ) func finalFrameStorageBytes(, int) (int64, error) { , := checkedMultiply(int64(), int64()) if ! { return 0, fmt.Errorf("d2raster: final frame storage exceeds the int64 domain") } // Render converts its premultiplied RGBA canvas to NRGBA in place, so the // returned image retains the same four-byte-per-pixel backing store. , := checkedMultiply(, 4) if ! || > platformMaxInt() { return 0, fmt.Errorf("d2raster: final frame storage exceeds the platform integer domain") } return , nil } // preparationBounds keeps off-viewport source pixels available to bounded // filters that can move them back into the destination. It remains a strict // platform-sized domain; actual allocation is still charged from node-local // ink/filter bounds before rendering. func ( *preflight) () image.Rectangle { := platformMaxInt() := - - 1 := / 4 , := , if int64(.frameBounds.Min.X) >= + { = int64(.frameBounds.Min.X) - } if int64(.frameBounds.Min.Y) >= + { = int64(.frameBounds.Min.Y) - } , := , if int64(.frameBounds.Max.X) <= - { = int64(.frameBounds.Max.X) + } if int64(.frameBounds.Max.Y) <= - { = int64(.frameBounds.Max.Y) + } return image.Rect(int(), int(), int(), int()) } func validateOptions( FrameOptions) error { if !finite(.Scale) || .Scale <= 0 { return fmt.Errorf("d2raster: scale must be finite and positive") } if .Time < 0 { return fmt.Errorf("d2raster: frame time must not be negative") } if .MaxWidth <= 0 || .MaxHeight <= 0 || .MaxPixels <= 0 || .MaxNodes <= 0 || .MaxDepth <= 0 || .MaxPathCommands <= 0 || .MaxTextRunesPerRun <= 0 || .MaxFontFacesPerText <= 0 || .MaxTextCoverageChecks <= 0 || .MaxTextShapingRuns <= 0 || .MaxAnimationTracks <= 0 || .MaxAnimationKeyframes <= 0 || .MaxAssets <= 0 || .MaxAssetBytes <= 0 || .MaxDecodedAssetBytes <= 0 || .MaxImportDepth <= 0 || .MaxOffscreenBytes <= 0 || .MaxEvenOddClipWork <= 0 || .MaxScanlineWork <= 0 { return fmt.Errorf("d2raster: every frame resource limit must be positive") } return nil } func ( *preflight) () error { if len(.document.Assets) > .options.MaxAssets { return fmt.Errorf("d2raster: asset count %d exceeds limit %d", len(.document.Assets), .options.MaxAssets) } if len(.document.Assets) == 0 { return nil } := make([]string, 0, len(.document.Assets)) for := range .document.Assets { = append(, string()) } sort.Strings() for , := range { if := .ctx.Err(); != nil { return } := d2scene.AssetID() if == "" { return fmt.Errorf("d2raster: empty asset ID") } := .document.Assets[] switch asset := .(type) { case d2scene.FontAsset: if := .addFontAsset(, ); != nil { return } case *d2scene.FontAsset: if == nil { return fmt.Errorf("d2raster: font asset %q is nil", ) } if := .addFontAsset(, *); != nil { return } case d2scene.RasterAsset: if := .addRasterAsset(, ); != nil { return } case *d2scene.RasterAsset: if == nil { return fmt.Errorf("d2raster: raster asset %q is nil", ) } if := .addRasterAsset(, *); != nil { return } case d2scene.VectorAsset: if := .addVectorAsset(, ); != nil { return } case *d2scene.VectorAsset: if == nil { return fmt.Errorf("d2raster: vector asset %q is nil", ) } if := .addVectorAsset(, *); != nil { return } case nil: return fmt.Errorf("d2raster: asset %q is nil", ) default: return fmt.Errorf("d2raster: asset %q has unsupported type %T", , ) } } return .validateRetainedVectorAssets() } func ( *preflight) ( d2scene.AssetID, d2scene.FontAsset) error { if := .addAssetBytes(, len(.Data)); != nil { return } var ( *preparedFont error ) if .session == nil { , = parsePreparedFont(.Data, .FaceIndex) } else { , = .session.font(.ctx, .document, , ) } if != nil { return fmt.Errorf("d2raster: font asset %q: %w", , ) } if .fonts == nil { .fonts = make(map[d2scene.AssetID]*preparedFont) } .fonts[] = return nil } func ( *preflight) ( d2scene.AssetID, d2scene.RasterAsset) error { if := .addAssetBytes(, len(.Data)); != nil { return } := .options.MaxDecodedAssetBytes - .decodedBytes var ( *preparedRasterAsset int64 error ) if .session == nil { , , = prepareRasterAsset(.ctx, , , ) } else { , , = .session.raster(.ctx, .document, , , ) } if != nil { return } .decodedBytes += if .rasters == nil { .rasters = make(map[d2scene.AssetID]*preparedRasterAsset) } .rasters[] = return nil } func ( *preflight) ( d2scene.AssetID, d2scene.VectorAsset) error { if := validateBox(.ViewBox); != nil || .ViewBox.Width == 0 || .ViewBox.Height == 0 { return fmt.Errorf("d2raster: vector asset %q has invalid viewbox", ) } if .Root == nil { return fmt.Errorf("d2raster: vector asset %q has no root node", ) } // Retain the immutable scene handle during the collection pass. A second, // placement-independent pass validates each definition exactly once after // forward references are available. Visible Image occurrences are still // instantiated independently. if .vectors == nil { .vectors = make(map[d2scene.AssetID]d2scene.VectorAsset) } .vectors[] = return nil } func ( *preflight) ( d2scene.AssetID, int) error { := int64() if > .options.MaxAssetBytes-.assetBytes { return fmt.Errorf("d2raster: asset %q causes retained asset bytes to exceed limit %d", , .options.MaxAssetBytes) } .assetBytes += return nil } func ( *preflight) ( *d2scene.Node, d2scene.Matrix, , int) (*preparedNode, error) { if := .ctx.Err(); != nil { return nil, } if == nil { return nil, nil } if > .options.MaxDepth { return nil, fmt.Errorf("d2raster: node depth %d exceeds limit %d", , .options.MaxDepth) } if .active[] { return nil, fmt.Errorf("d2raster: node cycle at %q", .ID) } if := .addPreparedNodes(1); != nil { return nil, } if !.Transform.IsFinite() || !finite(.Opacity) || .Opacity < 0 || .Opacity > 1 { return nil, fmt.Errorf("d2raster: node %q has invalid transform or opacity", .ID) } if !supportedBlendMode(.Blend) { return nil, fmt.Errorf("d2raster: node %q uses invalid or unsupported blend mode %d", .ID, .Blend) } if := .chargeFilterWork(.ID, .Filters); != nil { return nil, } if := .chargeAnimationWork(.ID, .Animations); != nil { return nil, } .active[] = true defer delete(.active, ) := .Transform := .Opacity var animationOverrides := make(map[[2]int]bool, len(.Animations)) for , := range .Animations { if := .ctx.Err(); != nil { return nil, } := [2]int{int(.Property), .TargetIndex} if [] { return nil, fmt.Errorf("d2raster: node %q has duplicate animation target property %d index %d", .ID, .Property, .TargetIndex) } [] = true , := .animationValueAt() if != nil { return nil, fmt.Errorf("d2raster: node %q animation %d: %w", .ID, , ) } if .Property != d2scene.AnimateDropShadow && .TargetIndex != 0 { return nil, fmt.Errorf("d2raster: node %q animation %d uses non-zero target index for scalar property %d", .ID, , .Property) } switch .Property { case d2scene.AnimateOpacity: if .Number < 0 || .Number > 1 { return nil, fmt.Errorf("d2raster: node %q animation %d resolves opacity outside [0,1]", .ID, ) } = .Number case d2scene.AnimateTransform: = .Transform case d2scene.AnimateStrokeDashOffset: := .Number .dashOffset = & case d2scene.AnimateFillColor: := .Color .fillColor = & case d2scene.AnimateStrokeColor: := .Color .strokeColor = & case d2scene.AnimateDropShadow: if .dropShadows == nil { .dropShadows = make(map[int]d2scene.DropShadow) } .dropShadows[.TargetIndex] = .Shadow default: return nil, fmt.Errorf("d2raster: node %q animation %d uses unknown property %d", .ID, , .Property) } } := .Mul() if !.IsFinite() { return nil, fmt.Errorf("d2raster: node %q has a non-finite composed transform", .ID) } , := normalizeNodeFilters(.ctx, .ID, .Filters, .dropShadows) if != nil { return nil, } , := .primitive(.ID, .Primitive, , , , ) if != nil { return nil, } if == nil && (.fillColor != nil || .strokeColor != nil || .dashOffset != nil) { return nil, fmt.Errorf("d2raster: node %q has a paint or stroke animation but no primitive", .ID) } := &preparedNode{opacity: , blend: .Blend, primitive: } if != nil { .bounds = .bounds } if .Clip != nil { , := .clip(.ID, .Clip, ) if != nil { return nil, } .clip = } if .Mask != nil { if !.Mask.Transform.IsFinite() || .Mask.Type > d2scene.MaskLuminance || .Mask.Root == nil { return nil, fmt.Errorf("d2raster: node %q has invalid mask", .ID) } // Scene masks use node-local user space: the node's complete composed // transform is applied before the mask's own transform. := .Mul(.Mask.Transform) if !.IsFinite() { return nil, fmt.Errorf("d2raster: node %q has a non-finite composed mask transform", .ID) } , := .(.Mask.Root, , +1, ) if != nil { return nil, fmt.Errorf("d2raster: node %q mask: %w", .ID, ) } .mask = &preparedMask{kind: .Mask.Type, root: } } for , := range .Children { if := .ctx.Err(); != nil { return nil, } // Document, vector-asset, and mask roots are checked explicitly by // their owners. Inside a node, every child slot must identify an // actual node; otherwise arbitrarily large nil slices could evade both // work accounting and useful malformed-scene errors. if == nil { return nil, fmt.Errorf("d2raster: node %q child %d is nil", .ID, ) } , := .(, , +1, ) if != nil { return nil, } if != nil { .children = append(.children, ) .bounds = unionRect(.bounds, .bounds) } } .contentBounds = .bounds .filters, .bounds, = .prepareFilters(.ID, , , .bounds) if != nil { return nil, } if .clip != nil { .bounds = .bounds.Intersect(.clip.bounds) } if .mask != nil && .mask.root != nil { .bounds = .bounds.Intersect(.mask.root.bounds) } return , nil } func ( *preflight) ( int) error { if <= 0 { return fmt.Errorf("d2raster: internal prepared node charge must be positive") } if > .options.MaxNodes-.nodes { return fmt.Errorf("d2raster: node count exceeds limit %d", .options.MaxNodes) } .nodes += return nil } func ( *preflight) ( string, *d2scene.Clip, d2scene.Matrix) (*preparedClip, error) { if == nil { return nil, nil } if !.Transform.IsFinite() { return nil, fmt.Errorf("d2raster: node %q has invalid clip transform", ) } if .Path.FillRule > d2scene.EvenOdd { return nil, fmt.Errorf("d2raster: node %q clip has invalid fill rule %d", , .Path.FillRule) } // Clip.Path is geometry-only. Its Fill and Stroke paints do not contribute // pixels or coverage, consistent with SVG clipping-path geometry. := .Mul(.Transform) if !.IsFinite() { return nil, fmt.Errorf("d2raster: node %q has a non-finite composed clip transform", ) } , := flattenScenePath(.ctx, .Path, , .addPathSegment) if != nil { return nil, fmt.Errorf("d2raster: node %q clip: %w", , ) } := make([]subpath, len()) var int64 for , := range { [].closed = .closed [].points = make([]d2scene.Point, len(.points)) for , := range .points { := .Point() if := validateRasterPoint(); != nil { return nil, fmt.Errorf("d2raster: node %q clip geometry: %w", , ) } [].points[] = } if len(.points) >= 2 { += int64(len(.points)) } } return &preparedClip{ subpaths: , fillRule: .Path.FillRule, bounds: subpathPixelBounds(, d2scene.Identity(), 0, .preparationBounds()), edges: , }, nil } func ( *preflight) ( string, d2scene.Primitive, d2scene.Matrix, animationOverrides, , int) (*preparedPrimitive, error) { if == nil { return nil, nil } switch primitive := .(type) { case d2scene.Rect: return .rect(, , , , ) case *d2scene.Rect: if == nil { return nil, nil } return .rect(, *, , , ) case d2scene.Ellipse: return .ellipse(, , , , ) case *d2scene.Ellipse: if == nil { return nil, nil } return .ellipse(, *, , , ) case d2scene.Path: return .path(, , , , ) case *d2scene.Path: if == nil { return nil, nil } return .path(, *, , , ) case d2scene.TextRun: return .text(, , , , ) case *d2scene.TextRun: if == nil { return nil, nil } return .text(, *, , , ) case d2scene.Image: return .image(, , , , , ) case *d2scene.Image: if == nil { return nil, nil } return .image(, *, , , , ) default: return nil, fmt.Errorf("d2raster: node %q uses unsupported primitive %T", , ) } } func ( *preflight) ( string, d2scene.Rect, d2scene.Matrix, animationOverrides, int) (*preparedPrimitive, error) { if := validateBox(.Box); != nil { return nil, fmt.Errorf("d2raster: node %q rectangle: %w", , ) } if !finite(.RadiusX) || !finite(.RadiusY) || .RadiusX < 0 || .RadiusY < 0 { return nil, fmt.Errorf("d2raster: node %q rectangle has invalid corner radius", ) } , := roundedRectSubpaths(.ctx, , flattenTolerance(), .addPathSegment) if != nil { return nil, fmt.Errorf("d2raster: node %q rectangle: %w", , ) } := .Box , := .prepareAnimatedPaint(.Fill, .fillColor, , , ) if != nil { return nil, fmt.Errorf("d2raster: node %q rectangle fill: %w", , ) } , := .prepareAnimatedStroke(.Stroke, .strokeColor, .dashOffset, , , ) if != nil { return nil, fmt.Errorf("d2raster: node %q rectangle stroke: %w", , ) } return .finishPrimitive(, , , , ) } func ( *preflight) ( string, d2scene.Ellipse, d2scene.Matrix, animationOverrides, int) (*preparedPrimitive, error) { if !finitePoint(.Center) || !finite(.RadiusX) || !finite(.RadiusY) || .RadiusX < 0 || .RadiusY < 0 { return nil, fmt.Errorf("d2raster: node %q ellipse has invalid geometry", ) } , := ellipseSubpaths(.ctx, .Center, .RadiusX, .RadiusY, flattenTolerance(), .addPathSegment) if != nil { return nil, fmt.Errorf("d2raster: node %q ellipse: %w", , ) } := d2scene.Box{ X: .Center.X - .RadiusX, Y: .Center.Y - .RadiusY, Width: 2 * .RadiusX, Height: 2 * .RadiusY, } , := .prepareAnimatedPaint(.Fill, .fillColor, , , ) if != nil { return nil, fmt.Errorf("d2raster: node %q ellipse fill: %w", , ) } , := .prepareAnimatedStroke(.Stroke, .strokeColor, .dashOffset, , , ) if != nil { return nil, fmt.Errorf("d2raster: node %q ellipse stroke: %w", , ) } return .finishPrimitive(, , , , ) } func ( *preflight) ( string, d2scene.Path, d2scene.Matrix, animationOverrides, int) (*preparedPrimitive, error) { if .FillRule > d2scene.EvenOdd { return nil, fmt.Errorf("d2raster: node %q path has invalid fill rule %d", , .FillRule) } , := flattenScenePath(.ctx, , , .addPathSegment) if != nil { return nil, fmt.Errorf("d2raster: node %q path: %w", , ) } , := .GeometryBounds() if != nil { return nil, fmt.Errorf("d2raster: node %q path bounds: %w", , ) } := .Box() , := .prepareAnimatedPaint(.Fill, .fillColor, , , ) if != nil { return nil, fmt.Errorf("d2raster: node %q path fill: %w", , ) } , := .prepareAnimatedStroke(.Stroke, .strokeColor, .dashOffset, , , ) if != nil { return nil, fmt.Errorf("d2raster: node %q path stroke: %w", , ) } , := .finishPrimitive(, , , , ) if != nil { return nil, } .fillRule = .FillRule if .FillRule == d2scene.EvenOdd && != nil { .evenOddSubpaths = make([]subpath, len()) for , := range { if := .ctx.Err(); != nil { return nil, } .evenOddSubpaths[].closed = .closed .evenOddSubpaths[].points = make([]d2scene.Point, len(.points)) for , := range .points { if &255 == 0 { if := .ctx.Err(); != nil { return nil, } } .evenOddSubpaths[].points[] = .Point() } if len(.points) >= 2 { .evenOddEdges += int64(len(.points)) } } } return , nil } func ( *preflight) ( string, []subpath, d2scene.Matrix, *preparedPaint, *preparedStroke) (*preparedPrimitive, error) { for , := range { for , := range .points { if := .ctx.Err(); != nil { return nil, } if := validateRasterPoint(.Point()); != nil { return nil, fmt.Errorf("d2raster: node %q geometry: %w", , ) } } } := &preparedPrimitive{subpaths: , transform: , fill: , stroke: } if != nil { .bounds = subpathPixelBounds(, , 0, .preparationBounds()) } if == nil { return , nil } if !finite(.width * .MaxScale()) { return nil, fmt.Errorf("d2raster: node %q stroke has non-finite transformed width", ) } for , := range { if := validateStrokeCenterline(); != nil { return nil, fmt.Errorf("d2raster: node %q stroke geometry: %w", , ) } , := makeStrokeRuns(.ctx, , .dashes, .dashOffset, .addPathSegment) if != nil { return nil, fmt.Errorf("d2raster: node %q stroke: %w", , ) } .strokeRuns = append(.strokeRuns, ...) } := .width / 2 for , := range .strokeRuns { for , := range .points { for , := range [...]d2scene.Point{ {X: -, Y: -}, {X: , Y: -}, {X: , Y: }, {X: -, Y: }, } { if := validateRasterPoint(.Point(d2scene.Point{X: .X + .X, Y: .Y + .Y})); != nil { return nil, fmt.Errorf("d2raster: node %q stroke geometry: %w", , ) } } } if .join != d2scene.JoinRound { , := 1, len(.points)-1 if .closed { , = 0, len(.points) } := - 1 if < 0 { = len(.points) - 1 } , := unitVector(.points[], .points[]) for := ; < ; ++ { if := .ctx.Err(); != nil { return nil, fmt.Errorf("d2raster: node %q stroke join geometry: %w", , ) } := + 1 if == len(.points) { = 0 } := .points[] , := unitVector(, .points[]) var [4]d2scene.Point := strokeJoinPolygonFromUnits(&, , , , && , , .join, .miterLimit) if == 0 { , = , continue } for , := range [:] { if := validateRasterPoint(.Point()); != nil { return nil, fmt.Errorf("d2raster: node %q stroke join geometry: %w", , ) } } , = , } } } := .width * .MaxScale() / 2 := 1.0 if .cap == d2scene.CapSquare { = math.Sqrt2 } if .join == d2scene.JoinMiter && .miterLimit > { = .miterLimit } .bounds = unionRect(.bounds, strokeRunPixelBounds(.strokeRuns, , *, .preparationBounds())) return , nil } func ( *preflight) () error { .pathSegments++ if .pathSegments > .options.MaxPathCommands { return fmt.Errorf("path command count exceeds limit %d", .options.MaxPathCommands) } return nil } func ( *preflight) ( string, []d2scene.Track) error { if := .ctx.Err(); != nil { return } if len() > .options.MaxAnimationTracks-.animationTracks { return fmt.Errorf( "d2raster: node %q causes animation track count to exceed limit %d", , .options.MaxAnimationTracks, ) } .animationTracks += len() for , := range { if := .ctx.Err(); != nil { return } if len(.Keyframes) > .options.MaxAnimationKeyframes-.animationKeyframes { return fmt.Errorf( "d2raster: node %q animation %d causes keyframe count to exceed limit %d", , , .options.MaxAnimationKeyframes, ) } .animationKeyframes += len(.Keyframes) } return nil } // MaxNodes also caps filter entries as a separate structural-work dimension. // This keeps the public options stable while preventing one node from carrying // an unbounded identity-filter slice that evades node and path accounting. func ( *preflight) ( string, []d2scene.Filter) error { if := .ctx.Err(); != nil { return } if len() > .options.MaxNodes-.filters { return fmt.Errorf("d2raster: node %q causes filter count to exceed structural limit %d", , .options.MaxNodes) } .filters += len() return nil } func ( *preflight) ( *d2scene.Stroke, *color.NRGBA, *float64, d2scene.Box, d2scene.Matrix, int) (*preparedStroke, error) { return prepareAnimatedStrokeWithPaint(, , , , , func( d2scene.Paint, *color.NRGBA, d2scene.Box, d2scene.Matrix) (*preparedPaint, error) { return .prepareAnimatedPaint(, , , , ) }) } type animatedPaintPreparer func(d2scene.Paint, *color.NRGBA, d2scene.Box, d2scene.Matrix) (*preparedPaint, error) func prepareAnimatedStrokeWithPaint( *d2scene.Stroke, *color.NRGBA, *float64, d2scene.Box, d2scene.Matrix, animatedPaintPreparer) (*preparedStroke, error) { if == nil { if != nil || != nil { return nil, fmt.Errorf("stroke animation targets missing stroke") } return nil, nil } if !finite(.Width) || .Width < 0 || !finite(.MiterLimit) || !finite(.DashOffset) { return nil, fmt.Errorf("invalid stroke") } , := (.Paint, , , ) if != nil { return nil, } := 0.0 for , := range .Dashes { if !finite() || <= 0 { return nil, fmt.Errorf("invalid stroke dash") } += if !finite() { return nil, fmt.Errorf("invalid stroke dash total") } } if .Width == 0 || == nil { return nil, nil } switch .Cap { case d2scene.CapButt, d2scene.CapRound, d2scene.CapSquare: default: return nil, fmt.Errorf("unsupported line cap %d", .Cap) } switch .Join { case d2scene.JoinMiter, d2scene.JoinRound, d2scene.JoinBevel: default: return nil, fmt.Errorf("unsupported line join %d", .Join) } := .MiterLimit if .Join == d2scene.JoinMiter { if == 0 { = 4 } if < 1 { return nil, fmt.Errorf("invalid miter limit %g", .MiterLimit) } } := .DashOffset if != nil { = * } return &preparedStroke{ paint: , width: .Width, cap: .Cap, join: .Join, miterLimit: , dashes: .Dashes, dashOffset: , }, nil } func renderNode( context.Context, *image.RGBA, *preparedNode, *rasterScratch) error { if == nil || .opacity == 0 || .bounds.Intersect(.Bounds()).Empty() { return nil } if := .Err(); != nil { return } if .isolated || .opacity < 1 || .blend != d2scene.BlendNormal || len(.filters) != 0 || .clip != nil || .mask != nil { return renderEffectNode(, , , ) } if .primitive != nil && !.primitive.bounds.Intersect(.Bounds()).Empty() { if := drawPrimitive(, , .primitive, ); != nil { return } } for , := range .children { if := (, , , ); != nil { return } } return .Err() } func drawPrimitive( context.Context, *image.RGBA, *preparedPrimitive, *rasterScratch) error { if := .Err(); != nil { return } if .image != nil { return drawPreparedImage(, , .image) } if .vector != nil { return renderNode(, , .vector, ) } if .fill != nil { if .fillRule == d2scene.EvenOdd { := subpathPixelBounds(.subpaths, .transform, 0, .Bounds()) := drawPaintMask(, , , .fill, , "even-odd fill Alpha mask", func( *image.Alpha) error { return rasterizeEvenOddMask(, , .Min, .evenOddSubpaths) }) if != nil { return } } else if .fill.kind == preparedSolidPaint { := .reset(.Bounds()) := setRasterizerReference(, .Bounds().Min, .referenceBounds.Min, .regionFilters) := d2scene.Translate(-float64(.X), -float64(.Y)).Mul(.transform) for , := range .subpaths { addFillSubpath(, , ) } if := .DrawRGBA(, .workBudget(), , .fill.solid); != nil { return fmt.Errorf("d2raster: solid fill: %w", ) } } else { := subpathPixelBounds(.subpaths, .transform, 0, .Bounds()) := drawRasterizedPaint(, , , .fill, , "gradient fill", func( *scanline.Rasterizer) error { := .Min if .regionFilters { = subpathPixelBounds(.subpaths, .transform, 0, .referenceBounds).Min } := setRasterizerReference(, .Min, , .regionFilters) := d2scene.Translate(-float64(.X), -float64(.Y)).Mul(.transform) for , := range .subpaths { addFillSubpath(, , ) } return .Err() }) if != nil { return } } if := .Err(); != nil { return } } if .stroke != nil { if := drawStroke(, , .strokeRuns, .transform, .stroke, ); != nil { return } } return nil } func addFillSubpath( *scanline.Rasterizer, subpath, d2scene.Matrix) { if len(.points) < 2 { return } := .Point(.points[0]) .MoveTo64(.X, .Y) for , := range .points[1:] { = .Point() .LineTo64(.X, .Y) } // SVG fill closes open subpaths implicitly. .ClosePath() } func validateBox( d2scene.Box) error { if !finite(.X) || !finite(.Y) || !finite(.Width) || !finite(.Height) { return fmt.Errorf("non-finite box") } if .Width < 0 || .Height < 0 { return fmt.Errorf("negative box size") } return nil } func finite( float64) bool { return !math.IsNaN() && !math.IsInf(, 0) } func finitePoint( d2scene.Point) bool { return finite(.X) && finite(.Y) } func validateRasterPoint( d2scene.Point) error { if !finitePoint() || math.Abs(.X) > math.MaxFloat32 || math.Abs(.Y) > math.MaxFloat32 { return fmt.Errorf("point is outside the finite float32 raster domain") } return nil } func validateStrokeCenterline( subpath) error { := cleanPoints(.points, .closed) := len() - 1 if .closed && len() > 1 { = len() } for := 0; < ; ++ { := [] := [(+1)%len()] , := .X-.X, .Y-.Y if !finite() || !finite() || !finite(math.Hypot(, )) { return fmt.Errorf("centerline delta is non-finite") } } return nil }