package d2raster
import (
"context"
"errors"
"fmt"
"image"
"math"
"math/bits"
"github.com/pancsta/d2/d2renderers/d2raster/internal/scanline"
"github.com/pancsta/d2/d2renderers/d2scene"
)
const evenOddSamplesPerAxis = 4
type renderResources struct {
peakOffscreenBytes int64
evenOddClipWork int64
scanlineWork int64
rasterizerBytes int64
rasterizerWidth int
rasterizerHeight int
rasterizerEdges int
}
type rasterizerResourcePlanner struct {
counter *scanline .Rasterizer
regionFilters bool
}
type offscreenPixelKind uint8
const (
offscreenRGBA offscreenPixelKind = iota
offscreenAlpha
)
type cachedOffscreenImage struct {
rgba *image .RGBA
alpha *image .Alpha
}
func (entry cachedOffscreenImage ) byteLen () int {
if entry .rgba != nil {
return len (entry .rgba .Pix )
}
if entry .alpha != nil {
return len (entry .alpha .Pix )
}
return 0
}
func (entry cachedOffscreenImage ) isKind (kind offscreenPixelKind ) bool {
if kind == offscreenRGBA {
return entry .rgba != nil
}
return entry .alpha != nil
}
type offscreenBudget struct {
limit int64
live int64
peak int64
cachedBytes int64
cache cachedOffscreenImage
}
func (b *offscreenBudget ) reserveBytes (bytes int64 , purpose string ) (int64 , error ) {
if bytes > b .limit -b .live {
return 0 , fmt .Errorf (
"d2raster: offscreen %s requires %d bytes with %d bytes already live, exceeding limit %d" ,
purpose , bytes , b .live , b .limit ,
)
}
newLive := b .live + bytes
retainedLimit := maxInt64 (b .peak , newLive )
b .trimCache (retainedLimit - newLive )
b .addLive (bytes )
return bytes , nil
}
func (b *offscreenBudget ) addLive (bytes int64 ) {
b .live += bytes
if b .live > b .peak {
b .peak = b .live
}
}
func (b *offscreenBudget ) release (bytes int64 ) {
b .live -= bytes
}
func (b *offscreenBudget ) newRGBA (bounds image .Rectangle , purpose string ) (*image .RGBA , int64 , error ) {
bytes , err := pixelStorageBytes (bounds , 4 )
if err != nil {
return nil , 0 , fmt .Errorf ("d2raster: %s: %w" , purpose , err )
}
entry , reservation , err := b .reserveImage (bytes , offscreenRGBA , purpose )
if err != nil {
return nil , 0 , err
}
if entry .rgba == nil {
return image .NewRGBA (bounds ), reservation , nil
}
clear (entry .rgba .Pix )
entry .rgba .Stride = 4 * bounds .Dx ()
entry .rgba .Rect = bounds
return entry .rgba , reservation , nil
}
func (b *offscreenBudget ) newAlpha (bounds image .Rectangle , purpose string ) (*image .Alpha , int64 , error ) {
bytes , err := pixelStorageBytes (bounds , 1 )
if err != nil {
return nil , 0 , fmt .Errorf ("d2raster: %s: %w" , purpose , err )
}
entry , reservation , err := b .reserveImage (bytes , offscreenAlpha , purpose )
if err != nil {
return nil , 0 , err
}
if entry .alpha == nil {
return image .NewAlpha (bounds ), reservation , nil
}
clear (entry .alpha .Pix )
entry .alpha .Stride = bounds .Dx ()
entry .alpha .Rect = bounds
return entry .alpha , reservation , nil
}
func (b *offscreenBudget ) reserveImage (bytes int64 , kind offscreenPixelKind , purpose string ) (cachedOffscreenImage , int64 , error ) {
if bytes > b .limit -b .live {
return cachedOffscreenImage {}, 0 , fmt .Errorf (
"d2raster: offscreen %s requires %d bytes with %d bytes already live, exceeding limit %d" ,
purpose , bytes , b .live , b .limit ,
)
}
if bytes != 0 && b .cache .isKind (kind ) && int64 (b .cache .byteLen ()) == bytes {
entry := b .cache
b .dropCached ()
b .addLive (bytes )
return entry , bytes , nil
}
if b .cachedBytes != 0 {
b .dropCached ()
}
reservation , err := b .reserveBytes (bytes , purpose )
return cachedOffscreenImage {}, reservation , err
}
func (b *offscreenBudget ) recycleRGBA (buffer *image .RGBA , reservation int64 ) {
if buffer == nil {
b .release (reservation )
return
}
b .recycleImage (cachedOffscreenImage {rgba : buffer }, reservation )
}
func (b *offscreenBudget ) recycleAlpha (buffer *image .Alpha , reservation int64 ) {
if buffer == nil {
b .release (reservation )
return
}
b .recycleImage (cachedOffscreenImage {alpha : buffer }, reservation )
}
func (b *offscreenBudget ) recycleImage (entry cachedOffscreenImage , reservation int64 ) {
b .release (reservation )
if reservation == 0 || int64 (entry .byteLen ()) != reservation {
return
}
if b .cachedBytes != 0 {
b .dropCached ()
}
b .cache = entry
b .cachedBytes += reservation
}
func (b *offscreenBudget ) trimCache (maxBytes int64 ) {
if b .cachedBytes > maxBytes {
b .dropCached ()
}
}
func (b *offscreenBudget ) dropCached () {
b .cache = cachedOffscreenImage {}
b .cachedBytes = 0
}
func pixelStorageBytes(bounds image .Rectangle , bytesPerPixel int64 ) (int64 , error ) {
if bounds .Empty () {
return 0 , nil
}
pixels , ok := checkedMultiply (int64 (bounds .Dx ()), int64 (bounds .Dy ()))
if !ok {
return 0 , fmt .Errorf ("offscreen pixel storage exceeds the int64 domain" )
}
bytes , ok := checkedMultiply (pixels , bytesPerPixel )
if !ok || bytes > platformMaxInt () {
return 0 , fmt .Errorf ("offscreen pixel storage exceeds the platform integer domain" )
}
return bytes , nil
}
func platformMaxInt() int64 {
return int64 (int (^uint (0 ) >> 1 ))
}
func checkedMultiply(left , right int64 ) (int64 , bool ) {
if left < 0 || right < 0 || left != 0 && right > math .MaxInt64 /left {
return 0 , false
}
return left * right , true
}
func checkedAdd(left , right int64 ) (int64 , bool ) {
if left < 0 || right < 0 || right > math .MaxInt64 -left {
return 0 , false
}
return left + right , true
}
func maxInt64(left , right int64 ) int64 {
if left > right {
return left
}
return right
}
func addResourceWork(resources *renderResources , work int64 ) error {
total , ok := checkedAdd (resources .evenOddClipWork , work )
if !ok {
return fmt .Errorf ("d2raster: even-odd clip work exceeds the int64 domain" )
}
resources .evenOddClipWork = total
return nil
}
func addScanlineWork(resources *renderResources , work int64 ) error {
total , ok := checkedAdd (resources .scanlineWork , work )
if !ok {
return fmt .Errorf ("d2raster: scanline work exceeds the int64 domain" )
}
resources .scanlineWork = total
return nil
}
func planRenderResources(ctx context .Context , node *preparedNode , dst image .Rectangle , patterns []*preparedPatternTile , maxOffscreenBytes int64 ) (renderResources , error ) {
return planRenderResourcesRegion (ctx , node , dst , patterns , maxOffscreenBytes , false )
}
func planRenderResourcesRegion(ctx context .Context , node *preparedNode , dst image .Rectangle , patterns []*preparedPatternTile , maxOffscreenBytes int64 , regionFilters bool ) (renderResources , error ) {
planner := &rasterizerResourcePlanner {
counter : scanline .NewCounter (0 , 0 , scanline .MaxEdgesForBytes (maxOffscreenBytes )),
regionFilters : regionFilters ,
}
resources , err := planNodeResources (ctx , node , dst , planner )
if err != nil {
return renderResources {}, err
}
scenePeak := resources .peakOffscreenBytes
livePatternBytes := int64 (0 )
patternPhasePeak := int64 (0 )
for _ , pattern := range patterns {
if err := ctx .Err (); err != nil {
return renderResources {}, err
}
if pattern == nil {
return renderResources {}, fmt .Errorf ("d2raster: internal nil prepared pattern" )
}
patternResources , err := planNodeResources (ctx , pattern .root , pattern .bounds , planner )
if err != nil {
return renderResources {}, err
}
duringTile , ok := checkedAdd (livePatternBytes , pattern .tileBytes )
if !ok {
return renderResources {}, fmt .Errorf ("d2raster: pattern tile storage exceeds the int64 domain" )
}
duringTile , ok = checkedAdd (duringTile , patternResources .peakOffscreenBytes )
if !ok {
return renderResources {}, fmt .Errorf ("d2raster: peak offscreen pixel storage exceeds the int64 domain" )
}
patternPhasePeak = maxInt64 (patternPhasePeak , duringTile )
mergeRasterizerRequirements (&resources , patternResources )
if err := addScanlineWork (&resources , patternResources .scanlineWork ); err != nil {
return renderResources {}, err
}
if err := addResourceWork (&resources , patternResources .evenOddClipWork ); err != nil {
return renderResources {}, err
}
livePatternBytes , ok = checkedAdd (livePatternBytes , pattern .tileBytes )
if !ok {
return renderResources {}, fmt .Errorf ("d2raster: pattern tile storage exceeds the int64 domain" )
}
}
scenePeak , ok := checkedAdd (scenePeak , livePatternBytes )
if !ok {
return renderResources {}, fmt .Errorf ("d2raster: peak offscreen pixel storage exceeds the int64 domain" )
}
resources .peakOffscreenBytes = maxInt64 (patternPhasePeak , scenePeak )
resources .peakOffscreenBytes , ok = checkedAdd (resources .peakOffscreenBytes , resources .rasterizerBytes )
if !ok {
return renderResources {}, fmt .Errorf ("d2raster: peak offscreen pixel storage exceeds the int64 domain" )
}
return resources , nil
}
func planNodeResources(ctx context .Context , node *preparedNode , dst image .Rectangle , planner *rasterizerResourcePlanner ) (renderResources , error ) {
var resources renderResources
if node == nil || node .opacity == 0 {
return resources , nil
}
if err := ctx .Err (); err != nil {
return resources , err
}
visibleBounds := node .bounds .Intersect (dst )
if visibleBounds .Empty () {
return resources , nil
}
usesFilters := len (node .filters ) != 0
usesEffectLayer := node .isolated || node .opacity < 1 || node .blend != d2scene .BlendNormal || usesFilters || node .clip != nil || node .mask != nil
if planner .regionFilters && usesEffectLayer && !usesFilters {
var err error
visibleBounds , err = effectRenderBounds (ctx , node , dst )
if err != nil {
return resources , err
}
if visibleBounds .Empty () {
return resources , nil
}
}
baseBytes := int64 (0 )
finalLayerBytes := int64 (0 )
paintBounds := dst
if usesFilters {
filters , inputBounds := node .filters , node .contentBounds
var err error
if planner .regionFilters {
filters , inputBounds , err = regionalFilters (filters , inputBounds , visibleBounds )
if err != nil {
return resources , err
}
}
baseBytes , err = pixelStorageBytes (inputBounds , 4 )
if err != nil {
return resources , fmt .Errorf ("d2raster: filter input layer: %w" , err )
}
resources .peakOffscreenBytes , finalLayerBytes , err = planFilterResources (filters , inputBounds )
if err != nil {
return resources , err
}
paintBounds = inputBounds
} else if usesEffectLayer {
var err error
baseBytes , err = pixelStorageBytes (visibleBounds , 4 )
if err != nil {
return resources , fmt .Errorf ("d2raster: effect layer: %w" , err )
}
finalLayerBytes = baseBytes
resources .peakOffscreenBytes = baseBytes
paintBounds = visibleBounds
}
var primitiveResources renderResources
if node .primitive != nil && !node .primitive .bounds .Intersect (paintBounds ).Empty () {
var err error
primitiveResources , err = planPrimitiveResources (ctx , node .primitive , paintBounds , planner )
if err != nil {
return resources , err
}
}
withPrimitive , ok := checkedAdd (baseBytes , primitiveResources .peakOffscreenBytes )
if !ok {
return resources , fmt .Errorf ("d2raster: peak offscreen pixel storage exceeds the int64 domain" )
}
resources .peakOffscreenBytes = maxInt64 (resources .peakOffscreenBytes , withPrimitive )
mergeRasterizerRequirements (&resources , primitiveResources )
if err := addScanlineWork (&resources , primitiveResources .scanlineWork ); err != nil {
return resources , err
}
if err := addResourceWork (&resources , primitiveResources .evenOddClipWork ); err != nil {
return resources , err
}
for _ , child := range node .children {
if err := ctx .Err (); err != nil {
return resources , err
}
childResources , err := planNodeResources (ctx , child , paintBounds , planner )
if err != nil {
return resources , err
}
withChild , ok := checkedAdd (baseBytes , childResources .peakOffscreenBytes )
if !ok {
return resources , fmt .Errorf ("d2raster: peak offscreen pixel storage exceeds the int64 domain" )
}
resources .peakOffscreenBytes = maxInt64 (resources .peakOffscreenBytes , withChild )
mergeRasterizerRequirements (&resources , childResources )
if err := addScanlineWork (&resources , childResources .scanlineWork ); err != nil {
return resources , err
}
if err := addResourceWork (&resources , childResources .evenOddClipWork ); err != nil {
return resources , err
}
}
if node .clip != nil {
if node .clip .fillRule == d2scene .EvenOdd {
clipBytes , err := pixelStorageBytes (visibleBounds , 1 )
if err != nil {
return resources , fmt .Errorf ("d2raster: clip mask: %w" , err )
}
withClip , ok := checkedAdd (finalLayerBytes , clipBytes )
if !ok {
return resources , fmt .Errorf ("d2raster: peak offscreen pixel storage exceeds the int64 domain" )
}
resources .peakOffscreenBytes = maxInt64 (resources .peakOffscreenBytes , withClip )
work , err := evenOddMaskWork (visibleBounds , node .clip .edges )
if err != nil {
return resources , err
}
if err := addResourceWork (&resources , work ); err != nil {
return resources , err
}
} else if !node .clip .bounds .Intersect (visibleBounds ).Empty () {
origin := visibleBounds .Min
if planner .regionFilters {
origin = node .clip .referenceBounds .Min
}
shifted := d2scene .Translate (-float64 (origin .X ), -float64 (origin .Y ))
if err := planner .recordFill (ctx , &resources , visibleBounds , node .clip .subpaths , shifted , "clip" , origin ); err != nil {
return resources , err
}
}
}
if node .mask != nil {
maskBounds , err := maskRenderBounds (ctx , node .mask , visibleBounds , planner .regionFilters )
if err != nil {
return resources , err
}
maskBytes , err := pixelStorageBytes (maskBounds , 4 )
if err != nil {
return resources , fmt .Errorf ("d2raster: scene mask: %w" , err )
}
maskResources , err := planNodeResources (ctx , node .mask .root , maskBounds , planner )
if err != nil {
return resources , err
}
withMask , ok := checkedAdd (finalLayerBytes , maskBytes )
if ok {
withMask , ok = checkedAdd (withMask , maskResources .peakOffscreenBytes )
}
if !ok {
return resources , fmt .Errorf ("d2raster: peak offscreen pixel storage exceeds the int64 domain" )
}
resources .peakOffscreenBytes = maxInt64 (resources .peakOffscreenBytes , withMask )
mergeRasterizerRequirements (&resources , maskResources )
if err := addScanlineWork (&resources , maskResources .scanlineWork ); err != nil {
return resources , err
}
if err := addResourceWork (&resources , maskResources .evenOddClipWork ); err != nil {
return resources , err
}
}
return resources , nil
}
func evenOddMaskWork(bounds image .Rectangle , edges int64 ) (int64 , error ) {
if bounds .Empty () || edges == 0 {
return 0 , nil
}
pixels , ok := checkedMultiply (int64 (bounds .Dx ()), int64 (bounds .Dy ()))
if !ok {
return 0 , fmt .Errorf ("d2raster: even-odd clip work exceeds the int64 domain" )
}
work , ok := checkedMultiply (pixels , evenOddSamplesPerAxis *evenOddSamplesPerAxis )
if ok {
work , ok = checkedMultiply (work , edges )
}
if !ok {
return 0 , fmt .Errorf ("d2raster: even-odd clip work exceeds the int64 domain" )
}
return work , nil
}
func planPrimitiveResources(ctx context .Context , primitive *preparedPrimitive , dst image .Rectangle , planner *rasterizerResourcePlanner ) (renderResources , error ) {
var resources renderResources
if primitive == nil {
return resources , nil
}
if primitive .image != nil {
return resources , nil
}
if primitive .vector != nil {
return planNodeResources (ctx , primitive .vector , dst , planner )
}
if primitive .fill != nil {
bounds := subpathPixelBounds (primitive .subpaths , primitive .transform , 0 , dst )
if primitive .fillRule == d2scene .EvenOdd {
bytes , err := pixelStorageBytes (bounds , 1 )
if err != nil {
return resources , fmt .Errorf ("d2raster: even-odd fill mask: %w" , err )
}
resources .peakOffscreenBytes = bytes
work , err := evenOddMaskWork (bounds , primitive .evenOddEdges )
if err != nil {
return resources , err
}
if err := addResourceWork (&resources , work ); err != nil {
return resources , err
}
} else if primitive .fill .kind == preparedSolidPaint {
origin := dst .Min
if planner .regionFilters {
origin = primitive .referenceBounds .Min
}
shifted := d2scene .Translate (-float64 (origin .X ), -float64 (origin .Y )).Mul (primitive .transform )
if err := planner .recordFill (ctx , &resources , dst , primitive .subpaths , shifted , "solid fill" , origin ); err != nil {
return resources , err
}
} else {
if !bounds .Empty () {
origin := bounds .Min
if planner .regionFilters {
origin = subpathPixelBounds (primitive .subpaths , primitive .transform , 0 , primitive .referenceBounds ).Min
}
shifted := d2scene .Translate (-float64 (origin .X ), -float64 (origin .Y )).Mul (primitive .transform )
if err := planner .recordFill (ctx , &resources , bounds , primitive .subpaths , shifted , "gradient fill" , origin ); err != nil {
return resources , err
}
}
}
}
if primitive .stroke != nil {
if primitive .stroke .paint .kind == preparedSolidPaint {
origin := dst .Min
if planner .regionFilters {
origin = primitive .stroke .referenceBounds .Min
}
shifted := d2scene .Translate (-float64 (origin .X ), -float64 (origin .Y )).Mul (primitive .transform )
if err := planner .recordStroke (ctx , &resources , dst , primitive .strokeRuns , shifted , primitive .stroke , "solid stroke" , origin ); err != nil {
return resources , err
}
} else {
bounds := paintedStrokePixelBounds (primitive .strokeRuns , primitive .transform , primitive .stroke , dst )
if !bounds .Empty () {
origin := bounds .Min
if planner .regionFilters {
origin = paintedStrokePixelBounds (primitive .strokeRuns , primitive .transform , primitive .stroke , primitive .stroke .referenceBounds ).Min
}
shifted := d2scene .Translate (-float64 (origin .X ), -float64 (origin .Y )).Mul (primitive .transform )
if err := planner .recordStroke (ctx , &resources , bounds , primitive .strokeRuns , shifted , primitive .stroke , "gradient stroke" , origin ); err != nil {
return resources , err
}
}
}
}
return resources , nil
}
func mergeRasterizerRequirements(destination *renderResources , source renderResources ) {
destination .rasterizerWidth = max (destination .rasterizerWidth , source .rasterizerWidth )
destination .rasterizerHeight = max (destination .rasterizerHeight , source .rasterizerHeight )
destination .rasterizerEdges = max (destination .rasterizerEdges , source .rasterizerEdges )
updateRasterizerBytes (destination )
}
func recordRasterizerRequirement(resources *renderResources , bounds image .Rectangle ) {
if bounds .Empty () {
return
}
resources .rasterizerWidth = max (resources .rasterizerWidth , bounds .Dx ())
resources .rasterizerHeight = max (resources .rasterizerHeight , bounds .Dy ())
updateRasterizerBytes (resources )
}
func recordRasterizerEdges(resources *renderResources , edges int ) {
resources .rasterizerEdges = max (resources .rasterizerEdges , edges )
updateRasterizerBytes (resources )
}
func updateRasterizerBytes(resources *renderResources ) {
bytes , ok := scanline .RetainedBytes (resources .rasterizerWidth , resources .rasterizerHeight , resources .rasterizerEdges )
if !ok {
resources .rasterizerBytes = math .MaxInt64
return
}
resources .rasterizerBytes = bytes
}
func (p *rasterizerResourcePlanner ) recordFill (ctx context .Context , resources *renderResources , target image .Rectangle , paths []subpath , transform d2scene .Matrix , purpose string , referenceOrigin image .Point ) error {
recordRasterizerRequirement (resources , target )
p .counter .Reset (target .Dx (), target .Dy ())
setRasterizerReference (p .counter , target .Min , referenceOrigin , p .regionFilters )
for index , path := range paths {
if index &255 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
addFillSubpath (p .counter , path , transform )
if err := p .counter .Err (); err != nil {
return rasterizerPlanError (purpose , err )
}
}
edges := p .counter .EdgeCount ()
recordRasterizerEdges (resources , edges )
work , ok := p .counter .WorkBound ()
if !ok {
return fmt .Errorf ("d2raster: %s scanline work exceeds the int64 domain" , purpose )
}
return addScanlineWork (resources , work )
}
func (p *rasterizerResourcePlanner ) recordStroke (ctx context .Context , resources *renderResources , target image .Rectangle , runs []strokeRun , transform d2scene .Matrix , stroke *preparedStroke , purpose string , referenceOrigin image .Point ) error {
recordRasterizerRequirement (resources , target )
p .counter .Reset (target .Dx (), target .Dy ())
setRasterizerReference (p .counter , target .Min , referenceOrigin , p .regionFilters )
for _ , run := range runs {
if err := addStrokeRun (ctx , p .counter , run , transform , stroke ); err != nil {
return err
}
if err := p .counter .Err (); err != nil {
return rasterizerPlanError (purpose , err )
}
}
edges := p .counter .EdgeCount ()
recordRasterizerEdges (resources , edges )
work , ok := p .counter .WorkBound ()
if !ok {
return fmt .Errorf ("d2raster: %s scanline work exceeds the int64 domain" , purpose )
}
return addScanlineWork (resources , work )
}
func rasterizerPlanError(purpose string , err error ) error {
if errors .Is (err , scanline .ErrEdgeLimit ) {
return fmt .Errorf ("d2raster: %s scanline edges exceed the offscreen storage limit: %w" , purpose , err )
}
return fmt .Errorf ("d2raster: %s geometry: %w" , purpose , err )
}
func unionRect(left , right image .Rectangle ) image .Rectangle {
if left .Empty () {
return right
}
if right .Empty () {
return left
}
return left .Union (right )
}
func renderEffectNode(ctx context .Context , dst *image .RGBA , node *preparedNode , scratch *rasterScratch ) error {
if len (node .filters ) != 0 {
return renderFilteredEffectNode (ctx , dst , node , scratch )
}
bounds := node .bounds .Intersect (dst .Bounds ())
if scratch .regionFilters {
var err error
bounds , err = effectRenderBounds (ctx , node , dst .Bounds ())
if err != nil {
return err
}
}
if bounds .Empty () {
return nil
}
layer , layerBytes , err := scratch .offscreen .newRGBA (bounds , "effect layer" )
if err != nil {
return err
}
defer scratch .offscreen .recycleRGBA (layer , layerBytes )
if node .primitive != nil && !node .primitive .bounds .Intersect (bounds ).Empty () {
if err := drawPrimitive (ctx , layer , node .primitive , scratch ); err != nil {
return err
}
}
for _ , child := range node .children {
if err := renderNode (ctx , layer , child , scratch ); err != nil {
return err
}
}
if node .clip != nil {
if err := applyClip (ctx , layer , node .clip , scratch ); err != nil {
return err
}
}
if node .mask != nil {
if err := applyMask (ctx , layer , node .mask , scratch ); err != nil {
return err
}
}
return compositeLayer (ctx , dst , layer , node .opacity , node .blend )
}
func renderFilteredEffectNode(ctx context .Context , dst *image .RGBA , node *preparedNode , scratch *rasterScratch ) error {
visibleBounds := node .bounds .Intersect (dst .Bounds ())
if visibleBounds .Empty () || node .contentBounds .Empty () {
return nil
}
filters , inputBounds := node .filters , node .contentBounds
var err error
if scratch .regionFilters {
filters , inputBounds , err = regionalFilters (filters , inputBounds , visibleBounds )
if err != nil {
return err
}
}
current , err := reserveRGBA (scratch , inputBounds , "filter input layer" )
if err != nil {
return err
}
defer func () { current .release () }()
if node .primitive != nil && !node .primitive .bounds .Intersect (inputBounds ).Empty () {
if err := drawPrimitive (ctx , current .image , node .primitive , scratch ); err != nil {
return err
}
}
for _ , child := range node .children {
if err := renderNode (ctx , current .image , child , scratch ); err != nil {
return err
}
}
for _ , filter := range filters {
if err := applyPreparedFilter (ctx , ¤t , filter , scratch ); err != nil {
return err
}
}
layer := current .image
if layer .Bounds () != visibleBounds {
layer = layer .SubImage (visibleBounds ).(*image .RGBA )
}
if node .clip != nil {
if err := applyClip (ctx , layer , node .clip , scratch ); err != nil {
return err
}
}
if node .mask != nil {
if err := applyMask (ctx , layer , node .mask , scratch ); err != nil {
return err
}
}
return compositeLayer (ctx , dst , layer , node .opacity , node .blend )
}
func applyClip(ctx context .Context , layer *image .RGBA , clip *preparedClip , scratch *rasterScratch ) error {
if clip .fillRule != d2scene .EvenOdd {
return applyNonZeroClip (ctx , layer , clip , scratch )
}
bounds := layer .Bounds ()
maskBounds := image .Rect (0 , 0 , bounds .Dx (), bounds .Dy ())
mask , maskBytes , err := scratch .offscreen .newAlpha (maskBounds , "clip Alpha mask" )
if err != nil {
return err
}
defer scratch .offscreen .recycleAlpha (mask , maskBytes )
if !clip .bounds .Intersect (bounds ).Empty () {
if err := rasterizeEvenOddMask (ctx , mask , bounds .Min , clip .subpaths ); err != nil {
return err
}
}
return multiplyLayerByAlpha (ctx , layer , mask )
}
func applyNonZeroClip(ctx context .Context , layer *image .RGBA , clip *preparedClip , scratch *rasterScratch ) error {
bounds := layer .Bounds ()
width , height := bounds .Dx (), bounds .Dy ()
if width == 0 || height == 0 {
return ctx .Err ()
}
if clip .bounds .Intersect (bounds ).Empty () {
return clearLayerRows (ctx , layer , 0 , height )
}
rasterizer := scratch .reset (image .Rect (0 , 0 , width , height ))
origin := setRasterizerReference (rasterizer , bounds .Min , clip .referenceBounds .Min , scratch .regionFilters )
shifted := d2scene .Translate (-float64 (origin .X ), -float64 (origin .Y ))
for index , path := range clip .subpaths {
if index &255 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
addFillSubpath (rasterizer , path , shifted )
}
nextRow := 0
var callbackErr error
err := rasterizer .WalkCoverage (ctx , scratch .workBudget (), func (y , minX int , partial , difference []float32 ) error {
if err := ctx .Err (); err != nil {
callbackErr = err
return err
}
if err := clearLayerRows (ctx , layer , nextRow , y ); err != nil {
callbackErr = err
return err
}
rowOffset := layer .PixOffset (bounds .Min .X , bounds .Min .Y +y )
row := layer .Pix [rowOffset : rowOffset +width *4 ]
if err := clearLayerPixelRange (ctx , row , 0 , minX ); err != nil {
callbackErr = err
return err
}
var winding float32
for index := 0 ; index < len (partial ); {
if index != 0 && index &4095 == 0 {
if err := ctx .Err (); err != nil {
callbackErr = err
return err
}
}
if partial [index ] == 0 && difference [index ] == 0 {
runEnd := index + 1
for runEnd < len (partial ) && partial [runEnd ] == 0 && difference [runEnd ] == 0 {
runEnd ++
}
if err := scaleLayerCoverageSpan (ctx , row , minX +index , minX +runEnd , scanline .QuantizeCoverage (winding )); err != nil {
callbackErr = err
return err
}
index = runEnd
continue
}
winding += difference [index ]
coverage := scanline .QuantizeCoverage (partial [index ] + winding )
if err := scaleLayerCoverageSpan (ctx , row , minX +index , minX +index +1 , coverage ); err != nil {
callbackErr = err
return err
}
index ++
}
maxX := minX + len (partial )
if err := clearLayerPixelRange (ctx , row , maxX , width ); err != nil {
callbackErr = err
return err
}
nextRow = y + 1
return nil
})
if err != nil {
if callbackErr != nil && err == callbackErr {
return err
}
return fmt .Errorf ("d2raster: clip: %w" , err )
}
return clearLayerRows (ctx , layer , nextRow , height )
}
func clearLayerRows(ctx context .Context , layer *image .RGBA , first , last int ) error {
if first >= last {
return nil
}
width := layer .Bounds ().Dx ()
for y := first ; y < last ; y ++ {
if y == first || (y -first )&31 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
rowOffset := layer .PixOffset (layer .Bounds ().Min .X , layer .Bounds ().Min .Y +y )
row := layer .Pix [rowOffset : rowOffset +width *4 ]
if err := clearLayerPixelRange (ctx , row , 0 , width ); err != nil {
return err
}
}
return ctx .Err ()
}
func clearLayerPixelRange(ctx context .Context , row []byte , first , last int ) error {
for start := first ; start < last ; {
if start != first && (start -first )&4095 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
end := min (start +4096 , last )
clear (row [start *4 : end *4 ])
start = end
}
return nil
}
func scaleLayerCoverageSpan(ctx context .Context , row []byte , first , last int , coverage uint8 ) error {
switch coverage {
case 0 :
return clearLayerPixelRange (ctx , row , first , last )
case 0xff :
return nil
}
for x := first ; x < last ; x ++ {
if x != first && (x -first )&4095 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
offset := x * 4
scalePremultiplied (row [offset :offset +4 ], coverage )
}
return nil
}
func rasterizeEvenOddMask(ctx context .Context , mask *image .Alpha , origin image .Point , paths []subpath ) error {
const sampleCount = evenOddSamplesPerAxis * evenOddSamplesPerAxis
var edgeEvaluations uint64
for y := 0 ; y < mask .Bounds ().Dy (); y ++ {
if y &31 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
row := y * mask .Stride
pixelY := float64 (origin .Y + y )
sampleY0 := pixelY + 0.5 /evenOddSamplesPerAxis
sampleY1 := pixelY + 1.5 /evenOddSamplesPerAxis
sampleY2 := pixelY + 2.5 /evenOddSamplesPerAxis
sampleY3 := pixelY + 3.5 /evenOddSamplesPerAxis
for x := 0 ; x < mask .Bounds ().Dx (); x ++ {
if x &255 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
pixelX := float64 (origin .X + x )
sampleX0 := pixelX + 0.5 /evenOddSamplesPerAxis
sampleX1 := pixelX + 1.5 /evenOddSamplesPerAxis
sampleX2 := pixelX + 2.5 /evenOddSamplesPerAxis
sampleX3 := pixelX + 3.5 /evenOddSamplesPerAxis
inside , err := countEvenOddPathSamples (
ctx , paths ,
sampleX0 , sampleX1 , sampleX2 , sampleX3 ,
sampleY0 , sampleY1 , sampleY2 , sampleY3 ,
&edgeEvaluations ,
)
if err != nil {
return err
}
mask .Pix [row +x ] = uint8 ((inside *255 + sampleCount /2 ) / sampleCount )
}
}
return ctx .Err ()
}
func countEvenOddPathSamples(
ctx context .Context ,
paths []subpath ,
x0 , x1 , x2 , x3 float64 ,
y0 , y1 , y2 , y3 float64 ,
edgeEvaluations *uint64 ,
) (int , error ) {
inside := 0
for _ , y := range [...]float64 {y0 , y1 , y2 , y3 } {
var parity uint8
for _ , path := range paths {
if len (path .points ) < 2 {
continue
}
previous := path .points [len (path .points )-1 ]
for _ , current := range path .points {
*edgeEvaluations += evenOddSamplesPerAxis
if *edgeEvaluations &255 == 0 {
if err := ctx .Err (); err != nil {
return 0 , err
}
}
if (current .Y > y ) != (previous .Y > y ) {
crossingX := (previous .X -current .X )*(y -current .Y )/(previous .Y -current .Y ) + current .X
switch {
case x3 < crossingX :
parity ^= 0b1111
case x2 < crossingX :
parity ^= 0b0111
case x1 < crossingX :
parity ^= 0b0011
case x0 < crossingX :
parity ^= 0b0001
}
}
previous = current
}
}
inside += bits .OnesCount8 (parity )
}
return inside , nil
}
func multiplyLayerByAlpha(ctx context .Context , layer *image .RGBA , mask *image .Alpha ) error {
width , height := layer .Bounds ().Dx (), layer .Bounds ().Dy ()
for y := 0 ; y < height ; y ++ {
if y &31 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
layerOffset := layer .PixOffset (layer .Bounds ().Min .X , layer .Bounds ().Min .Y +y )
maskOffset := mask .PixOffset (mask .Bounds ().Min .X , mask .Bounds ().Min .Y +y )
if width == 0 {
continue
}
layerRow := layer .Pix [layerOffset : layerOffset +width *4 ]
maskRow := mask .Pix [maskOffset : maskOffset +width ]
for x := 0 ; x < width ; {
if x &4095 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
chunkEnd := min ((x |4095 )+1 , width )
useRuns := chunkEnd -x >= 64
if useRuns {
for probe := range 16 {
alpha := maskRow [x +(chunkEnd -x )*probe /16 ]
if alpha != 0 && alpha != 0xff {
useRuns = false
break
}
}
}
if !useRuns {
for ; x < chunkEnd ; x ++ {
pixelOffset := x * 4
scalePremultiplied (layerRow [pixelOffset :pixelOffset +4 ], maskRow [x ])
}
continue
}
alpha := maskRow [x ]
switch alpha {
case 0 :
runStart := x
x ++
for x < chunkEnd && maskRow [x ] == 0 {
x ++
}
clear (layerRow [runStart *4 : x *4 ])
case 0xff :
x ++
for x < chunkEnd && maskRow [x ] == 0xff {
x ++
}
default :
pixelOffset := x * 4
scalePremultiplied (layerRow [pixelOffset :pixelOffset +4 ], alpha )
x ++
}
}
}
return ctx .Err ()
}
func applyMask(ctx context .Context , layer *image .RGBA , mask *preparedMask , scratch *rasterScratch ) error {
bounds , err := maskRenderBounds (ctx , mask , layer .Bounds (), scratch .regionFilters )
if err != nil {
return err
}
if bounds .Empty () {
return ctx .Err ()
}
if bounds != layer .Bounds () {
layer = layer .SubImage (bounds ).(*image .RGBA )
}
maskLayer , maskBytes , err := scratch .offscreen .newRGBA (bounds , "scene mask RGBA layer" )
if err != nil {
return err
}
defer scratch .offscreen .recycleRGBA (maskLayer , maskBytes )
if err := renderNode (ctx , maskLayer , mask .root , scratch ); err != nil {
return err
}
if mask .kind == d2scene .MaskAlpha {
return multiplyLayerByRGBAAlpha (ctx , layer , maskLayer )
}
return multiplyLayerByRGBALuminance (ctx , layer , maskLayer )
}
func multiplyLayerByRGBALuminance(ctx context .Context , layer , mask *image .RGBA ) error {
width , height := layer .Bounds ().Dx (), layer .Bounds ().Dy ()
for y := 0 ; y < height ; y ++ {
if y &31 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
layerOffset := layer .PixOffset (layer .Bounds ().Min .X , layer .Bounds ().Min .Y +y )
maskOffset := mask .PixOffset (mask .Bounds ().Min .X , mask .Bounds ().Min .Y +y )
layerRow := layer .Pix [layerOffset : layerOffset +width *4 ]
maskRow := mask .Pix [maskOffset : maskOffset +width *4 ]
if width < 64 || !binaryLuminancePixel (maskRow , 0 ) {
for x := 0 ; x < width ; x ++ {
if x &4095 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
i := maskOffset + x *4
coverage := uint8 ((2126 *uint32 (mask .Pix [i ]) +
7152 *uint32 (mask .Pix [i +1 ]) +
722 *uint32 (mask .Pix [i +2 ]) + 5000 ) / 10000 )
j := layerOffset + x *4
scalePremultiplied (layer .Pix [j :j +4 ], coverage )
}
continue
}
for x := 0 ; x < width ; {
if x &4095 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
chunkEnd := min ((x |4095 )+1 , width )
if chunkEnd -x < 64 || x != 0 && !binaryLuminancePixel (maskRow , x ) {
for ; x < chunkEnd ; x ++ {
pixelOffset := x * 4
scalePremultiplied (layerRow [pixelOffset :pixelOffset +4 ], rgbaLuminance (maskRow , pixelOffset ))
}
continue
}
binarySamples := 1
span := chunkEnd - x
for probe := 1 ; probe < 16 ; probe ++ {
if binaryLuminancePixel (maskRow , x +(span -1 )*probe /15 ) {
binarySamples ++
}
}
if binarySamples < 7 {
for ; x < chunkEnd ; x ++ {
pixelOffset := x * 4
scalePremultiplied (layerRow [pixelOffset :pixelOffset +4 ], rgbaLuminance (maskRow , pixelOffset ))
}
continue
}
runStart := x
if blackLuminancePixel (maskRow , x *4 ) {
x ++
for x < chunkEnd && blackLuminancePixel (maskRow , x *4 ) {
x ++
}
clear (layerRow [runStart *4 : x *4 ])
} else {
x ++
for x < chunkEnd && whiteLuminancePixel (maskRow , x *4 ) {
x ++
}
}
for x < chunkEnd {
pixelOffset := x * 4
switch rgbaRGB (maskRow , pixelOffset ) {
case 0 :
layerRow [pixelOffset ], layerRow [pixelOffset +1 ], layerRow [pixelOffset +2 ], layerRow [pixelOffset +3 ] = 0 , 0 , 0 , 0
case 0xffffff :
default :
scalePremultiplied (layerRow [pixelOffset :pixelOffset +4 ], rgbaLuminance (maskRow , pixelOffset ))
}
x ++
}
}
}
return ctx .Err ()
}
func rgbaLuminance(pixels []byte , offset int ) uint8 {
return uint8 ((2126 *uint32 (pixels [offset ]) +
7152 *uint32 (pixels [offset +1 ]) +
722 *uint32 (pixels [offset +2 ]) + 5000 ) / 10000 )
}
func binaryLuminancePixel(pixels []byte , pixel int ) bool {
rgb := rgbaRGB (pixels , pixel *4 )
return rgb == 0 || rgb == 0xffffff
}
func blackLuminancePixel(pixels []byte , offset int ) bool {
return rgbaRGB (pixels , offset ) == 0
}
func whiteLuminancePixel(pixels []byte , offset int ) bool {
return rgbaRGB (pixels , offset ) == 0xffffff
}
func rgbaRGB(pixels []byte , offset int ) uint32 {
return uint32 (pixels [offset ]) | uint32 (pixels [offset +1 ])<<8 | uint32 (pixels [offset +2 ])<<16
}
func multiplyLayerByRGBAAlpha(ctx context .Context , layer , mask *image .RGBA ) error {
width , height := layer .Bounds ().Dx (), layer .Bounds ().Dy ()
for y := 0 ; y < height ; y ++ {
if y &31 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
layerOffset := layer .PixOffset (layer .Bounds ().Min .X , layer .Bounds ().Min .Y +y )
maskOffset := mask .PixOffset (mask .Bounds ().Min .X , mask .Bounds ().Min .Y +y )
if width == 0 {
continue
}
layerRow := layer .Pix [layerOffset : layerOffset +width *4 ]
maskRow := mask .Pix [maskOffset : maskOffset +width *4 ]
if width < 64 || maskRow [3 ] != 0 && maskRow [3 ] != 0xff {
for x := 0 ; x < width ; x ++ {
if x &4095 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
pixelOffset := layerOffset + x *4
scalePremultiplied (layer .Pix [pixelOffset :pixelOffset +4 ], mask .Pix [maskOffset +x *4 +3 ])
}
continue
}
for x := 0 ; x < width ; {
if x &4095 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
chunkEnd := min ((x |4095 )+1 , width )
firstAlpha := mask .Pix [maskOffset +x *4 +3 ]
useRuns := chunkEnd -x >= 64 && (firstAlpha == 0 || firstAlpha == 0xff )
if useRuns {
for probe := range 16 {
alpha := mask .Pix [maskOffset +(x +probe )*4 +3 ]
if alpha != 0 && alpha != 0xff {
useRuns = false
break
}
}
}
if !useRuns {
for ; x < chunkEnd ; x ++ {
pixelOffset := layerOffset + x *4
scalePremultiplied (layer .Pix [pixelOffset :pixelOffset +4 ], mask .Pix [maskOffset +x *4 +3 ])
}
continue
}
alpha := maskRow [x *4 +3 ]
switch alpha {
case 0 :
runStart := x
x ++
for x < chunkEnd && maskRow [x *4 +3 ] == 0 {
x ++
}
clear (layerRow [runStart *4 : x *4 ])
case 0xff :
x ++
for x < chunkEnd && maskRow [x *4 +3 ] == 0xff {
x ++
}
default :
pixelOffset := x * 4
scalePremultiplied (layerRow [pixelOffset :pixelOffset +4 ], alpha )
x ++
}
}
}
return ctx .Err ()
}
func scalePremultiplied(pixel []byte , alpha uint8 ) {
if alpha == 255 {
return
}
if alpha == 0 {
pixel [0 ], pixel [1 ], pixel [2 ], pixel [3 ] = 0 , 0 , 0 , 0
return
}
for channel := range 4 {
pixel [channel ] = uint8 ((uint32 (pixel [channel ])*uint32 (alpha ) + 127 ) / 255 )
}
}
func supportedBlendMode(mode d2scene .BlendMode ) bool {
switch mode {
case d2scene .BlendNormal , d2scene .BlendMultiply , d2scene .BlendDarken ,
d2scene .BlendColorBurn , d2scene .BlendOverlay , d2scene .BlendLighten :
return true
default :
return false
}
}
func supportedPreparedBlendMode(mode d2scene .BlendMode ) bool {
return supportedBlendMode (mode ) || mode == preparedCOLRv1SoftLight
}
func compositeLayer(ctx context .Context , dst , layer *image .RGBA , opacity float64 , mode d2scene .BlendMode ) error {
if mode == d2scene .BlendNormal {
if opaquePartialOverPrefixEligible (dst , layer , opacity ) {
return compositeLayerOverOpaquePartialPrefix (ctx , dst , layer , uint32 (math .Round (opacity *255 )))
}
return compositeLayerOver (ctx , dst , layer , opacity )
}
if !supportedPreparedBlendMode (mode ) {
return fmt .Errorf ("d2raster: invalid or unsupported blend mode %d" , mode )
}
if mode == preparedCOLRv1SoftLight {
return compositeCOLRv1SoftLightLayer (ctx , dst , layer , opacity )
}
if opaqueBlendPrefixEligible (dst , layer , opacity ) {
return compositeBlendLayerOpaquePrefix (ctx , dst , layer , mode )
}
return compositeBlendLayer (ctx , dst , layer , opacity , mode )
}
func opaquePartialOverPrefixEligible(dst , layer *image .RGBA , opacity float64 ) bool {
opacityByte := math .Round (opacity * 255 )
if !(opacityByte > 0 && opacityByte < 255 ) {
return false
}
bounds := layer .Bounds ().Intersect (dst .Bounds ())
if bounds .Empty () {
return false
}
firstSource := layer .PixOffset (bounds .Min .X , bounds .Min .Y )
firstDestination := dst .PixOffset (bounds .Min .X , bounds .Min .Y )
return layer .Pix [firstSource +3 ] == 255 && dst .Pix [firstDestination +3 ] == 255
}
func opaqueBlendPrefixEligible(dst , layer *image .RGBA , opacity float64 ) bool {
if opacity == 0 || math .Round (opacity *255 ) != 255 {
return false
}
bounds := layer .Bounds ().Intersect (dst .Bounds ())
if bounds .Empty () {
return false
}
firstSource := layer .PixOffset (bounds .Min .X , bounds .Min .Y )
firstDestination := dst .PixOffset (bounds .Min .X , bounds .Min .Y )
return layer .Pix [firstSource +3 ] == 255 && dst .Pix [firstDestination +3 ] == 255
}
func compositeCOLRv1SoftLightLayer(ctx context .Context , dst , layer *image .RGBA , opacity float64 ) error {
bounds := layer .Bounds ().Intersect (dst .Bounds ())
if bounds .Empty () || opacity == 0 {
return nil
}
opacityScale := math .Round (opacity *255 ) / 255
for y := bounds .Min .Y ; y < bounds .Max .Y ; y ++ {
if (y -bounds .Min .Y )&31 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
destinationOffset := dst .PixOffset (bounds .Min .X , y )
sourceOffset := layer .PixOffset (bounds .Min .X , y )
x := 0
if layer .Pix [sourceOffset +3 ] == 0 {
for ; x < bounds .Dx (); x ++ {
if layer .Pix [sourceOffset +x *4 +3 ] != 0 {
break
}
if x &4095 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
}
}
for ; x < bounds .Dx (); x ++ {
if x &4095 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
si := sourceOffset + x *4
di := destinationOffset + x *4
storedSourceAlpha := float64 (layer .Pix [si +3 ]) / 255
sourceAlpha := storedSourceAlpha * opacityScale
if sourceAlpha == 0 {
continue
}
backdropAlpha := float64 (dst .Pix [di +3 ]) / 255
outputAlpha := sourceAlpha + backdropAlpha *(1 -sourceAlpha )
outputAlphaByte := roundedByte (outputAlpha * 255 )
for channel := range 3 {
sourceLinear := premultipliedSRGBByteToLinear (layer .Pix [si +channel ], layer .Pix [si +3 ])
backdropLinear := premultipliedSRGBByteToLinear (dst .Pix [di +channel ], dst .Pix [di +3 ])
mixedLinear := softLight (backdropLinear , sourceLinear )
outputPremultipliedLinear := sourceAlpha *(1 -backdropAlpha )*sourceLinear +
sourceAlpha *backdropAlpha *mixedLinear +
backdropAlpha *(1 -sourceAlpha )*backdropLinear
encoded := 0.0
if outputAlpha > 0 {
encoded = linearToSRGB (outputPremultipliedLinear / outputAlpha )
}
value := roundedByte (encoded * outputAlpha * 255 )
if value > outputAlphaByte {
value = outputAlphaByte
}
dst .Pix [di +channel ] = value
}
dst .Pix [di +3 ] = outputAlphaByte
}
}
return ctx .Err ()
}
func compositeBlendLayer(ctx context .Context , dst , layer *image .RGBA , opacity float64 , mode d2scene .BlendMode ) error {
bounds := layer .Bounds ().Intersect (dst .Bounds ())
if bounds .Empty () || opacity == 0 {
return nil
}
opacityByte := math .Round (opacity * 255 )
opacityScale := opacityByte / 255
for y := bounds .Min .Y ; y < bounds .Max .Y ; y ++ {
if (y -bounds .Min .Y )&31 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
destinationOffset := dst .PixOffset (bounds .Min .X , y )
sourceOffset := layer .PixOffset (bounds .Min .X , y )
for x := 0 ; x < bounds .Dx (); x ++ {
if x &4095 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
si := sourceOffset + x *4
di := destinationOffset + x *4
sourceAlphaStored := float64 (layer .Pix [si +3 ]) / 255
sourceAlpha := sourceAlphaStored * opacityScale
if sourceAlpha == 0 {
continue
}
backdropAlpha := float64 (dst .Pix [di +3 ]) / 255
outputAlpha := sourceAlpha + backdropAlpha *(1 -sourceAlpha )
outputAlphaByte := roundedByte (outputAlpha * 255 )
for channel := 0 ; channel < 3 ; channel ++ {
sourcePremultiplied := float64 (layer .Pix [si +channel ]) / 255 * opacityScale
backdropPremultiplied := float64 (dst .Pix [di +channel ]) / 255
sourceColor := float64 (layer .Pix [si +channel ]) / float64 (layer .Pix [si +3 ])
backdropColor := 0.0
if dst .Pix [di +3 ] != 0 {
backdropColor = float64 (dst .Pix [di +channel ]) / float64 (dst .Pix [di +3 ])
}
mixed := blendComponent (mode , backdropColor , sourceColor )
outputPremultiplied := sourcePremultiplied *(1 -backdropAlpha ) +
sourceAlpha *backdropAlpha *mixed +
backdropPremultiplied *(1 -sourceAlpha )
value := roundedByte (outputPremultiplied * 255 )
if value > outputAlphaByte {
value = outputAlphaByte
}
dst .Pix [di +channel ] = value
}
dst .Pix [di +3 ] = outputAlphaByte
}
}
return ctx .Err ()
}
func compositeBlendLayerOpaquePrefix(ctx context .Context , dst , layer *image .RGBA , mode d2scene .BlendMode ) error {
bounds := layer .Bounds ().Intersect (dst .Bounds ())
for y := bounds .Min .Y ; y < bounds .Max .Y ; y ++ {
if (y -bounds .Min .Y )&31 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
destinationOffset := dst .PixOffset (bounds .Min .X , y )
sourceOffset := layer .PixOffset (bounds .Min .X , y )
x := 0
if layer .Pix [sourceOffset +3 ] == 0 {
for ; x < bounds .Dx (); x ++ {
if layer .Pix [sourceOffset +x *4 +3 ] != 0 {
break
}
if x &4095 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
}
} else if layer .Pix [sourceOffset +3 ] == 255 && dst .Pix [destinationOffset +3 ] == 255 {
for ; x < bounds .Dx (); x ++ {
si := sourceOffset + x *4
di := destinationOffset + x *4
if layer .Pix [si +3 ] != 255 || dst .Pix [di +3 ] != 255 {
break
}
if x &4095 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
destination := dst .Pix [di : di +4 ]
source := layer .Pix [si : si +4 ]
switch mode {
case d2scene .BlendMultiply :
destination [0 ] = multiplyOpaqueByte (destination [0 ], source [0 ])
destination [1 ] = multiplyOpaqueByte (destination [1 ], source [1 ])
destination [2 ] = multiplyOpaqueByte (destination [2 ], source [2 ])
case d2scene .BlendDarken :
destination [0 ] = min (destination [0 ], source [0 ])
destination [1 ] = min (destination [1 ], source [1 ])
destination [2 ] = min (destination [2 ], source [2 ])
case d2scene .BlendColorBurn :
for channel := range 3 {
backdropColor := float64 (destination [channel ]) / 255
sourceColor := float64 (source [channel ]) / 255
switch {
case backdropColor >= 1 :
destination [channel ] = 255
case sourceColor <= 0 :
destination [channel ] = 0
default :
destination [channel ] = roundedByte ((1 - math .Min (1 , (1 -backdropColor )/sourceColor )) * 255 )
}
}
case d2scene .BlendOverlay :
destination [0 ] = overlayOpaqueByte (destination [0 ], source [0 ])
destination [1 ] = overlayOpaqueByte (destination [1 ], source [1 ])
destination [2 ] = overlayOpaqueByte (destination [2 ], source [2 ])
case d2scene .BlendLighten :
destination [0 ] = max (destination [0 ], source [0 ])
destination [1 ] = max (destination [1 ], source [1 ])
destination [2 ] = max (destination [2 ], source [2 ])
}
destination [3 ] = 255
}
}
for ; x < bounds .Dx (); x ++ {
if x &4095 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
si := sourceOffset + x *4
di := destinationOffset + x *4
sourceAlphaStored := float64 (layer .Pix [si +3 ]) / 255
if sourceAlphaStored == 0 {
continue
}
backdropAlpha := float64 (dst .Pix [di +3 ]) / 255
outputAlpha := sourceAlphaStored + backdropAlpha *(1 -sourceAlphaStored )
outputAlphaByte := roundedByte (outputAlpha * 255 )
for channel := 0 ; channel < 3 ; channel ++ {
sourcePremultiplied := float64 (layer .Pix [si +channel ]) / 255
backdropPremultiplied := float64 (dst .Pix [di +channel ]) / 255
sourceColor := float64 (layer .Pix [si +channel ]) / float64 (layer .Pix [si +3 ])
backdropColor := 0.0
if dst .Pix [di +3 ] != 0 {
backdropColor = float64 (dst .Pix [di +channel ]) / float64 (dst .Pix [di +3 ])
}
mixed := blendComponent (mode , backdropColor , sourceColor )
outputPremultiplied := sourcePremultiplied *(1 -backdropAlpha ) +
sourceAlphaStored *backdropAlpha *mixed +
backdropPremultiplied *(1 -sourceAlphaStored )
value := roundedByte (outputPremultiplied * 255 )
if value > outputAlphaByte {
value = outputAlphaByte
}
dst .Pix [di +channel ] = value
}
dst .Pix [di +3 ] = outputAlphaByte
}
}
return ctx .Err ()
}
func multiplyOpaqueByte(backdrop , source byte ) byte {
return byte ((uint32 (backdrop )*uint32 (source ) + 127 ) / 255 )
}
func overlayOpaqueByte(backdrop , source byte ) byte {
b , s := uint32 (backdrop ), uint32 (source )
if b <= 127 {
return byte ((2 *b *s + 127 ) / 255 )
}
return byte (255 - (2 *(255 -b )*(255 -s )+127 )/255 )
}
func blendComponent(mode d2scene .BlendMode , backdrop , source float64 ) float64 {
switch mode {
case d2scene .BlendMultiply :
return backdrop * source
case d2scene .BlendDarken :
return math .Min (backdrop , source )
case d2scene .BlendColorBurn :
if backdrop >= 1 {
return 1
}
if source <= 0 {
return 0
}
return 1 - math .Min (1 , (1 -backdrop )/source )
case d2scene .BlendOverlay :
if backdrop <= .5 {
return 2 * backdrop * source
}
return 1 - 2 *(1 -backdrop )*(1 -source )
case d2scene .BlendLighten :
return math .Max (backdrop , source )
case preparedCOLRv1SoftLight :
return softLight (backdrop , source )
default :
return source
}
}
func softLight(backdrop , source float64 ) float64 {
if source <= .5 {
return backdrop - (1 -2 *source )*backdrop *(1 -backdrop )
}
d := math .Sqrt (backdrop )
if backdrop <= .25 {
d = ((16 *backdrop -12 )*backdrop + 4 ) * backdrop
}
return backdrop + (2 *source -1 )*(d -backdrop )
}
func roundedByte(value float64 ) uint8 {
if value <= 0 {
return 0
}
if value >= 255 {
return 255
}
return uint8 (math .Round (value ))
}
func compositeLayerOverOpaquePartialPrefix(ctx context .Context , dst , layer *image .RGBA , opacityByte uint32 ) error {
bounds := layer .Bounds ().Intersect (dst .Bounds ())
mul255 := func (left , right uint32 ) uint32 { return (left *right + 127 ) / 255 }
for y := bounds .Min .Y ; y < bounds .Max .Y ; y ++ {
if (y -bounds .Min .Y )&31 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
destinationOffset := dst .PixOffset (bounds .Min .X , y )
sourceOffset := layer .PixOffset (bounds .Min .X , y )
x := 0
if layer .Pix [sourceOffset +3 ] == 255 && dst .Pix [destinationOffset +3 ] == 255 {
for ; x < bounds .Dx (); x ++ {
si := sourceOffset + x *4
di := destinationOffset + x *4
if layer .Pix [si +3 ] != 255 || dst .Pix [di +3 ] != 255 {
break
}
if x &4095 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
dst .Pix [di ] = compositeOpaquePartialOverByte (layer .Pix [si ], dst .Pix [di ], opacityByte )
dst .Pix [di +1 ] = compositeOpaquePartialOverByte (layer .Pix [si +1 ], dst .Pix [di +1 ], opacityByte )
dst .Pix [di +2 ] = compositeOpaquePartialOverByte (layer .Pix [si +2 ], dst .Pix [di +2 ], opacityByte )
dst .Pix [di +3 ] = 255
}
}
for ; x < bounds .Dx (); x ++ {
if x &4095 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
si := sourceOffset + x *4
di := destinationOffset + x *4
sourceAlpha := mul255 (uint32 (layer .Pix [si +3 ]), opacityByte )
if sourceAlpha == 0 {
continue
}
inverseAlpha := 255 - sourceAlpha
for channel := range 3 {
source := mul255 (uint32 (layer .Pix [si +channel ]), opacityByte )
value := source + mul255 (uint32 (dst .Pix [di +channel ]), inverseAlpha )
if value > 255 {
value = 255
}
dst .Pix [di +channel ] = uint8 (value )
}
alpha := sourceAlpha + mul255 (uint32 (dst .Pix [di +3 ]), inverseAlpha )
if alpha > 255 {
alpha = 255
}
dst .Pix [di +3 ] = uint8 (alpha )
}
}
return ctx .Err ()
}
func compositeOpaquePartialOverByte(source , destination byte , opacity uint32 ) byte {
scaledSource := (uint32 (source )*opacity + 127 ) / 255
inverseAlpha := 255 - opacity
value := scaledSource + (uint32 (destination )*inverseAlpha +127 )/255
if value > 255 {
value = 255
}
return byte (value )
}
func compositeLayerOver(ctx context .Context , dst , layer *image .RGBA , opacity float64 ) error {
bounds := layer .Bounds ().Intersect (dst .Bounds ())
if bounds .Empty () || opacity == 0 {
return nil
}
opacityByte := uint32 (math .Round (opacity * 255 ))
mul255 := func (left , right uint32 ) uint32 { return (left *right + 127 ) / 255 }
if opacityByte == 0xff {
for y := bounds .Min .Y ; y < bounds .Max .Y ; y ++ {
if (y -bounds .Min .Y )&31 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
destinationOffset := dst .PixOffset (bounds .Min .X , y )
sourceOffset := layer .PixOffset (bounds .Min .X , y )
blockPixels := 4096
detectUniformBlocks := layer .Pix [sourceOffset +3 ] == 0 || layer .Pix [sourceOffset +3 ] == 0xff
if detectUniformBlocks {
blockPixels = 64
}
for x := 0 ; x < bounds .Dx (); {
if x &4095 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
end := min (x +blockPixels , bounds .Dx ())
blockSource := sourceOffset + x *4
blockDestination := destinationOffset + x *4
blockAlpha := layer .Pix [blockSource +3 ]
if detectUniformBlocks && end -x == blockPixels && (blockAlpha == 0 || blockAlpha == 0xff ) && layer .Pix [blockSource +(blockPixels -1 )*4 +3 ] == blockAlpha {
uniformPixels := blockPixels
for blockX := 1 ; blockX < blockPixels -1 ; blockX ++ {
if layer .Pix [blockSource +blockX *4 +3 ] != blockAlpha {
uniformPixels = blockX
break
}
}
if uniformPixels == blockPixels {
if blockAlpha == 0xff {
copy (dst .Pix [blockDestination :blockDestination +blockPixels *4 ], layer .Pix [blockSource :blockSource +blockPixels *4 ])
}
x = end
continue
}
if blockAlpha == 0 || uniformPixels >= 8 {
if blockAlpha == 0xff {
copy (dst .Pix [blockDestination :blockDestination +uniformPixels *4 ], layer .Pix [blockSource :blockSource +uniformPixels *4 ])
}
x += uniformPixels
}
}
for ; x < end ; x ++ {
si := sourceOffset + x *4
di := destinationOffset + x *4
sourceAlpha := uint32 (layer .Pix [si +3 ])
if sourceAlpha == 0 {
continue
}
if sourceAlpha == 0xff {
dst .Pix [di ], dst .Pix [di +1 ], dst .Pix [di +2 ], dst .Pix [di +3 ] = layer .Pix [si ], layer .Pix [si +1 ], layer .Pix [si +2 ], 0xff
continue
}
inverseAlpha := 255 - sourceAlpha
for channel := 0 ; channel < 3 ; channel ++ {
value := uint32 (layer .Pix [si +channel ]) + mul255 (uint32 (dst .Pix [di +channel ]), inverseAlpha )
if value > 255 {
value = 255
}
dst .Pix [di +channel ] = uint8 (value )
}
alpha := sourceAlpha + mul255 (uint32 (dst .Pix [di +3 ]), inverseAlpha )
if alpha > 255 {
alpha = 255
}
dst .Pix [di +3 ] = uint8 (alpha )
}
}
}
return ctx .Err ()
}
for y := bounds .Min .Y ; y < bounds .Max .Y ; y ++ {
if (y -bounds .Min .Y )&31 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
destinationOffset := dst .PixOffset (bounds .Min .X , y )
sourceOffset := layer .PixOffset (bounds .Min .X , y )
for x := 0 ; x < bounds .Dx (); x ++ {
if x &4095 == 0 {
if err := ctx .Err (); err != nil {
return err
}
}
si := sourceOffset + x *4
di := destinationOffset + x *4
sourceAlpha := mul255 (uint32 (layer .Pix [si +3 ]), opacityByte )
if sourceAlpha == 0 {
continue
}
inverseAlpha := 255 - sourceAlpha
for channel := 0 ; channel < 3 ; channel ++ {
source := mul255 (uint32 (layer .Pix [si +channel ]), opacityByte )
value := source + mul255 (uint32 (dst .Pix [di +channel ]), inverseAlpha )
if value > 255 {
value = 255
}
dst .Pix [di +channel ] = uint8 (value )
}
alpha := sourceAlpha + mul255 (uint32 (dst .Pix [di +3 ]), inverseAlpha )
if alpha > 255 {
alpha = 255
}
dst .Pix [di +3 ] = uint8 (alpha )
}
}
return ctx .Err ()
}
The pages are generated with Golds v0.8.7 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .